From 39638da7baa9c7bf1c4438511c8292812a05c96e Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Fri, 29 Jan 2021 20:38:55 +0000 Subject: [PATCH 01/48] Update debian scripts for 2.11.0 release --- debian/changelog | 10 ++++++++++ debian/control | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/debian/changelog b/debian/changelog index ba9d3b19bc..df66ed9e96 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +darkradiant (2.11.0~focal1) focal; urgency=medium + + * New favourites system for assets like entities, textures and sound shaders. + * Improved text search in resource browsers. + * Ability to list model definitions in model chooser. + * Initial GUI improvements to game connection plugin. + * Various bug fixes. + + -- Matthew Mott Fri, 29 Jan 2021 20:36:34 +0000 + darkradiant (2.10.0~focal1) focal; urgency=medium * New major release on all platforms. diff --git a/debian/control b/debian/control index 6424a2dcd7..34ed150e71 100644 --- a/debian/control +++ b/debian/control @@ -3,7 +3,7 @@ Section: editors Priority: optional Maintainer: Matthew Mott Build-Depends: debhelper (>= 10), cmake (>= 3.12), pkg-config, libxml2-dev, libglew-dev, python-dev, libvorbis-dev, libopenal-dev, libalut-dev, libjpeg-dev, libftgl-dev, libwxbase3.0-dev, libwxgtk3.0-gtk3-dev, libsigc++-2.0-dev -Standards-Version: 4.1.5 +Standards-Version: 4.2.1 Package: darkradiant Architecture: any From 3a72848e14d295899d3298632597689023b65361 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sat, 30 Jan 2021 13:22:34 +0000 Subject: [PATCH 02/48] Add libglib2.0-dev to debian/control GLib is needed for the changes in RadiantApp to intercept and discard unwanted GTK warning messages to console. --- debian/changelog | 2 +- debian/control | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index df66ed9e96..52e777614c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -darkradiant (2.11.0~focal1) focal; urgency=medium +darkradiant (2.11.0~focal2) focal; urgency=medium * New favourites system for assets like entities, textures and sound shaders. * Improved text search in resource browsers. diff --git a/debian/control b/debian/control index 34ed150e71..cd3566b7bf 100644 --- a/debian/control +++ b/debian/control @@ -2,7 +2,7 @@ Source: darkradiant Section: editors Priority: optional Maintainer: Matthew Mott -Build-Depends: debhelper (>= 10), cmake (>= 3.12), pkg-config, libxml2-dev, libglew-dev, python-dev, libvorbis-dev, libopenal-dev, libalut-dev, libjpeg-dev, libftgl-dev, libwxbase3.0-dev, libwxgtk3.0-gtk3-dev, libsigc++-2.0-dev +Build-Depends: debhelper (>= 10), cmake (>= 3.12), pkg-config, libxml2-dev, libglew-dev, python-dev, libvorbis-dev, libopenal-dev, libalut-dev, libjpeg-dev, libftgl-dev, libwxbase3.0-dev, libwxgtk3.0-gtk3-dev, libsigc++-2.0-dev, libglib2.0-dev Standards-Version: 4.2.1 Package: darkradiant From 5c365636d50a14039d22b61a711c3f0dd0f3fea7 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sat, 30 Jan 2021 17:51:05 +0000 Subject: [PATCH 03/48] Merge math tests into a single test suite Instead of four separate top-level test suites cluttering up the list, all of the tests for Matrix, Plane, Quaternion and Vector are in a single MathTest suite. --- test/math/Matrix4.cpp | 38 +++++++++++++++++++------------------- test/math/Plane3.cpp | 12 ++++++------ test/math/Quaternion.cpp | 10 +++++----- test/math/Vector3.cpp | 2 +- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/test/math/Matrix4.cpp b/test/math/Matrix4.cpp index 7757bee1ff..b7488e9e47 100644 --- a/test/math/Matrix4.cpp +++ b/test/math/Matrix4.cpp @@ -14,7 +14,7 @@ namespace double EPSILON = 0.00001f; } -TEST(Matrix4, getIdentity) +TEST(MathTest, IdentityMatrix) { const Matrix4 identity = Matrix4::getIdentity(); @@ -63,7 +63,7 @@ TEST(Matrix4, getIdentity) EXPECT_TRUE(identity == identity2) << "Explicitly constructed identity not equal to Matrix4::getIdentity()"; } -TEST(Matrix4, ConstructByRows) +TEST(MathTest, ConstructMatrixByRows) { auto m = Matrix4::byRows(1, 2.5, 3, 0.34, 51, -6, 7, 9, @@ -99,12 +99,12 @@ TEST(Matrix4, ConstructByRows) EXPECT_EQ(m.translation(), Vector3(0.34, 9, 20)); } -TEST(Matrix4, getRotationAboutXDegrees) +TEST(MathTest, MatrixRotationAboutXDegrees) { double angle = 30.0; double cosAngle = cos(degrees_to_radians(angle)); double sinAngle = sin(degrees_to_radians(angle)); - + // Test X rotation auto xRot = Matrix4::getRotationAboutXDegrees(angle); @@ -112,24 +112,24 @@ TEST(Matrix4, getRotationAboutXDegrees) EXPECT_DOUBLE_EQ(xRot.xy(), 0) << "Matrix rotation constructor failed"; EXPECT_DOUBLE_EQ(xRot.xz(), 0) << "Matrix rotation constructor failed"; EXPECT_DOUBLE_EQ(xRot.xw(), 0) << "Matrix rotation constructor failed"; - + EXPECT_DOUBLE_EQ(xRot.yx(), 0) << "Matrix rotation constructor failed"; EXPECT_DOUBLE_EQ(xRot.yy(), cosAngle) << "Matrix rotation constructor failed"; EXPECT_DOUBLE_EQ(xRot.yz(), sinAngle) << "Matrix rotation constructor failed"; EXPECT_DOUBLE_EQ(xRot.yw(), 0) << "Matrix rotation constructor failed"; - + EXPECT_DOUBLE_EQ(xRot.zx(), 0) << "Matrix rotation constructor failed"; EXPECT_DOUBLE_EQ(xRot.zy(), -sinAngle) << "Matrix rotation constructor failed"; EXPECT_DOUBLE_EQ(xRot.zz(), cosAngle) << "Matrix rotation constructor failed"; EXPECT_DOUBLE_EQ(xRot.zw(), 0) << "Matrix rotation constructor failed"; - + EXPECT_DOUBLE_EQ(xRot.tx(), 0) << "Matrix rotation constructor failed"; EXPECT_DOUBLE_EQ(xRot.ty(), 0) << "Matrix rotation constructor failed"; EXPECT_DOUBLE_EQ(xRot.tz(), 0) << "Matrix rotation constructor failed"; EXPECT_DOUBLE_EQ(xRot.tw(), 1) << "Matrix rotation constructor failed"; } -TEST(Matrix4, getRotationAboutYDegrees) +TEST(MathTest, MatrixRotationAboutYDegrees) { double angle = 30.0; double cosAngle = cos(degrees_to_radians(angle)); @@ -159,7 +159,7 @@ TEST(Matrix4, getRotationAboutYDegrees) EXPECT_DOUBLE_EQ(yRot.tw(), 1) << "Matrix rotation constructor failed"; } -TEST(Matrix4, getRotationAboutZDegrees) +TEST(MathTest, MatrixRotationAboutZDegrees) { double angle = 30.0; double cosAngle = cos(degrees_to_radians(angle)); @@ -189,7 +189,7 @@ TEST(Matrix4, getRotationAboutZDegrees) EXPECT_DOUBLE_EQ(zRot.tw(), 1) <<"Matrix rotation constructor failed"; } -TEST(Matrix4, getRotationForEulerXYZDegrees) +TEST(MathTest, MatrixRotationForEulerXYZDegrees) { // Test euler angle constructors Vector3 euler(30, -55, 75); @@ -233,7 +233,7 @@ TEST(Matrix4, getRotationForEulerXYZDegrees) EXPECT_DOUBLE_EQ(testEuler.z(), euler.z()) << "getEulerAnglesXYZDegrees fault at z()"; } -TEST(Matrix4, getRotationForEulerYZXDegrees) +TEST(MathTest, MatrixRotationForEulerYZXDegrees) { // Test euler angle constructors Vector3 euler(30, -55, 75); @@ -270,7 +270,7 @@ TEST(Matrix4, getRotationForEulerYZXDegrees) EXPECT_DOUBLE_EQ(eulerYZX.tw(), 1) << "Matrix getRotationForEulerYZXDegrees failed"; } -TEST(Matrix4, getRotationForEulerXZYDegrees) +TEST(MathTest, MatrixRotationForEulerXZYDegrees) { // Test euler angle constructors Vector3 euler(30, -55, 75); @@ -307,7 +307,7 @@ TEST(Matrix4, getRotationForEulerXZYDegrees) EXPECT_DOUBLE_EQ(eulerXZY.tw(), 1) << "Matrix getRotationForEulerXZYDegrees failed"; } -TEST(Matrix4, getRotationForEulerYXZDegrees) +TEST(MathTest, MatrixRotationForEulerYXZDegrees) { // Test euler angle constructors Vector3 euler(30, -55, 75); @@ -351,7 +351,7 @@ TEST(Matrix4, getRotationForEulerYXZDegrees) EXPECT_DOUBLE_EQ(testEuler.z(), euler.z()) << "getEulerAnglesYXZDegrees fault at z()"; } -TEST(Matrix4, getRotationForEulerZXYDegrees) +TEST(MathTest, MatrixRotationForEulerZXYDegrees) { // Test euler angle constructors Vector3 euler(30, -55, 75); @@ -395,7 +395,7 @@ TEST(Matrix4, getRotationForEulerZXYDegrees) EXPECT_DOUBLE_EQ(testEuler.z(), euler.z()) << "getEulerAnglesZXYDegrees fault at z()"; } -TEST(Matrix4, getRotationForEulerZYXDegrees) +TEST(MathTest, MatrixRotationForEulerZYXDegrees) { // Test euler angle constructors Vector3 euler(30, -55, 75); @@ -439,7 +439,7 @@ TEST(Matrix4, getRotationForEulerZYXDegrees) EXPECT_DOUBLE_EQ(testEuler.z(), euler.z()) << "getEulerAnglesZYXDegrees fault at z()"; } -TEST(Matrix4, Multiplication) +TEST(MathTest, MatrixMultiplication) { auto a = Matrix4::byColumns(3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59); auto b = Matrix4::byColumns(61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127, 131, 137); @@ -480,7 +480,7 @@ TEST(Matrix4, Multiplication) EXPECT_TRUE(affineA.isAffine()) << "Affine check failed"; } -TEST(Matrix4, Transformation) +TEST(MathTest, MatrixTransformation) { auto a = Matrix4::byColumns(3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59); @@ -515,14 +515,14 @@ TEST(Matrix4, Transformation) EXPECT_EQ(a.t().z(), 53) << "Matrix4::t failed"; } -TEST(Matrix4, getDeterminant) +TEST(MathTest, MatrixDeterminant) { Matrix4 a = Matrix4::byColumns(3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59); EXPECT_EQ(a.getDeterminant(), -448) << "Matrix determinant calculation failed"; } -TEST(Matrix4, getFullInverse) +TEST(MathTest, MatrixFullInverse) { auto a = Matrix4::byColumns(3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59); diff --git a/test/math/Plane3.cpp b/test/math/Plane3.cpp index 6aaceb8277..5aff0c9af2 100644 --- a/test/math/Plane3.cpp +++ b/test/math/Plane3.cpp @@ -12,7 +12,7 @@ namespace const double ONE_OVER_ROOT_TWO = 1.0 / sqrt(2.0); } -TEST(Plane3, ConstructPlane) +TEST(MathTest, ConstructPlane) { Plane3 plane(1, 0, 0, 4); EXPECT_EQ(plane.normal(), Vector3(1, 0, 0)); @@ -27,7 +27,7 @@ TEST(Plane3, ConstructPlane) EXPECT_EQ(copy.dist(), 19.23); } -TEST(Plane3, TranslatePlaneDirectly) +TEST(MathTest, TranslatePlaneDirectly) { // Basic plane Plane3 plane(1, 0, 0, 2.5); @@ -63,7 +63,7 @@ TEST(Plane3, TranslatePlaneDirectly) EXPECT_NEAR(p3_matrix.normal().z(), p3_trans.normal().z(), EPSILON); } -TEST(Plane3, TranslatePlaneWithMatrix) +TEST(MathTest, TranslatePlaneWithMatrix) { // Plane for y = 5 Plane3 plane(0, 1, 0, 5); @@ -97,7 +97,7 @@ TEST(Plane3, TranslatePlaneWithMatrix) EXPECT_EQ(movedZ.dist(), 0); } -TEST(Plane3, RotatePlane) +TEST(MathTest, RotatePlane) { // A plane at 5 in the Y direction Plane3 plane(0, 1, 0, 5); @@ -113,7 +113,7 @@ TEST(Plane3, RotatePlane) EXPECT_EQ(rotated.normal().z(), 0); } -TEST(Plane3, ScalePlane) +TEST(MathTest, ScalePlane) { Plane3 plane(1, -1, 0, 3.5); @@ -128,7 +128,7 @@ TEST(Plane3, ScalePlane) EXPECT_EQ(scaled.dist(), 28); } -TEST(Plane3, TransformPlane) +TEST(MathTest, TransformPlane) { // Check transform with some randomly-generated values with no particular // geometric meaning diff --git a/test/math/Quaternion.cpp b/test/math/Quaternion.cpp index bc74bd9198..c938ca4821 100644 --- a/test/math/Quaternion.cpp +++ b/test/math/Quaternion.cpp @@ -5,7 +5,7 @@ namespace test { -TEST(Quaternion, Multiplication) +TEST(MathTest, QuaternionMultiplication) { Quaternion q1(3, 5, 7, 11); Quaternion q2(13, 17, 19, 23); @@ -18,7 +18,7 @@ TEST(Quaternion, Multiplication) EXPECT_EQ(product.w(), -4) << "Quaternion multiplication failed on w"; } -TEST(Quaternion, InPlaceMultiplication) +TEST(MathTest, QuaternionInPlaceMultiplication) { Quaternion q1(3, 5, 7, 11); Quaternion q2(13, 17, 19, 23); @@ -31,7 +31,7 @@ TEST(Quaternion, InPlaceMultiplication) EXPECT_EQ(q1multiplied.w(), -4) << "Quaternion in-place multiplication failed on w"; } -TEST(Quaternion, getInverse) +TEST(MathTest, QuaternionInverse) { Quaternion q1(3, 5, 7, 11); Quaternion q1inverted = q1.getInverse(); @@ -42,7 +42,7 @@ TEST(Quaternion, getInverse) EXPECT_EQ(q1inverted.w(), 11) << "Quaternion inversion failed on w"; } -TEST(Quaternion, getNormalised) +TEST(MathTest, QuaternionNormalised) { Quaternion q1(3, 5, 7, 11); Quaternion normalised = q1.getNormalised(); @@ -53,7 +53,7 @@ TEST(Quaternion, getNormalised) EXPECT_DOUBLE_EQ(normalised.w(), 0.7701540462154052) << "Quaternion normalisation failed on w"; } -TEST(Quaternion, transformPoint) +TEST(MathTest, QuaternionTransformPoint) { Quaternion q1(3, 5, 7, 11); Vector3 point(13, 17, 19); diff --git a/test/math/Vector3.cpp b/test/math/Vector3.cpp index 0647238796..316a716a50 100644 --- a/test/math/Vector3.cpp +++ b/test/math/Vector3.cpp @@ -5,7 +5,7 @@ namespace test { -TEST(Vector3, Constructor) +TEST(MathTest, ConstructVector3) { Vector3 vec(1.0, 2.0, 3.5); From 628129a0385ee84d3f9bbc26c92b9a1cd29d266e Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sun, 17 Jan 2021 21:10:31 +0000 Subject: [PATCH 04/48] Expose attachment information on IEntityClass Doom3EntityClass was already parsing def_attach information for several years, but this was only ever stored internally without being exposed on the public interface. IEntityClass now offers a forEachAttachment() method which enumerates attached object information, although this method is not yet used anywhere. --- include/ieclass.h | 44 ++++++++++++++++++------- radiantcore/eclass/Doom3EntityClass.cpp | 17 ++++++++-- radiantcore/eclass/Doom3EntityClass.h | 1 + 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/include/ieclass.h b/include/ieclass.h index 31e0f40246..9a384ac1c4 100644 --- a/include/ieclass.h +++ b/include/ieclass.h @@ -226,11 +226,38 @@ class IEntityClass /// Query whether this entity class represents a light. virtual bool isLight() const = 0; - /* ENTITY CLASS SIZE */ + /* ENTITY ATTACHMENTS */ + + /// Details of an attached entity + struct Attachment + { + /// Entity class of the attached entity + std::string eclass; + + /// Vector offset where the attached entity should appear + Vector3 offset; + }; + + /// A functor which can received Attachments + using AttachmentFunc = std::function; /** - * Query whether this entity has a fixed size. + * \brief + * Iterate over attached entities, if any. + * + * Each entity class can define one or more attached entities, which should + * appear at specific offsets relative to the parent entity. Such attached + * entities are for visualisation only, and should not be saved into the + * map as genuine map entities. + * + * \param func + * Functor to receive attachment information. */ + virtual void forEachAttachment(AttachmentFunc func) const = 0; + + /* ENTITY CLASS SIZE */ + + /// Query whether this entity has a fixed size. virtual bool isFixedSize() const = 0; /** @@ -251,16 +278,10 @@ class IEntityClass // Overrides the colour defined in the .def files virtual void setColour(const Vector3& colour) = 0; - /** - * Get the named Shader used for rendering this entity class in - * wireframe mode. - */ + /// Get the shader used for rendering this entity class in wireframe mode. virtual const std::string& getWireShader() const = 0; - /** - * Get the Shader used for rendering this entity class in - * filled mode. - */ + /// Get the shader used for rendering this entity class in filled mode. virtual const std::string& getFillShader() const = 0; @@ -303,8 +324,7 @@ class IEntityClass */ virtual const std::string& getModelPath() const = 0; - /** Get the model skin, or the empty string if there is no skin. - */ + /// Get the model skin, or the empty string if there is no skin. virtual const std::string& getSkin() const = 0; /** diff --git a/radiantcore/eclass/Doom3EntityClass.cpp b/radiantcore/eclass/Doom3EntityClass.cpp index 6873a132eb..ee2cb8ed3b 100644 --- a/radiantcore/eclass/Doom3EntityClass.cpp +++ b/radiantcore/eclass/Doom3EntityClass.cpp @@ -192,6 +192,16 @@ class Doom3EntityClass::Attachments } } } + + // Iterate over attachments + void forEachAttachment(IEntityClass::AttachmentFunc func) + { + for (auto i = _objects.begin(); i != _objects.end(); ++i) + { + IEntityClass::Attachment a; + a.eclass = i->second.className; + } + } }; const std::string Doom3EntityClass::DefaultWireShader("<0.3 0.3 1>"); @@ -237,8 +247,11 @@ sigc::signal& Doom3EntityClass::changedSignal() return _changedSignal; } -/** Query whether this entity has a fixed size. - */ +void Doom3EntityClass::forEachAttachment(AttachmentFunc func) const +{ + _attachments->forEachAttachment(func); +} + bool Doom3EntityClass::isFixedSize() const { if (_fixedSize) { diff --git a/radiantcore/eclass/Doom3EntityClass.h b/radiantcore/eclass/Doom3EntityClass.h index 0e0f0872f2..f68f65f9cf 100644 --- a/radiantcore/eclass/Doom3EntityClass.h +++ b/radiantcore/eclass/Doom3EntityClass.h @@ -159,6 +159,7 @@ class Doom3EntityClass std::string getName() const override; const IEntityClass* getParent() const override; sigc::signal& changedSignal() override; + void forEachAttachment(AttachmentFunc func) const override; bool isFixedSize() const override; AABB getBounds() const override; bool isLight() const override; From f532d6e518e677a6904a5c768e45e91b422c8f09 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Mon, 18 Jan 2021 20:30:39 +0000 Subject: [PATCH 05/48] Merge GenericEntity with GenericEntityNode Remove a largely useless level of indirection by merging these two classes. --- radiantcore/CMakeLists.txt | 1 - radiantcore/entity/generic/GenericEntity.cpp | 239 ------------------ radiantcore/entity/generic/GenericEntity.h | 101 -------- .../entity/generic/GenericEntityNode.cpp | 210 +++++++++++++-- .../entity/generic/GenericEntityNode.h | 65 ++++- 5 files changed, 241 insertions(+), 375 deletions(-) delete mode 100644 radiantcore/entity/generic/GenericEntity.cpp delete mode 100644 radiantcore/entity/generic/GenericEntity.h diff --git a/radiantcore/CMakeLists.txt b/radiantcore/CMakeLists.txt index a0b102c408..b44853949e 100644 --- a/radiantcore/CMakeLists.txt +++ b/radiantcore/CMakeLists.txt @@ -39,7 +39,6 @@ add_library(radiantcore MODULE entity/EntityModule.cpp entity/EntityNode.cpp entity/EntitySettings.cpp - entity/generic/GenericEntity.cpp entity/generic/GenericEntityNode.cpp entity/KeyValue.cpp entity/KeyValueObserver.cpp diff --git a/radiantcore/entity/generic/GenericEntity.cpp b/radiantcore/entity/generic/GenericEntity.cpp deleted file mode 100644 index 5f2dae0454..0000000000 --- a/radiantcore/entity/generic/GenericEntity.cpp +++ /dev/null @@ -1,239 +0,0 @@ -#include "GenericEntity.h" - -#include "iregistry.h" -#include "irenderable.h" -#include "math/Frustum.h" - -#include "../EntitySettings.h" -#include "GenericEntityNode.h" -#include - -namespace entity { - -GenericEntity::GenericEntity(GenericEntityNode& node) : - _owner(node), - m_entity(node._entity), - m_originKey(std::bind(&GenericEntity::originChanged, this)), - m_origin(ORIGINKEY_IDENTITY), - m_angleKey(std::bind(&GenericEntity::angleChanged, this)), - m_angle(AngleKey::IDENTITY), - m_rotationKey(std::bind(&GenericEntity::rotationChanged, this)), - m_arrow(m_ray), - m_aabb_solid(m_aabb_local), - m_aabb_wire(m_aabb_local), - _allow3Drotations(m_entity.getKeyValue("editor_rotatable") == "1") -{} - -GenericEntity::GenericEntity(const GenericEntity& other, - GenericEntityNode& node) : - _owner(node), - m_entity(node._entity), - m_originKey(std::bind(&GenericEntity::originChanged, this)), - m_origin(ORIGINKEY_IDENTITY), - m_angleKey(std::bind(&GenericEntity::angleChanged, this)), - m_angle(AngleKey::IDENTITY), - m_rotationKey(std::bind(&GenericEntity::rotationChanged, this)), - m_arrow(m_ray), - m_aabb_solid(m_aabb_local), - m_aabb_wire(m_aabb_local), - _allow3Drotations(m_entity.getKeyValue("editor_rotatable") == "1") -{} - -GenericEntity::~GenericEntity() -{ - destroy(); -} - -const AABB& GenericEntity::localAABB() const { - return m_aabb_local; -} - -void GenericEntity::renderArrow(const ShaderPtr& shader, RenderableCollector& collector, - const VolumeTest& volume, const Matrix4& localToWorld) const -{ - if (EntitySettings::InstancePtr()->getShowEntityAngles()) - { - collector.addRenderable(*shader, m_arrow, localToWorld); - } -} - -void GenericEntity::renderSolid(RenderableCollector& collector, - const VolumeTest& volume, const Matrix4& localToWorld) const -{ - // greebo: Don't render a filled cube if we have a proper model - const ShaderPtr& shader = _owner.getSolidAABBRenderMode() == GenericEntityNode::WireFrameOnly ? - _owner.getWireShader() : _owner.getFillShader(); - - collector.addRenderable(*shader, m_aabb_solid, localToWorld); - renderArrow(shader, collector, volume, localToWorld); -} - -void GenericEntity::renderWireframe(RenderableCollector& collector, - const VolumeTest& volume, const Matrix4& localToWorld) const -{ - collector.addRenderable(*_owner.getWireShader(), m_aabb_wire, localToWorld); - renderArrow(_owner.getWireShader(), collector, volume, localToWorld); -} - -void GenericEntity::testSelect(Selector& selector, - SelectionTest& test, const Matrix4& localToWorld) -{ - test.BeginMesh(localToWorld); - - SelectionIntersection best; - aabb_testselect(m_aabb_local, test, best); - if(best.isValid()) { - selector.addIntersection(best); - } -} - -void GenericEntity::translate(const Vector3& translation) -{ - m_origin += translation; -} - -void GenericEntity::rotate(const Quaternion& rotation) -{ - if (_allow3Drotations) - { - m_rotation.rotate(rotation); - } - else - { - m_angle = AngleKey::getRotatedValue(m_angle, rotation); - } -} - -void GenericEntity::snapto(float snap) -{ - m_originKey.snap(snap); - m_originKey.write(m_entity); -} - -void GenericEntity::revertTransform() -{ - m_origin = m_originKey.get(); - - if (_allow3Drotations) - { - m_rotation = m_rotationKey.m_rotation; - } - else - { - m_angle = m_angleKey.getValue(); - } -} - -void GenericEntity::freezeTransform() -{ - m_originKey.set(m_origin); - m_originKey.write(m_entity); - - if (_allow3Drotations) - { - m_rotationKey.m_rotation = m_rotation; - m_rotationKey.m_rotation.writeToEntity(&m_entity); - } - else - { - m_angleKey.setValue(m_angle); - m_angleKey.write(&m_entity); - } -} - -void GenericEntity::construct() -{ - m_aabb_local = m_entity.getEntityClass()->getBounds(); - m_ray.origin = m_aabb_local.getOrigin(); - m_ray.direction = Vector3(1, 0, 0); - m_rotation.setIdentity(); - - if (!_allow3Drotations) - { - _angleObserver.setCallback(std::bind(&AngleKey::angleChanged, &m_angleKey, std::placeholders::_1)); - - // Ordinary rotation (2D around z axis), use angle key observer - _owner.addKeyObserver("angle", _angleObserver); - } - else - { - _angleObserver.setCallback(std::bind(&RotationKey::angleChanged, &m_rotationKey, std::placeholders::_1)); - _rotationObserver.setCallback(std::bind(&RotationKey::rotationChanged, &m_rotationKey, std::placeholders::_1)); - - // Full 3D rotations allowed, observe both keys using the rotation key observer - _owner.addKeyObserver("angle", _angleObserver); - _owner.addKeyObserver("rotation", _rotationObserver); - } - - _owner.addKeyObserver("origin", m_originKey); -} - -void GenericEntity::destroy() -{ - if (!_allow3Drotations) - { - // Ordinary rotation (2D around z axis), use angle key observer - _owner.removeKeyObserver("angle", _angleObserver); - } - else - { - // Full 3D rotations allowed, observe both keys using the rotation key observer - _owner.removeKeyObserver("angle", _angleObserver); - _owner.removeKeyObserver("rotation", _rotationObserver); - } - - _owner.removeKeyObserver("origin", m_originKey); -} - -void GenericEntity::updateTransform() -{ - _owner.localToParent() = Matrix4::getTranslation(m_origin); - - if (_allow3Drotations) - { - // greebo: Use the z-direction as base for rotations - m_ray.direction = m_rotation.getMatrix4().transformDirection(Vector3(0,0,1)); - } - else - { - m_ray.direction = Matrix4::getRotationAboutZDegrees(m_angle).transformDirection(Vector3(1, 0, 0)); - } - - _owner.transformChanged(); -} - -void GenericEntity::originChanged() -{ - m_origin = m_originKey.get(); - updateTransform(); -} - -void GenericEntity::angleChanged() -{ - // Ignore the angle key when 3D rotations are enabled - if (_allow3Drotations) return; - - m_angle = m_angleKey.getValue(); - updateTransform(); -} - -void GenericEntity::rotationChanged() -{ - // Ignore the rotation key, when in 2D "angle" mode - if (!_allow3Drotations) return; - - m_rotation = m_rotationKey.m_rotation; - updateTransform(); -} - -const Vector3& GenericEntity::getDirection() const -{ - return m_ray.direction; -} - -const Vector3& GenericEntity::getUntransformedOrigin() const -{ - return m_originKey.get(); -} - -} // namespace entity diff --git a/radiantcore/entity/generic/GenericEntity.h b/radiantcore/entity/generic/GenericEntity.h deleted file mode 100644 index 16d2cede50..0000000000 --- a/radiantcore/entity/generic/GenericEntity.h +++ /dev/null @@ -1,101 +0,0 @@ -#pragma once - -#include "Bounded.h" -#include "editable.h" - -#include "math/Vector3.h" -#include "entitylib.h" - -#include "../OriginKey.h" -#include "../AngleKey.h" -#include "../RotationKey.h" -#include "../Doom3Entity.h" -#include "../KeyObserverDelegate.h" -#include "transformlib.h" - -#include "RenderableArrow.h" - -namespace entity { - -class GenericEntityNode; - -class GenericEntity : - public Bounded, - public Snappable -{ -private: - GenericEntityNode& _owner; - - Doom3Entity& m_entity; - - OriginKey m_originKey; - Vector3 m_origin; - - // The AngleKey wraps around the "angle" spawnarg - AngleKey m_angleKey; - - // This is the "working copy" of the angle value - float m_angle; - - // The RotationKey takes care of the "rotation" spawnarg - RotationKey m_rotationKey; - - // This is the "working copy" of the rotation value - RotationMatrix m_rotation; - - AABB m_aabb_local; - Ray m_ray; - - RenderableArrow m_arrow; - RenderableSolidAABB m_aabb_solid; - RenderableWireframeAABB m_aabb_wire; - - // TRUE if this entity's arrow can be rotated in all directions, - // FALSE if the arrow is caught in the xy plane - bool _allow3Drotations; - - KeyObserverDelegate _rotationObserver; - KeyObserverDelegate _angleObserver; - -public: - // Constructor - GenericEntity(GenericEntityNode& node); - - // Copy constructor - GenericEntity(const GenericEntity& other, - GenericEntityNode& node); - - ~GenericEntity(); - - const AABB& localAABB() const; - - void renderArrow(const ShaderPtr& shader, RenderableCollector& collector, const VolumeTest& volume, const Matrix4& localToWorld) const; - void renderSolid(RenderableCollector& collector, const VolumeTest& volume, const Matrix4& localToWorld) const; - void renderWireframe(RenderableCollector& collector, const VolumeTest& volume, const Matrix4& localToWorld) const; - - void testSelect(Selector& selector, SelectionTest& test, const Matrix4& localToWorld); - - void translate(const Vector3& translation); - void rotate(const Quaternion& rotation); - - void snapto(float snap); - - void revertTransform(); - void freezeTransform(); - - const Vector3& getDirection() const; - const Vector3& getUntransformedOrigin() const; - -public: - - void construct(); - void destroy(); - - void updateTransform(); - - void originChanged(); - void angleChanged(); - void rotationChanged(); -}; - -} // namespace entity diff --git a/radiantcore/entity/generic/GenericEntityNode.cpp b/radiantcore/entity/generic/GenericEntityNode.cpp index de39a2eb24..2fdf35bf24 100644 --- a/radiantcore/entity/generic/GenericEntityNode.cpp +++ b/radiantcore/entity/generic/GenericEntityNode.cpp @@ -1,22 +1,57 @@ #include "GenericEntityNode.h" +#include "../EntitySettings.h" #include "math/Frustum.h" -namespace entity { +namespace entity +{ GenericEntityNode::GenericEntityNode(const IEntityClassPtr& eclass) : EntityNode(eclass), - m_contained(*this), + m_originKey(std::bind(&GenericEntityNode::originChanged, this)), + m_origin(ORIGINKEY_IDENTITY), + m_angleKey(std::bind(&GenericEntityNode::angleChanged, this)), + m_angle(AngleKey::IDENTITY), + m_rotationKey(std::bind(&GenericEntityNode::rotationChanged, this)), + m_arrow(m_ray), + m_aabb_solid(m_aabb_local), + m_aabb_wire(m_aabb_local), + _allow3Drotations(_entity.getKeyValue("editor_rotatable") == "1"), _solidAABBRenderMode(SolidBoxes) {} GenericEntityNode::GenericEntityNode(const GenericEntityNode& other) : EntityNode(other), Snappable(other), - m_contained(other.m_contained, *this), + m_originKey(std::bind(&GenericEntityNode::originChanged, this)), + m_origin(ORIGINKEY_IDENTITY), + m_angleKey(std::bind(&GenericEntityNode::angleChanged, this)), + m_angle(AngleKey::IDENTITY), + m_rotationKey(std::bind(&GenericEntityNode::rotationChanged, this)), + m_arrow(m_ray), + m_aabb_solid(m_aabb_local), + m_aabb_wire(m_aabb_local), + _allow3Drotations(_entity.getKeyValue("editor_rotatable") == "1"), _solidAABBRenderMode(other._solidAABBRenderMode) {} +GenericEntityNode::~GenericEntityNode() +{ + if (!_allow3Drotations) + { + // Ordinary rotation (2D around z axis), use angle key observer + removeKeyObserver("angle", _angleObserver); + } + else + { + // Full 3D rotations allowed, observe both keys using the rotation key observer + removeKeyObserver("angle", _angleObserver); + removeKeyObserver("rotation", _rotationObserver); + } + + removeKeyObserver("origin", m_originKey); +} + GenericEntityNodePtr GenericEntityNode::Create(const IEntityClassPtr& eclass) { GenericEntityNodePtr instance(new GenericEntityNode(eclass)); @@ -29,24 +64,53 @@ void GenericEntityNode::construct() { EntityNode::construct(); - m_contained.construct(); + m_aabb_local = _entity.getEntityClass()->getBounds(); + m_ray.origin = m_aabb_local.getOrigin(); + m_ray.direction = Vector3(1, 0, 0); + m_rotation.setIdentity(); + + if (!_allow3Drotations) + { + _angleObserver.setCallback(std::bind(&AngleKey::angleChanged, &m_angleKey, std::placeholders::_1)); + + // Ordinary rotation (2D around z axis), use angle key observer + addKeyObserver("angle", _angleObserver); + } + else + { + _angleObserver.setCallback(std::bind(&RotationKey::angleChanged, &m_rotationKey, std::placeholders::_1)); + _rotationObserver.setCallback(std::bind(&RotationKey::rotationChanged, &m_rotationKey, std::placeholders::_1)); + + // Full 3D rotations allowed, observe both keys using the rotation key observer + addKeyObserver("angle", _angleObserver); + addKeyObserver("rotation", _rotationObserver); + } + + addKeyObserver("origin", m_originKey); } -// Snappable implementation -void GenericEntityNode::snapto(float snap) { - m_contained.snapto(snap); +void GenericEntityNode::snapto(float snap) +{ + m_originKey.snap(snap); + m_originKey.write(_entity); } -// Bounded implementation -const AABB& GenericEntityNode::localAABB() const { - return m_contained.localAABB(); +const AABB& GenericEntityNode::localAABB() const +{ + return m_aabb_local; } void GenericEntityNode::testSelect(Selector& selector, SelectionTest& test) { EntityNode::testSelect(selector, test); - m_contained.testSelect(selector, test, localToWorld()); + test.BeginMesh(localToWorld()); + + SelectionIntersection best; + aabb_testselect(m_aabb_local, test, best); + if(best.isValid()) { + selector.addIntersection(best); + } } scene::INodePtr GenericEntityNode::clone() const @@ -63,36 +127,109 @@ GenericEntityNode::SolidAAABBRenderMode GenericEntityNode::getSolidAABBRenderMod return _solidAABBRenderMode; } +void GenericEntityNode::renderArrow(const ShaderPtr& shader, RenderableCollector& collector, + const VolumeTest& volume, const Matrix4& localToWorld) const +{ + if (EntitySettings::InstancePtr()->getShowEntityAngles()) + { + collector.addRenderable(*shader, m_arrow, localToWorld); + } +} + void GenericEntityNode::renderSolid(RenderableCollector& collector, const VolumeTest& volume) const { EntityNode::renderSolid(collector, volume); - m_contained.renderSolid(collector, volume, localToWorld()); + const ShaderPtr& shader = getSolidAABBRenderMode() == GenericEntityNode::WireFrameOnly ? + getWireShader() : getFillShader(); + + collector.addRenderable(*shader, m_aabb_solid, localToWorld()); + renderArrow(shader, collector, volume, localToWorld()); } void GenericEntityNode::renderWireframe(RenderableCollector& collector, const VolumeTest& volume) const { EntityNode::renderWireframe(collector, volume); - m_contained.renderWireframe(collector, volume, localToWorld()); + collector.addRenderable(*getWireShader(), m_aabb_wire, localToWorld()); + renderArrow(getWireShader(), collector, volume, localToWorld()); } const Vector3& GenericEntityNode::getDirection() const { - // Return the direction as specified by the angle/rotation keys - return m_contained.getDirection(); + return m_ray.direction; +} + +void GenericEntityNode::rotate(const Quaternion& rotation) +{ + if (_allow3Drotations) + { + m_rotation.rotate(rotation); + } + else + { + m_angle = AngleKey::getRotatedValue(m_angle, rotation); + } +} + +void GenericEntityNode::revertTransform() +{ + m_origin = m_originKey.get(); + + if (_allow3Drotations) + { + m_rotation = m_rotationKey.m_rotation; + } + else + { + m_angle = m_angleKey.getValue(); + } +} + +void GenericEntityNode::freezeTransform() +{ + m_originKey.set(m_origin); + m_originKey.write(_entity); + + if (_allow3Drotations) + { + m_rotationKey.m_rotation = m_rotation; + m_rotationKey.m_rotation.writeToEntity(&_entity); + } + else + { + m_angleKey.setValue(m_angle); + m_angleKey.write(&_entity); + } +} + +void GenericEntityNode::updateTransform() +{ + localToParent() = Matrix4::getTranslation(m_origin); + + if (_allow3Drotations) + { + // greebo: Use the z-direction as base for rotations + m_ray.direction = m_rotation.getMatrix4().transformDirection(Vector3(0,0,1)); + } + else + { + m_ray.direction = Matrix4::getRotationAboutZDegrees(m_angle).transformDirection(Vector3(1, 0, 0)); + } + + transformChanged(); } void GenericEntityNode::_onTransformationChanged() { if (getType() == TRANSFORM_PRIMITIVE) { - m_contained.revertTransform(); + revertTransform(); - m_contained.translate(getTranslation()); - m_contained.rotate(getRotation()); + m_origin += getTranslation(); + rotate(getRotation()); - m_contained.updateTransform(); + updateTransform(); } } @@ -100,18 +237,18 @@ void GenericEntityNode::_applyTransformation() { if (getType() == TRANSFORM_PRIMITIVE) { - m_contained.revertTransform(); + revertTransform(); - m_contained.translate(getTranslation()); - m_contained.rotate(getRotation()); + m_origin += getTranslation(); + rotate(getRotation()); - m_contained.freezeTransform(); + freezeTransform(); } } const Vector3& GenericEntityNode::getUntransformedOrigin() { - return m_contained.getUntransformedOrigin(); + return m_originKey.get(); } void GenericEntityNode::onChildAdded(const scene::INodePtr& child) @@ -155,4 +292,29 @@ void GenericEntityNode::onChildRemoved(const scene::INodePtr& child) }); } +void GenericEntityNode::originChanged() +{ + m_origin = m_originKey.get(); + updateTransform(); +} + +void GenericEntityNode::angleChanged() +{ + // Ignore the angle key when 3D rotations are enabled + if (_allow3Drotations) return; + + m_angle = m_angleKey.getValue(); + updateTransform(); +} + +void GenericEntityNode::rotationChanged() +{ + // Ignore the rotation key, when in 2D "angle" mode + if (!_allow3Drotations) return; + + m_rotation = m_rotationKey.m_rotation; + updateTransform(); +} + + } // namespace entity diff --git a/radiantcore/entity/generic/GenericEntityNode.h b/radiantcore/entity/generic/GenericEntityNode.h index ecd27347fd..43f342b64b 100644 --- a/radiantcore/entity/generic/GenericEntityNode.h +++ b/radiantcore/entity/generic/GenericEntityNode.h @@ -6,10 +6,17 @@ #include "selectionlib.h" #include "transformlib.h" #include "irenderable.h" +#include "math/Ray.h" -#include "GenericEntity.h" #include "../target/TargetableNode.h" #include "../EntityNode.h" +#include "../OriginKey.h" +#include "../AngleKey.h" +#include "../RotationKey.h" +#include "../Doom3Entity.h" +#include "../KeyObserverDelegate.h" + +#include "RenderableArrow.h" namespace entity { @@ -17,13 +24,37 @@ namespace entity class GenericEntityNode; typedef std::shared_ptr GenericEntityNodePtr; -class GenericEntityNode : - public EntityNode, - public Snappable +/// EntityNode subclass for all entity types not handled by a specific class +class GenericEntityNode: public EntityNode, public Snappable { - friend class GenericEntity; + OriginKey m_originKey; + Vector3 m_origin; + + // The AngleKey wraps around the "angle" spawnarg + AngleKey m_angleKey; + + // This is the "working copy" of the angle value + float m_angle; + + // The RotationKey takes care of the "rotation" spawnarg + RotationKey m_rotationKey; + + // This is the "working copy" of the rotation value + RotationMatrix m_rotation; + + AABB m_aabb_local; + Ray m_ray; - GenericEntity m_contained; + RenderableArrow m_arrow; + RenderableSolidAABB m_aabb_solid; + RenderableWireframeAABB m_aabb_wire; + + // TRUE if this entity's arrow can be rotated in all directions, + // FALSE if the arrow is caught in the xy plane + bool _allow3Drotations; + + KeyObserverDelegate _rotationObserver; + KeyObserverDelegate _angleObserver; // Whether to draw a solid/shaded box in full material render mode or just the wireframe enum SolidAAABBRenderMode @@ -36,18 +67,30 @@ class GenericEntityNode : public: GenericEntityNode(const IEntityClassPtr& eclass); + ~GenericEntityNode(); private: GenericEntityNode(const GenericEntityNode& other); + void translate(const Vector3& translation); + void rotate(const Quaternion& rotation); + + void revertTransform(); + void freezeTransform(); + void updateTransform(); + + void originChanged(); + void angleChanged(); + void rotationChanged(); + public: static GenericEntityNodePtr Create(const IEntityClassPtr& eclass); // Snappable implementation - virtual void snapto(float snap) override; + void snapto(float snap) override; // Bounded implementation - virtual const AABB& localAABB() const override; + const AABB& localAABB() const override; // SelectionTestable implementation void testSelect(Selector& selector, SelectionTest& test) override; @@ -55,6 +98,8 @@ class GenericEntityNode : scene::INodePtr clone() const override; // Renderable implementation + void renderArrow(const ShaderPtr& shader, RenderableCollector& collector, + const VolumeTest& volume, const Matrix4& localToWorld) const; void renderSolid(RenderableCollector& collector, const VolumeTest& volume) const override; void renderWireframe(RenderableCollector& collector, const VolumeTest& volume) const override; @@ -66,8 +111,8 @@ class GenericEntityNode : // Returns the original "origin" value const Vector3& getUntransformedOrigin() override; - virtual void onChildAdded(const scene::INodePtr& child) override; - virtual void onChildRemoved(const scene::INodePtr& child) override; + void onChildAdded(const scene::INodePtr& child) override; + void onChildRemoved(const scene::INodePtr& child) override; protected: // Gets called by the Transformable implementation whenever From 0edac7f4cefcbc86d1e99257000067e5464a8331 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Mon, 18 Jan 2021 20:50:31 +0000 Subject: [PATCH 06/48] Rename Doom3Entity to SpawnArgs This is the implementation of Entity, which is an interface entirely concerned with storing and retrieving spawnargs, and really has nothing to do with Doom3 at all. --- radiantcore/CMakeLists.txt | 2 +- radiantcore/entity/EntityModule.cpp | 2 +- radiantcore/entity/EntityNode.cpp | 30 ++++----- radiantcore/entity/EntityNode.h | 5 +- radiantcore/entity/KeyObserverMap.h | 6 +- radiantcore/entity/NameKey.h | 6 +- radiantcore/entity/NamespaceManager.cpp | 2 +- radiantcore/entity/NamespaceManager.h | 6 +- .../entity/{Doom3Entity.cpp => SpawnArgs.cpp} | 67 ++++++++++--------- .../entity/{Doom3Entity.h => SpawnArgs.h} | 30 ++++----- radiantcore/entity/doom3group/Doom3Group.cpp | 34 +++++----- radiantcore/entity/doom3group/Doom3Group.h | 4 +- .../entity/doom3group/Doom3GroupNode.cpp | 12 ++-- .../entity/eclassmodel/EclassModelNode.cpp | 6 +- .../entity/generic/GenericEntityNode.cpp | 14 ++-- .../entity/generic/GenericEntityNode.h | 2 +- radiantcore/entity/light/Light.cpp | 4 +- radiantcore/entity/light/Light.h | 8 +-- radiantcore/entity/light/LightNode.cpp | 4 +- radiantcore/entity/speaker/SpeakerNode.cpp | 16 ++--- radiantcore/entity/target/TargetableNode.cpp | 2 +- radiantcore/entity/target/TargetableNode.h | 10 +-- 22 files changed, 134 insertions(+), 138 deletions(-) rename radiantcore/entity/{Doom3Entity.cpp => SpawnArgs.cpp} (76%) rename radiantcore/entity/{Doom3Entity.h => SpawnArgs.h} (80%) diff --git a/radiantcore/CMakeLists.txt b/radiantcore/CMakeLists.txt index b44853949e..f77b15c9ac 100644 --- a/radiantcore/CMakeLists.txt +++ b/radiantcore/CMakeLists.txt @@ -32,7 +32,7 @@ add_library(radiantcore MODULE entity/curve/Curve.cpp entity/curve/CurveEditInstance.cpp entity/curve/CurveNURBS.cpp - entity/Doom3Entity.cpp + entity/SpawnArgs.cpp entity/doom3group/Doom3Group.cpp entity/doom3group/Doom3GroupNode.cpp entity/eclassmodel/EclassModelNode.cpp diff --git a/radiantcore/entity/EntityModule.cpp b/radiantcore/entity/EntityModule.cpp index a07702ae8e..8f1b390a64 100644 --- a/radiantcore/entity/EntityModule.cpp +++ b/radiantcore/entity/EntityModule.cpp @@ -12,7 +12,7 @@ #include "string/replace.h" -#include "Doom3Entity.h" +#include "SpawnArgs.h" #include "light/LightNode.h" #include "doom3group/Doom3GroupNode.h" diff --git a/radiantcore/entity/EntityNode.cpp b/radiantcore/entity/EntityNode.cpp index 6503f6f179..27a22f8ead 100644 --- a/radiantcore/entity/EntityNode.cpp +++ b/radiantcore/entity/EntityNode.cpp @@ -12,14 +12,14 @@ namespace entity { EntityNode::EntityNode(const IEntityClassPtr& eclass) : - TargetableNode(_entity, *this), + TargetableNode(_spawnArgs, *this), _eclass(eclass), - _entity(_eclass), - _namespaceManager(_entity), - _nameKey(_entity), + _spawnArgs(_eclass), + _namespaceManager(_spawnArgs), + _nameKey(_spawnArgs), _renderableName(_nameKey), _modelKey(*this), - _keyObservers(_entity), + _keyObservers(_spawnArgs), _shaderParms(_keyObservers, _colourKey), _direction(1,0,0) {} @@ -29,17 +29,17 @@ EntityNode::EntityNode(const EntityNode& other) : SelectableNode(other), SelectionTestable(other), Namespaced(other), - TargetableNode(_entity, *this), + TargetableNode(_spawnArgs, *this), Transformable(other), MatrixTransform(other), scene::Cloneable(other), _eclass(other._eclass), - _entity(other._entity), - _namespaceManager(_entity), - _nameKey(_entity), + _spawnArgs(other._spawnArgs), + _namespaceManager(_spawnArgs), + _nameKey(_spawnArgs), _renderableName(_nameKey), _modelKey(*this), - _keyObservers(_entity), + _keyObservers(_spawnArgs), _shaderParms(_keyObservers, _colourKey), _direction(1,0,0) {} @@ -134,7 +134,7 @@ void EntityNode::removeKeyObserver(const std::string& key, KeyObserver& observer Entity& EntityNode::getEntity() { - return _entity; + return _spawnArgs; } void EntityNode::refreshModel() @@ -201,7 +201,7 @@ void EntityNode::onInsertIntoScene(scene::IMapRootNode& root) { GlobalCounters().getCounter(counterEntities).increment(); - _entity.connectUndoSystem(root.getUndoChangeTracker()); + _spawnArgs.connectUndoSystem(root.getUndoChangeTracker()); _modelKey.connectUndoSystem(root.getUndoChangeTracker()); SelectableNode::onInsertIntoScene(root); @@ -214,7 +214,7 @@ void EntityNode::onRemoveFromScene(scene::IMapRootNode& root) SelectableNode::onRemoveFromScene(root); _modelKey.disconnectUndoSystem(root.getUndoChangeTracker()); - _entity.disconnectUndoSystem(root.getUndoChangeTracker()); + _spawnArgs.disconnectUndoSystem(root.getUndoChangeTracker()); GlobalCounters().getCounter(counterEntities).decrement(); } @@ -281,8 +281,8 @@ void EntityNode::acquireShaders(const RenderSystemPtr& renderSystem) { if (renderSystem) { - _fillShader = renderSystem->capture(_entity.getEntityClass()->getFillShader()); - _wireShader = renderSystem->capture(_entity.getEntityClass()->getWireShader()); + _fillShader = renderSystem->capture(_spawnArgs.getEntityClass()->getFillShader()); + _wireShader = renderSystem->capture(_spawnArgs.getEntityClass()->getWireShader()); } else { diff --git a/radiantcore/entity/EntityNode.h b/radiantcore/entity/EntityNode.h index 0cd082ccf5..a7559f5970 100644 --- a/radiantcore/entity/EntityNode.h +++ b/radiantcore/entity/EntityNode.h @@ -41,8 +41,7 @@ class EntityNode : IEntityClassPtr _eclass; // The actual entity (which contains the key/value pairs) - // TODO: Rename this to "spawnargs"? - Doom3Entity _entity; + SpawnArgs _spawnArgs; // The class taking care of all the namespace-relevant stuff NamespaceManager _namespaceManager; @@ -62,7 +61,7 @@ class EntityNode : KeyObserverDelegate _modelKeyObserver; KeyObserverDelegate _skinKeyObserver; - // A helper class managing the collection of KeyObservers attached to the Doom3Entity + // A helper class managing the collection of KeyObservers attached to the SpawnArgs KeyObserverMap _keyObservers; // Helper class observing the "shaderParmNN" spawnargs and caching their values diff --git a/radiantcore/entity/KeyObserverMap.h b/radiantcore/entity/KeyObserverMap.h index a77281755f..60a93efeff 100644 --- a/radiantcore/entity/KeyObserverMap.h +++ b/radiantcore/entity/KeyObserverMap.h @@ -26,7 +26,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include #include -#include "Doom3Entity.h" +#include "SpawnArgs.h" namespace entity { @@ -52,10 +52,10 @@ class KeyObserverMap : KeyObservers _keyObservers; // The observed entity - Doom3Entity& _entity; + SpawnArgs& _entity; public: - KeyObserverMap(Doom3Entity& entity) : + KeyObserverMap(SpawnArgs& entity) : _entity(entity) { // Start observing the entity diff --git a/radiantcore/entity/NameKey.h b/radiantcore/entity/NameKey.h index 5c4da14ddd..da2a810ad8 100644 --- a/radiantcore/entity/NameKey.h +++ b/radiantcore/entity/NameKey.h @@ -24,7 +24,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA #include "entitylib.h" -#include "Doom3Entity.h" +#include "SpawnArgs.h" namespace entity { @@ -32,13 +32,13 @@ class NameKey : public KeyObserver { // The reference to the spawnarg structure - Doom3Entity& m_entity; + SpawnArgs& m_entity; // Cached "name" keyvalue std::string _name; public: - NameKey(Doom3Entity& entity) : + NameKey(SpawnArgs& entity) : m_entity(entity) {} diff --git a/radiantcore/entity/NamespaceManager.cpp b/radiantcore/entity/NamespaceManager.cpp index 473ef7c987..8248fcbdab 100644 --- a/radiantcore/entity/NamespaceManager.cpp +++ b/radiantcore/entity/NamespaceManager.cpp @@ -11,7 +11,7 @@ namespace entity // The registry key pointing towards the "name" spawnarg const char* const GKEY_NAME_KEY("/defaults/nameKey"); -NamespaceManager::NamespaceManager(Doom3Entity& entity) : +NamespaceManager::NamespaceManager(SpawnArgs& entity) : _namespace(nullptr), _entity(entity), _updateMutex(false), diff --git a/radiantcore/entity/NamespaceManager.h b/radiantcore/entity/NamespaceManager.h index 86c9d5eca9..dd89b78970 100644 --- a/radiantcore/entity/NamespaceManager.h +++ b/radiantcore/entity/NamespaceManager.h @@ -4,7 +4,7 @@ #include "inamespace.h" #include -#include "Doom3Entity.h" +#include "SpawnArgs.h" #include "KeyValueObserver.h" #include "NameKeyObserver.h" #include "util/Noncopyable.h" @@ -20,7 +20,7 @@ class NamespaceManager : INamespace* _namespace; // The attached entity - Doom3Entity& _entity; + SpawnArgs& _entity; // All the observed key values of the entity get remembered // This prevents having to traverse all the keyvalues again when changing namespaces @@ -40,7 +40,7 @@ class NamespaceManager : std::string _nameKey; public: - NamespaceManager(Doom3Entity& entity); + NamespaceManager(SpawnArgs& entity); ~NamespaceManager(); diff --git a/radiantcore/entity/Doom3Entity.cpp b/radiantcore/entity/SpawnArgs.cpp similarity index 76% rename from radiantcore/entity/Doom3Entity.cpp rename to radiantcore/entity/SpawnArgs.cpp index 60c8aa2229..8092a32830 100644 --- a/radiantcore/entity/Doom3Entity.cpp +++ b/radiantcore/entity/SpawnArgs.cpp @@ -1,24 +1,25 @@ -#include "Doom3Entity.h" +#include "SpawnArgs.h" #include "ieclass.h" #include "debugging/debugging.h" #include "string/predicate.h" #include -namespace entity { +namespace entity +{ -Doom3Entity::Doom3Entity(const IEntityClassPtr& eclass) : +SpawnArgs::SpawnArgs(const IEntityClassPtr& eclass) : _eclass(eclass), - _undo(_keyValues, std::bind(&Doom3Entity::importState, this, std::placeholders::_1), "EntityKeyValues"), + _undo(_keyValues, std::bind(&SpawnArgs::importState, this, std::placeholders::_1), "EntityKeyValues"), _instanced(false), _observerMutex(false), _isContainer(!eclass->isFixedSize()) {} -Doom3Entity::Doom3Entity(const Doom3Entity& other) : +SpawnArgs::SpawnArgs(const SpawnArgs& other) : Entity(other), _eclass(other.getEntityClass()), - _undo(_keyValues, std::bind(&Doom3Entity::importState, this, std::placeholders::_1), "EntityKeyValues"), + _undo(_keyValues, std::bind(&SpawnArgs::importState, this, std::placeholders::_1), "EntityKeyValues"), _instanced(false), _observerMutex(false), _isContainer(other._isContainer) @@ -31,7 +32,7 @@ Doom3Entity::Doom3Entity(const Doom3Entity& other) : } } -bool Doom3Entity::isModel() const +bool SpawnArgs::isModel() const { std::string name = getKeyValue("name"); std::string model = getKeyValue("model"); @@ -40,12 +41,12 @@ bool Doom3Entity::isModel() const return (classname == "func_static" && !name.empty() && name != model); } -bool Doom3Entity::isOfType(const std::string& className) +bool SpawnArgs::isOfType(const std::string& className) { return _eclass->isOfType(className); } -void Doom3Entity::importState(const KeyValues& keyValues) +void SpawnArgs::importState(const KeyValues& keyValues) { // Remove the entity key values, one by one while (_keyValues.size() > 0) @@ -64,7 +65,7 @@ void Doom3Entity::importState(const KeyValues& keyValues) } } -void Doom3Entity::attachObserver(Observer* observer) +void SpawnArgs::attachObserver(Observer* observer) { ASSERT_MESSAGE(!_observerMutex, "observer cannot be attached during iteration"); @@ -78,7 +79,7 @@ void Doom3Entity::attachObserver(Observer* observer) } } -void Doom3Entity::detachObserver(Observer* observer) +void SpawnArgs::detachObserver(Observer* observer) { ASSERT_MESSAGE(!_observerMutex, "observer cannot be detached during iteration"); @@ -101,7 +102,7 @@ void Doom3Entity::detachObserver(Observer* observer) } } -void Doom3Entity::connectUndoSystem(IMapFileChangeTracker& changeTracker) +void SpawnArgs::connectUndoSystem(IMapFileChangeTracker& changeTracker) { _instanced = true; @@ -113,7 +114,7 @@ void Doom3Entity::connectUndoSystem(IMapFileChangeTracker& changeTracker) _undo.connectUndoSystem(changeTracker); } -void Doom3Entity::disconnectUndoSystem(IMapFileChangeTracker& changeTracker) +void SpawnArgs::disconnectUndoSystem(IMapFileChangeTracker& changeTracker) { _undo.disconnectUndoSystem(changeTracker); @@ -127,12 +128,12 @@ void Doom3Entity::disconnectUndoSystem(IMapFileChangeTracker& changeTracker) /** Return the EntityClass associated with this entity. */ -IEntityClassPtr Doom3Entity::getEntityClass() const +IEntityClassPtr SpawnArgs::getEntityClass() const { return _eclass; } -void Doom3Entity::forEachKeyValue(const KeyValueVisitFunctor& func) const +void SpawnArgs::forEachKeyValue(const KeyValueVisitFunctor& func) const { for (const KeyValuePair& pair : _keyValues) { @@ -140,7 +141,7 @@ void Doom3Entity::forEachKeyValue(const KeyValueVisitFunctor& func) const } } -void Doom3Entity::forEachEntityKeyValue(const EntityKeyValueVisitFunctor& func) +void SpawnArgs::forEachEntityKeyValue(const EntityKeyValueVisitFunctor& func) { for (const KeyValuePair& pair : _keyValues) { @@ -150,7 +151,7 @@ void Doom3Entity::forEachEntityKeyValue(const EntityKeyValueVisitFunctor& func) /** Set a keyvalue on the entity. */ -void Doom3Entity::setKeyValue(const std::string& key, const std::string& value) +void SpawnArgs::setKeyValue(const std::string& key, const std::string& value) { if (value.empty()) { @@ -166,7 +167,7 @@ void Doom3Entity::setKeyValue(const std::string& key, const std::string& value) /** Retrieve a keyvalue from the entity. */ -std::string Doom3Entity::getKeyValue(const std::string& key) const +std::string SpawnArgs::getKeyValue(const std::string& key) const { // Lookup the key in the map KeyValues::const_iterator i = find(key); @@ -183,7 +184,7 @@ std::string Doom3Entity::getKeyValue(const std::string& key) const } } -bool Doom3Entity::isInherited(const std::string& key) const +bool SpawnArgs::isInherited(const std::string& key) const { // Check if we have the key in the local keyvalue map bool definedLocally = (find(key) != _keyValues.end()); @@ -192,7 +193,7 @@ bool Doom3Entity::isInherited(const std::string& key) const return (!definedLocally && !_eclass->getAttribute(key).getValue().empty()); } -Entity::KeyValuePairs Doom3Entity::getKeyValuePairs(const std::string& prefix) const +Entity::KeyValuePairs SpawnArgs::getKeyValuePairs(const std::string& prefix) const { KeyValuePairs list; @@ -210,29 +211,29 @@ Entity::KeyValuePairs Doom3Entity::getKeyValuePairs(const std::string& prefix) c return list; } -EntityKeyValuePtr Doom3Entity::getEntityKeyValue(const std::string& key) +EntityKeyValuePtr SpawnArgs::getEntityKeyValue(const std::string& key) { KeyValues::const_iterator found = find(key); return (found != _keyValues.end()) ? found->second : EntityKeyValuePtr(); } -bool Doom3Entity::isWorldspawn() const +bool SpawnArgs::isWorldspawn() const { return getKeyValue("classname") == "worldspawn"; } -bool Doom3Entity::isContainer() const +bool SpawnArgs::isContainer() const { return _isContainer; } -void Doom3Entity::setIsContainer(bool isContainer) +void SpawnArgs::setIsContainer(bool isContainer) { _isContainer = isContainer; } -void Doom3Entity::notifyInsert(const std::string& key, KeyValue& value) +void SpawnArgs::notifyInsert(const std::string& key, KeyValue& value) { // Block the addition/removal of new Observers during this process _observerMutex = true; @@ -246,7 +247,7 @@ void Doom3Entity::notifyInsert(const std::string& key, KeyValue& value) _observerMutex = false; } -void Doom3Entity::notifyChange(const std::string& k, const std::string& v) +void SpawnArgs::notifyChange(const std::string& k, const std::string& v) { _observerMutex = true; @@ -260,7 +261,7 @@ void Doom3Entity::notifyChange(const std::string& k, const std::string& v) _observerMutex = false; } -void Doom3Entity::notifyErase(const std::string& key, KeyValue& value) +void SpawnArgs::notifyErase(const std::string& key, KeyValue& value) { // Block the addition/removal of new Observers during this process _observerMutex = true; @@ -273,7 +274,7 @@ void Doom3Entity::notifyErase(const std::string& key, KeyValue& value) _observerMutex = false; } -void Doom3Entity::insert(const std::string& key, const KeyValuePtr& keyValue) +void SpawnArgs::insert(const std::string& key, const KeyValuePtr& keyValue) { // Insert the new key at the end of the list KeyValues::iterator i = _keyValues.insert( @@ -290,7 +291,7 @@ void Doom3Entity::insert(const std::string& key, const KeyValuePtr& keyValue) } } -void Doom3Entity::insert(const std::string& key, const std::string& value) +void SpawnArgs::insert(const std::string& key, const std::string& value) { // Try to lookup the key in the map KeyValues::iterator i = find(key); @@ -317,7 +318,7 @@ void Doom3Entity::insert(const std::string& key, const std::string& value) } } -void Doom3Entity::erase(const KeyValues::iterator& i) +void SpawnArgs::erase(const KeyValues::iterator& i) { if (_instanced) { @@ -338,7 +339,7 @@ void Doom3Entity::erase(const KeyValues::iterator& i) // as the std::shared_ptr useCount will reach zero. } -void Doom3Entity::erase(const std::string& key) +void SpawnArgs::erase(const std::string& key) { // Try to lookup the key KeyValues::iterator i = find(key); @@ -350,7 +351,7 @@ void Doom3Entity::erase(const std::string& key) } } -Doom3Entity::KeyValues::const_iterator Doom3Entity::find(const std::string& key) const +SpawnArgs::KeyValues::const_iterator SpawnArgs::find(const std::string& key) const { for (KeyValues::const_iterator i = _keyValues.begin(); i != _keyValues.end(); @@ -366,7 +367,7 @@ Doom3Entity::KeyValues::const_iterator Doom3Entity::find(const std::string& key) return _keyValues.end(); } -Doom3Entity::KeyValues::iterator Doom3Entity::find(const std::string& key) +SpawnArgs::KeyValues::iterator SpawnArgs::find(const std::string& key) { for (KeyValues::iterator i = _keyValues.begin(); i != _keyValues.end(); diff --git a/radiantcore/entity/Doom3Entity.h b/radiantcore/entity/SpawnArgs.h similarity index 80% rename from radiantcore/entity/Doom3Entity.h rename to radiantcore/entity/SpawnArgs.h index 79046c26e9..8e3c5755e9 100644 --- a/radiantcore/entity/Doom3Entity.h +++ b/radiantcore/entity/SpawnArgs.h @@ -4,24 +4,20 @@ #include "KeyValue.h" #include -/** greebo: This is the implementation of the class Entity. +namespace entity { + +/** + * \brief + * Implementation of the class Entity. * - * A Doom3Entity basically just keeps track of all the - * spawnargs and delivers them on request, taking the - * inheritance tree (EntityClasses) into account. + * A SpawnArgs basically just keeps track of all the spawnargs and delivers + * them on request, taking the inheritance tree (EntityClasses) into account. + * The actual rendering and entity behaviour is handled by the EntityNode. * - * It's possible to attach observers to this entity to get - * notified upon key/value changes. + * It's possible to attach observers to this entity to get notified upon + * key/value changes. */ -namespace entity { - -/// \brief An unsorted list of key/value pairs. -/// -/// - Notifies observers when a pair is inserted or removed. -/// - Provides undo support through the global undo system. -/// - New keys are appended to the end of the list. -class Doom3Entity : - public Entity +class SpawnArgs: public Entity { IEntityClassPtr _eclass; @@ -46,10 +42,10 @@ class Doom3Entity : public: // Constructor, pass the according entity class - Doom3Entity(const IEntityClassPtr& eclass); + SpawnArgs(const IEntityClassPtr& eclass); // Copy constructor - Doom3Entity(const Doom3Entity& other); + SpawnArgs(const SpawnArgs& other); void importState(const KeyValues& keyValues); diff --git a/radiantcore/entity/doom3group/Doom3Group.cpp b/radiantcore/entity/doom3group/Doom3Group.cpp index f63b5bce26..7b659f6017 100644 --- a/radiantcore/entity/doom3group/Doom3Group.cpp +++ b/radiantcore/entity/doom3group/Doom3Group.cpp @@ -25,7 +25,7 @@ Doom3Group::Doom3Group( Doom3GroupNode& owner, const Callback& boundsChanged) : _owner(owner), - _entity(_owner._entity), + _spawnArgs(_owner._spawnArgs), m_originKey(std::bind(&Doom3Group::originChanged, this)), m_origin(ORIGINKEY_IDENTITY), m_nameOrigin(0,0,0), @@ -42,7 +42,7 @@ Doom3Group::Doom3Group(const Doom3Group& other, Doom3GroupNode& owner, const Callback& boundsChanged) : _owner(owner), - _entity(_owner._entity), + _spawnArgs(_owner._spawnArgs), m_originKey(std::bind(&Doom3Group::originChanged, this)), m_origin(other.m_origin), m_nameOrigin(other.m_nameOrigin), @@ -126,7 +126,7 @@ void Doom3Group::testSelect(Selector& selector, SelectionTest& test, SelectionIn void Doom3Group::snapOrigin(float snap) { m_originKey.snap(snap); - m_originKey.write(_entity); + m_originKey.write(_spawnArgs); m_renderOrigin.updatePivot(); } @@ -198,7 +198,7 @@ void Doom3Group::scale(const Vector3& scale) void Doom3Group::snapto(float snap) { m_originKey.snap(snap); - m_originKey.write(_entity); + m_originKey.write(_spawnArgs); } void Doom3Group::revertTransform() @@ -221,7 +221,7 @@ void Doom3Group::revertTransform() void Doom3Group::freezeTransform() { m_originKey.set(m_origin); - m_originKey.write(_entity); + m_originKey.write(_spawnArgs); if (!isModel()) { @@ -233,37 +233,37 @@ void Doom3Group::freezeTransform() else { m_rotationKey.m_rotation = m_rotation; - m_rotationKey.write(&_entity, isModel()); + m_rotationKey.write(&_spawnArgs, isModel()); } m_curveNURBS.freezeTransform(); - m_curveNURBS.saveToEntity(_entity); + m_curveNURBS.saveToEntity(_spawnArgs); m_curveCatmullRom.freezeTransform(); - m_curveCatmullRom.saveToEntity(_entity); + m_curveCatmullRom.saveToEntity(_spawnArgs); } void Doom3Group::appendControlPoints(unsigned int numPoints) { if (!m_curveNURBS.isEmpty()) { m_curveNURBS.appendControlPoints(numPoints); - m_curveNURBS.saveToEntity(_entity); + m_curveNURBS.saveToEntity(_spawnArgs); } if (!m_curveCatmullRom.isEmpty()) { m_curveCatmullRom.appendControlPoints(numPoints); - m_curveCatmullRom.saveToEntity(_entity); + m_curveCatmullRom.saveToEntity(_spawnArgs); } } void Doom3Group::convertCurveType() { if (!m_curveNURBS.isEmpty() && m_curveCatmullRom.isEmpty()) { - std::string keyValue = _entity.getKeyValue(curve_Nurbs); - _entity.setKeyValue(curve_Nurbs, ""); - _entity.setKeyValue(curve_CatmullRomSpline, keyValue); + std::string keyValue = _spawnArgs.getKeyValue(curve_Nurbs); + _spawnArgs.setKeyValue(curve_Nurbs, ""); + _spawnArgs.setKeyValue(curve_CatmullRomSpline, keyValue); } else if (!m_curveCatmullRom.isEmpty() && m_curveNURBS.isEmpty()) { - std::string keyValue = _entity.getKeyValue(curve_CatmullRomSpline); - _entity.setKeyValue(curve_CatmullRomSpline, ""); - _entity.setKeyValue(curve_Nurbs, keyValue); + std::string keyValue = _spawnArgs.getKeyValue(curve_CatmullRomSpline); + _spawnArgs.setKeyValue(curve_CatmullRomSpline, ""); + _spawnArgs.setKeyValue(curve_Nurbs, keyValue); } } @@ -323,7 +323,7 @@ void Doom3Group::setIsModel(bool newValue) { */ void Doom3Group::updateIsModel() { - if (m_modelKey != m_name && !_entity.isWorldspawn()) + if (m_modelKey != m_name && !_spawnArgs.isWorldspawn()) { setIsModel(true); diff --git a/radiantcore/entity/doom3group/Doom3Group.h b/radiantcore/entity/doom3group/Doom3Group.h index 1a051ca0c2..8d8e54d1b4 100644 --- a/radiantcore/entity/doom3group/Doom3Group.h +++ b/radiantcore/entity/doom3group/Doom3Group.h @@ -8,7 +8,7 @@ #include "../ModelKey.h" #include "../OriginKey.h" #include "../RotationKey.h" -#include "../Doom3Entity.h" +#include "../SpawnArgs.h" #include "../curve/CurveCatmullRom.h" #include "../curve/CurveNURBS.h" #include "../KeyObserverDelegate.h" @@ -28,7 +28,7 @@ class Doom3Group public Snappable { Doom3GroupNode& _owner; - Doom3Entity& _entity; + SpawnArgs& _spawnArgs; OriginKey m_originKey; Vector3 m_origin; diff --git a/radiantcore/entity/doom3group/Doom3GroupNode.cpp b/radiantcore/entity/doom3group/Doom3GroupNode.cpp index 230718e17f..ac49db40e3 100644 --- a/radiantcore/entity/doom3group/Doom3GroupNode.cpp +++ b/radiantcore/entity/doom3group/Doom3GroupNode.cpp @@ -82,22 +82,22 @@ void Doom3GroupNode::removeSelectedControlPoints() { if (_catmullRomEditInstance.isSelected()) { _catmullRomEditInstance.removeSelectedControlPoints(); - _catmullRomEditInstance.write(curve_CatmullRomSpline, _entity); + _catmullRomEditInstance.write(curve_CatmullRomSpline, _spawnArgs); } if (_nurbsEditInstance.isSelected()) { _nurbsEditInstance.removeSelectedControlPoints(); - _nurbsEditInstance.write(curve_Nurbs, _entity); + _nurbsEditInstance.write(curve_Nurbs, _spawnArgs); } } void Doom3GroupNode::insertControlPointsAtSelected() { if (_catmullRomEditInstance.isSelected()) { _catmullRomEditInstance.insertControlPointsAtSelected(); - _catmullRomEditInstance.write(curve_CatmullRomSpline, _entity); + _catmullRomEditInstance.write(curve_CatmullRomSpline, _spawnArgs); } if (_nurbsEditInstance.isSelected()) { _nurbsEditInstance.insertControlPointsAtSelected(); - _nurbsEditInstance.write(curve_Nurbs, _entity); + _nurbsEditInstance.write(curve_Nurbs, _spawnArgs); } } @@ -207,11 +207,11 @@ const AABB& Doom3GroupNode::getSelectedComponentsBounds() const { void Doom3GroupNode::snapComponents(float snap) { if (_nurbsEditInstance.isSelected()) { _nurbsEditInstance.snapto(snap); - _nurbsEditInstance.write(curve_Nurbs, _entity); + _nurbsEditInstance.write(curve_Nurbs, _spawnArgs); } if (_catmullRomEditInstance.isSelected()) { _catmullRomEditInstance.snapto(snap); - _catmullRomEditInstance.write(curve_CatmullRomSpline, _entity); + _catmullRomEditInstance.write(curve_CatmullRomSpline, _spawnArgs); } if (_originInstance.isSelected()) { _d3Group.snapOrigin(snap); diff --git a/radiantcore/entity/eclassmodel/EclassModelNode.cpp b/radiantcore/entity/eclassmodel/EclassModelNode.cpp index 1867fcbc43..e43cefd5a2 100644 --- a/radiantcore/entity/eclassmodel/EclassModelNode.cpp +++ b/radiantcore/entity/eclassmodel/EclassModelNode.cpp @@ -60,7 +60,7 @@ void EclassModelNode::construct() void EclassModelNode::snapto(float snap) { _originKey.snap(snap); - _originKey.write(_entity); + _originKey.write(_spawnArgs); } const AABB& EclassModelNode::localAABB() const @@ -123,10 +123,10 @@ void EclassModelNode::_revertTransform() void EclassModelNode::_freezeTransform() { _originKey.set(_origin); - _originKey.write(_entity); + _originKey.write(_spawnArgs); _rotationKey.m_rotation = _rotation; - _rotationKey.write(&_entity, true); + _rotationKey.write(&_spawnArgs, true); } void EclassModelNode::_onTransformationChanged() diff --git a/radiantcore/entity/generic/GenericEntityNode.cpp b/radiantcore/entity/generic/GenericEntityNode.cpp index 2fdf35bf24..6ed146b285 100644 --- a/radiantcore/entity/generic/GenericEntityNode.cpp +++ b/radiantcore/entity/generic/GenericEntityNode.cpp @@ -16,7 +16,7 @@ GenericEntityNode::GenericEntityNode(const IEntityClassPtr& eclass) : m_arrow(m_ray), m_aabb_solid(m_aabb_local), m_aabb_wire(m_aabb_local), - _allow3Drotations(_entity.getKeyValue("editor_rotatable") == "1"), + _allow3Drotations(_spawnArgs.getKeyValue("editor_rotatable") == "1"), _solidAABBRenderMode(SolidBoxes) {} @@ -31,7 +31,7 @@ GenericEntityNode::GenericEntityNode(const GenericEntityNode& other) : m_arrow(m_ray), m_aabb_solid(m_aabb_local), m_aabb_wire(m_aabb_local), - _allow3Drotations(_entity.getKeyValue("editor_rotatable") == "1"), + _allow3Drotations(_spawnArgs.getKeyValue("editor_rotatable") == "1"), _solidAABBRenderMode(other._solidAABBRenderMode) {} @@ -64,7 +64,7 @@ void GenericEntityNode::construct() { EntityNode::construct(); - m_aabb_local = _entity.getEntityClass()->getBounds(); + m_aabb_local = _spawnArgs.getEntityClass()->getBounds(); m_ray.origin = m_aabb_local.getOrigin(); m_ray.direction = Vector3(1, 0, 0); m_rotation.setIdentity(); @@ -92,7 +92,7 @@ void GenericEntityNode::construct() void GenericEntityNode::snapto(float snap) { m_originKey.snap(snap); - m_originKey.write(_entity); + m_originKey.write(_spawnArgs); } const AABB& GenericEntityNode::localAABB() const @@ -189,17 +189,17 @@ void GenericEntityNode::revertTransform() void GenericEntityNode::freezeTransform() { m_originKey.set(m_origin); - m_originKey.write(_entity); + m_originKey.write(_spawnArgs); if (_allow3Drotations) { m_rotationKey.m_rotation = m_rotation; - m_rotationKey.m_rotation.writeToEntity(&_entity); + m_rotationKey.m_rotation.writeToEntity(&_spawnArgs); } else { m_angleKey.setValue(m_angle); - m_angleKey.write(&_entity); + m_angleKey.write(&_spawnArgs); } } diff --git a/radiantcore/entity/generic/GenericEntityNode.h b/radiantcore/entity/generic/GenericEntityNode.h index 43f342b64b..c72b1b8c3a 100644 --- a/radiantcore/entity/generic/GenericEntityNode.h +++ b/radiantcore/entity/generic/GenericEntityNode.h @@ -13,7 +13,7 @@ #include "../OriginKey.h" #include "../AngleKey.h" #include "../RotationKey.h" -#include "../Doom3Entity.h" +#include "../SpawnArgs.h" #include "../KeyObserverDelegate.h" #include "RenderableArrow.h" diff --git a/radiantcore/entity/light/Light.cpp b/radiantcore/entity/light/Light.cpp index 6517fdbeff..231de8c12d 100644 --- a/radiantcore/entity/light/Light.cpp +++ b/radiantcore/entity/light/Light.cpp @@ -19,7 +19,7 @@ std::string LightShader::m_defaultShader = ""; // ----- Light Class Implementation ------------------------------------------------- // Constructor -Light::Light(Doom3Entity& entity, +Light::Light(SpawnArgs& entity, LightNode& owner, const Callback& transformChanged, const Callback& boundsChanged, @@ -46,7 +46,7 @@ Light::Light(Doom3Entity& entity, // Copy Constructor Light::Light(const Light& other, LightNode& owner, - Doom3Entity& entity, + SpawnArgs& entity, const Callback& transformChanged, const Callback& boundsChanged, const Callback& lightRadiusChanged) diff --git a/radiantcore/entity/light/Light.h b/radiantcore/entity/light/Light.h index 7cc60fd147..2c5fa7c947 100644 --- a/radiantcore/entity/light/Light.h +++ b/radiantcore/entity/light/Light.h @@ -12,7 +12,7 @@ #include "../RotationKey.h" #include "../ColourKey.h" #include "../ModelKey.h" -#include "../Doom3Entity.h" +#include "../SpawnArgs.h" #include "../KeyObserverDelegate.h" #include "Renderables.h" @@ -55,7 +55,7 @@ class Light: public RendererLight LightNode& _owner; // The parent entity object that uses this light - Doom3Entity& _entity; + SpawnArgs& _entity; OriginKey m_originKey; // The "working" version of the origin @@ -172,7 +172,7 @@ class Light: public RendererLight * \brief * Main constructor. */ - Light(Doom3Entity& entity, + Light(SpawnArgs& entity, LightNode& owner, const Callback& transformChanged, const Callback& boundsChanged, @@ -184,7 +184,7 @@ class Light: public RendererLight */ Light(const Light& other, LightNode& owner, - Doom3Entity& entity, + SpawnArgs& entity, const Callback& transformChanged, const Callback& boundsChanged, const Callback& lightRadiusChanged); diff --git a/radiantcore/entity/light/LightNode.cpp b/radiantcore/entity/light/LightNode.cpp index d57a297984..7e26cee7cd 100644 --- a/radiantcore/entity/light/LightNode.cpp +++ b/radiantcore/entity/light/LightNode.cpp @@ -13,7 +13,7 @@ namespace entity { LightNode::LightNode(const IEntityClassPtr& eclass) : EntityNode(eclass), - _light(_entity, + _light(_spawnArgs, *this, Callback(std::bind(&scene::Node::transformChanged, this)), Callback(std::bind(&scene::Node::boundsChanged, this)), @@ -34,7 +34,7 @@ LightNode::LightNode(const LightNode& other) : ILightNode(other), _light(other._light, *this, - _entity, + _spawnArgs, Callback(std::bind(&Node::transformChanged, this)), Callback(std::bind(&Node::boundsChanged, this)), Callback(std::bind(&LightNode::onLightRadiusChanged, this))), diff --git a/radiantcore/entity/speaker/SpeakerNode.cpp b/radiantcore/entity/speaker/SpeakerNode.cpp index 48addef327..ee550af09a 100644 --- a/radiantcore/entity/speaker/SpeakerNode.cpp +++ b/radiantcore/entity/speaker/SpeakerNode.cpp @@ -59,7 +59,7 @@ void SpeakerNode::construct() { EntityNode::construct(); - m_aabb_local = _entity.getEntityClass()->getBounds(); + m_aabb_local = _spawnArgs.getEntityClass()->getBounds(); m_aabb_border = m_aabb_local; addKeyObserver("origin", m_originKey); @@ -165,7 +165,7 @@ void SpeakerNode::sMaxChanged(const std::string& value) void SpeakerNode::snapto(float snap) { m_originKey.snap(snap); - m_originKey.write(_entity); + m_originKey.write(_spawnArgs); } // Bounded implementation @@ -375,33 +375,33 @@ void SpeakerNode::revertTransform() void SpeakerNode::freezeTransform() { m_originKey.set(m_origin); - m_originKey.write(_entity); + m_originKey.write(_spawnArgs); _radii = _radiiTransformed; // Write the s_mindistance/s_maxdistance keyvalues if we have a valid shader - if (!_entity.getKeyValue(KEY_S_SHADER).empty()) + if (!_spawnArgs.getKeyValue(KEY_S_SHADER).empty()) { // Note: Write the spawnargs in meters if (_radii.getMax() != _defaultRadii.getMax()) { - _entity.setKeyValue(KEY_S_MAXDISTANCE, string::to_string(_radii.getMax(true))); + _spawnArgs.setKeyValue(KEY_S_MAXDISTANCE, string::to_string(_radii.getMax(true))); } else { // Radius is matching default, clear the spawnarg - _entity.setKeyValue(KEY_S_MAXDISTANCE, ""); + _spawnArgs.setKeyValue(KEY_S_MAXDISTANCE, ""); } if (_radii.getMin() != _defaultRadii.getMin()) { - _entity.setKeyValue(KEY_S_MINDISTANCE, string::to_string(_radii.getMin(true))); + _spawnArgs.setKeyValue(KEY_S_MINDISTANCE, string::to_string(_radii.getMin(true))); } else { // Radius is matching default, clear the spawnarg - _entity.setKeyValue(KEY_S_MINDISTANCE, ""); + _spawnArgs.setKeyValue(KEY_S_MINDISTANCE, ""); } } } diff --git a/radiantcore/entity/target/TargetableNode.cpp b/radiantcore/entity/target/TargetableNode.cpp index 4709890fb1..4d6df3b313 100644 --- a/radiantcore/entity/target/TargetableNode.cpp +++ b/radiantcore/entity/target/TargetableNode.cpp @@ -6,7 +6,7 @@ namespace entity { -TargetableNode::TargetableNode(Doom3Entity& entity, EntityNode& node) : +TargetableNode::TargetableNode(SpawnArgs& entity, EntityNode& node) : _d3entity(entity), _targetKeys(*this), _node(node), diff --git a/radiantcore/entity/target/TargetableNode.h b/radiantcore/entity/target/TargetableNode.h index 1dad78e9bd..193bfaf27f 100644 --- a/radiantcore/entity/target/TargetableNode.h +++ b/radiantcore/entity/target/TargetableNode.h @@ -4,7 +4,7 @@ #include "scene/Node.h" #include "entitylib.h" -#include "../Doom3Entity.h" +#include "../SpawnArgs.h" #include "TargetKeyCollection.h" #include "RenderableTargetLines.h" @@ -21,7 +21,7 @@ typedef std::shared_ptr TargetLineNodePtr; * greebo: Each targetable entity (D3Group, Speaker, Lights, etc.) derives from * this class. * - * This registers itself with the contained Doom3Entity and observes its keys. + * This registers itself with the contained SpawnArgs and observes its keys. * As soon as "name" keys are encountered, the TargetManager is notified about * the change, so that the name can be associated with a Target object. */ @@ -29,7 +29,7 @@ class TargetableNode : public Entity::Observer, public KeyObserver { - Doom3Entity& _d3entity; + SpawnArgs& _d3entity; TargetKeyCollection _targetKeys; // The current name of this entity (used for comparison in "onKeyValueChanged") @@ -45,12 +45,12 @@ class TargetableNode : TargetLineNodePtr _targetLineNode; public: - TargetableNode(Doom3Entity& entity, EntityNode& node); + TargetableNode(SpawnArgs& entity, EntityNode& node); // This might return nullptr if the node is not inserted in a scene ITargetManager* getTargetManager(); - // Connect this class with the Doom3Entity + // Connect this class with the SpawnArgs void construct(); // Disconnect this class from the entity void destruct(); From a78163a28109f105e867379a478571b0ce983763 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Tue, 19 Jan 2021 20:40:52 +0000 Subject: [PATCH 07/48] Add .vscode and test/drtest to .gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 0c86a36f4a..6ce2a223bf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ cmake_install.cmake /install_manifest.txt /radiant/darkradiant /install/darkradiant.desktop +/test/drtest *.o *.lo *.so @@ -37,6 +38,9 @@ DarkRadiant.sdf gtkrc testsuite.ilk +# VS Code +.vscode + # VS 2012 /winbuild /tools\msvc2012/ipch From a08b50adedaa51251772f720e4c957eeeeefb301 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sat, 23 Jan 2021 13:04:46 +0000 Subject: [PATCH 08/48] Minor code cleanups - Non-const Doom3EntityClass::getAttribute() now uses const_cast and std::add_const to call the const version (which is safe, unlike having a const method call a non-const method which is UB). The StackOverflow consensus is while this is a somewhat ugly line of code, it is better than repeating several lines in a method body. - Add 'override' to various public methods (latest CLang++ actually warns about this). --- radiantcore/eclass/Doom3EntityClass.cpp | 9 ++------- radiantcore/eclass/Doom3EntityClass.h | 2 +- radiantcore/entity/SpawnArgs.h | 3 +-- radiantcore/entity/generic/GenericEntityNode.h | 4 ++-- radiantcore/entity/light/Light.h | 2 +- radiantcore/entity/light/LightNode.h | 2 +- 6 files changed, 8 insertions(+), 14 deletions(-) diff --git a/radiantcore/eclass/Doom3EntityClass.cpp b/radiantcore/eclass/Doom3EntityClass.cpp index ee2cb8ed3b..0cb34f8ba3 100644 --- a/radiantcore/eclass/Doom3EntityClass.cpp +++ b/radiantcore/eclass/Doom3EntityClass.cpp @@ -491,11 +491,7 @@ std::string Doom3EntityClass::getDefFileName() // Find a single attribute EntityClassAttribute& Doom3EntityClass::getAttribute(const std::string& name) { - StringPtr ref(new std::string(name)); - - EntityAttributeMap::iterator f = _attributes.find(ref); - - return (f != _attributes.end()) ? f->second : _emptyAttribute; + return const_cast(std::as_const(*this).getAttribute(name)); } // Find a single attribute @@ -503,8 +499,7 @@ const EntityClassAttribute& Doom3EntityClass::getAttribute(const std::string& na { StringPtr ref(new std::string(name)); - EntityAttributeMap::const_iterator f = _attributes.find(ref); - + auto f = _attributes.find(ref); return (f != _attributes.end()) ? f->second : _emptyAttribute; } diff --git a/radiantcore/eclass/Doom3EntityClass.h b/radiantcore/eclass/Doom3EntityClass.h index f68f65f9cf..898e9815ca 100644 --- a/radiantcore/eclass/Doom3EntityClass.h +++ b/radiantcore/eclass/Doom3EntityClass.h @@ -204,7 +204,7 @@ class Doom3EntityClass /** * Return the mod name. */ - std::string getModName() const { + std::string getModName() const override { return _modName; } diff --git a/radiantcore/entity/SpawnArgs.h b/radiantcore/entity/SpawnArgs.h index 8e3c5755e9..a8b9107114 100644 --- a/radiantcore/entity/SpawnArgs.h +++ b/radiantcore/entity/SpawnArgs.h @@ -7,8 +7,7 @@ namespace entity { /** - * \brief - * Implementation of the class Entity. + * \brief Implementation of the class Entity. * * A SpawnArgs basically just keeps track of all the spawnargs and delivers * them on request, taking the inheritance tree (EntityClasses) into account. diff --git a/radiantcore/entity/generic/GenericEntityNode.h b/radiantcore/entity/generic/GenericEntityNode.h index c72b1b8c3a..d845f46efe 100644 --- a/radiantcore/entity/generic/GenericEntityNode.h +++ b/radiantcore/entity/generic/GenericEntityNode.h @@ -75,8 +75,8 @@ class GenericEntityNode: public EntityNode, public Snappable void translate(const Vector3& translation); void rotate(const Quaternion& rotation); - void revertTransform(); - void freezeTransform(); + void revertTransform() override; + void freezeTransform() override; void updateTransform(); void originChanged(); diff --git a/radiantcore/entity/light/Light.h b/radiantcore/entity/light/Light.h index 2c5fa7c947..d445e470ca 100644 --- a/radiantcore/entity/light/Light.h +++ b/radiantcore/entity/light/Light.h @@ -192,7 +192,7 @@ class Light: public RendererLight ~Light(); const AABB& localAABB() const; - AABB lightAABB() const; + AABB lightAABB() const override; // Note: move this upwards mutable Matrix4 m_projectionOrientation; diff --git a/radiantcore/entity/light/LightNode.h b/radiantcore/entity/light/LightNode.h index 71df940c00..08ac3b4a28 100644 --- a/radiantcore/entity/light/LightNode.h +++ b/radiantcore/entity/light/LightNode.h @@ -114,7 +114,7 @@ class LightNode : void renderComponents(RenderableCollector& collector, const VolumeTest& volume) const override; // OpenGLRenderable implementation - void render(const RenderInfo& info) const; + void render(const RenderInfo& info) const override; const Matrix4& rotation() const; From 3a2cf88c53e000191b86e7ba8ba79a93bfa748f1 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sun, 24 Jan 2021 14:09:32 +0000 Subject: [PATCH 09/48] Move attachment information from IEntityClass to Entity Attachments are per-entity, not per-entity class (they could even be defined manually via spawnargs on a single entity), so this information needs to be on the Entity/SpawnArgs objects. --- include/ieclass.h | 36 +---- include/ientity.h | 28 ++++ radiantcore/CMakeLists.txt | 1 + radiantcore/eclass/Doom3EntityClass.cpp | 206 ------------------------ radiantcore/eclass/Doom3EntityClass.h | 5 - radiantcore/entity/AttachmentData.cpp | 104 ++++++++++++ radiantcore/entity/AttachmentData.h | 117 ++++++++++++++ radiantcore/entity/SpawnArgs.cpp | 11 +- radiantcore/entity/SpawnArgs.h | 19 +-- 9 files changed, 269 insertions(+), 258 deletions(-) create mode 100644 radiantcore/entity/AttachmentData.cpp create mode 100644 radiantcore/entity/AttachmentData.h diff --git a/include/ieclass.h b/include/ieclass.h index 9a384ac1c4..e048d51eaa 100644 --- a/include/ieclass.h +++ b/include/ieclass.h @@ -200,9 +200,10 @@ typedef std::shared_ptr IEntityClassPtr; typedef std::shared_ptr IEntityClassConstPtr; /** - * Entity class interface. An entity class represents a single type - * of entity that can be created by the EntityCreator. Entity classes are parsed - * from .DEF files during startup. + * \brief Entity class interface. + * + * An entity class represents a single type of entity that can be created by + * the EntityCreator. Entity classes are parsed from .DEF files during startup. * * Entity class attribute names are compared case-insensitively, as in the * Entity class. @@ -226,35 +227,6 @@ class IEntityClass /// Query whether this entity class represents a light. virtual bool isLight() const = 0; - /* ENTITY ATTACHMENTS */ - - /// Details of an attached entity - struct Attachment - { - /// Entity class of the attached entity - std::string eclass; - - /// Vector offset where the attached entity should appear - Vector3 offset; - }; - - /// A functor which can received Attachments - using AttachmentFunc = std::function; - - /** - * \brief - * Iterate over attached entities, if any. - * - * Each entity class can define one or more attached entities, which should - * appear at specific offsets relative to the parent entity. Such attached - * entities are for visualisation only, and should not be saved into the - * map as genuine map entities. - * - * \param func - * Functor to receive attachment information. - */ - virtual void forEachAttachment(AttachmentFunc func) const = 0; - /* ENTITY CLASS SIZE */ /// Query whether this entity has a fixed size. diff --git a/include/ientity.h b/include/ientity.h index 4510f29eeb..04e1a8b635 100644 --- a/include/ientity.h +++ b/include/ientity.h @@ -209,6 +209,34 @@ class Entity * given entity class name. className is treated case-sensitively. */ virtual bool isOfType(const std::string& className) = 0; + + /* ENTITY ATTACHMENTS */ + + /// Details of an attached entity + struct Attachment + { + /// Entity class of the attached entity + std::string eclass; + + /// Vector offset where the attached entity should appear + Vector3 offset; + }; + + /// A functor which can receive Attachment objects + using AttachmentFunc = std::function; + + /** + * \brief Iterate over attached entities, if any. + * + * Each entity can define one or more attached entities, which should + * appear at specific offsets relative to the parent entity. Such attached + * entities are for visualisation only, and should not be saved into the + * map as genuine map entities. + * + * \param func + * Functor to receive attachment information. + */ + virtual void forEachAttachment(AttachmentFunc func) const = 0; }; /// Interface for a INode subclass that contains an Entity diff --git a/radiantcore/CMakeLists.txt b/radiantcore/CMakeLists.txt index f77b15c9ac..56a43c06ac 100644 --- a/radiantcore/CMakeLists.txt +++ b/radiantcore/CMakeLists.txt @@ -28,6 +28,7 @@ add_library(radiantcore MODULE eclass/EClassColourManager.cpp eclass/EClassManager.cpp entity/AngleKey.cpp + entity/AttachmentData.cpp entity/curve/CurveCatmullRom.cpp entity/curve/Curve.cpp entity/curve/CurveEditInstance.cpp diff --git a/radiantcore/eclass/Doom3EntityClass.cpp b/radiantcore/eclass/Doom3EntityClass.cpp index 0cb34f8ba3..d016e21951 100644 --- a/radiantcore/eclass/Doom3EntityClass.cpp +++ b/radiantcore/eclass/Doom3EntityClass.cpp @@ -11,199 +11,6 @@ namespace eclass { -namespace -{ - -// Constants -const std::string DEF_ATTACH = "def_attach"; -const std::string NAME_ATTACH = "name_attach"; -const std::string POS_ATTACH = "pos_attach"; - -const std::string ATTACH_POS_NAME = "attach_pos_name"; -const std::string ATTACH_POS_ORIGIN = "attach_pos_origin"; -const std::string ATTACH_POS_JOINT = "attach_pos_joint"; -const std::string ATTACH_POS_ANGLES = "attach_pos_angles"; - -// Extract and return the string suffix for a key (which might be the empty -// string if there is no suffix). Returns false if the key did not match -// the prefix. -bool tryGetSuffixedKey(const std::string& key, const std::string& prefix, std::string& suffixedOutput) -{ - if (string::istarts_with(key, prefix)) - { - suffixedOutput = key.substr(prefix.length()); - return true; - } - - suffixedOutput.clear(); - return false; -} - -} // namespace - -// Attachment helper object -class Doom3EntityClass::Attachments -{ - // Name of the entity class being parsed (for debug/error purposes) - std::string _parentClassname; - - // Any def_attached entities. Each attachment has an entity class, a - // position and optionally a name. - struct Attachment - { - // Class of entity that is attached - std::string className; - - // Name of the entity that is attached - std::string name; - - // Name of the position (AttachPos) at which the entity should be - // attached - std::string posName; - }; - - // Attached object map initially indexed by key suffix (e.g. "1" for - // "name_attach1"), then by name. - typedef std::map AttachedObjects; - AttachedObjects _objects; - - // Positions at which def_attached entities can be attached. - struct AttachPos - { - // Name of this attachment position (referred to in the - // Attachment::posName variable) - std::string name; - - // 3D offset position from our origin or the model joint, if a joint is - // specified - Vector3 origin; - - // Rotation of the attached entity - Vector3 angles; - - // Optional model joint relative to which the origin should be - // calculated - std::string joint; - }; - - // Attach position map initially indexed by key suffix (e.g. "_zhandr" for - // "attach_pos_name_zhandr"), then by name. It appears that only attachpos - // keys are using arbitrary strings instead of numeric suffixes, but we - // might as well treat everything the same way. - typedef std::map AttachPositions; - AttachPositions _positions; - -private: - - template void reindexMapByName(Map& inputMap) - { - Map copy(inputMap); - inputMap.clear(); - - // Take each item from the copied map, and insert it into the original - // map using the name as the key. - for (typename Map::value_type pair : copy) - { - if (!pair.second.name.empty()) // ignore empty names - { - inputMap.insert( - typename Map::value_type(pair.second.name, pair.second) - ); - } - } - } - -public: - - // Initialise and set classname - Attachments(const std::string& name) - : _parentClassname(name) - { } - - // Clear all data - void clear() - { - _objects.clear(); - _positions.clear(); - } - - // Attempt to extract attachment data from the given key/value pair - void parseDefAttachKeys(const std::string& key, const std::string& value) - { - std::string suffix; - - if (tryGetSuffixedKey(key, DEF_ATTACH, suffix)) - { - _objects[suffix].className = value; - } - else if (tryGetSuffixedKey(key, NAME_ATTACH, suffix)) - { - _objects[suffix].name = value; - } - else if (tryGetSuffixedKey(key, POS_ATTACH, suffix)) - { - _objects[suffix].posName = value; - } - else if (tryGetSuffixedKey(key, ATTACH_POS_NAME, suffix)) - { - _positions[suffix].name = value; - } - else if (tryGetSuffixedKey(key, ATTACH_POS_ORIGIN, suffix)) - { - _positions[suffix].origin = string::convert(value); - } - else if (tryGetSuffixedKey(key, ATTACH_POS_ANGLES, suffix)) - { - _positions[suffix].angles = string::convert(value); - } - else if (tryGetSuffixedKey(key, ATTACH_POS_JOINT, suffix)) - { - _positions[suffix].joint = value; - } - } - - // Post-process after attachment parsing - void validateAttachments() - { - // During parsing we indexed spawnargs by string suffix so that matching - // keys could be found. From now on we are no longer interested in the - // suffixes so we will re-build the maps indexed by name instead. - reindexMapByName(_objects); - reindexMapByName(_positions); - - // Drop any attached objects that specify a non-existent position (I - // assume new positions cannot be dynamically created in game). - for (AttachedObjects::iterator i = _objects.begin(); - i != _objects.end(); - /* in-loop increment */) - { - if (_positions.find(i->second.posName) == _positions.end()) - { - rWarning() - << "[eclassmgr] Entity class '" << _parentClassname - << "' tries to attach '" << i->first << "' at non-existent " - << "position '" << i->second.posName << "'\n"; - - _objects.erase(i++); - } - else - { - ++i; - } - } - } - - // Iterate over attachments - void forEachAttachment(IEntityClass::AttachmentFunc func) - { - for (auto i = _objects.begin(); i != _objects.end(); ++i) - { - IEntityClass::Attachment a; - a.eclass = i->second.className; - } - } -}; - const std::string Doom3EntityClass::DefaultWireShader("<0.3 0.3 1>"); const std::string Doom3EntityClass::DefaultFillShader("(0.3 0.3 1)"); const Vector3 Doom3EntityClass::DefaultEntityColour(0.3, 0.3, 1); @@ -225,7 +32,6 @@ Doom3EntityClass::Doom3EntityClass(const std::string& name, const vfs::FileInfo& _inheritanceResolved(false), _modName("base"), _emptyAttribute("", "", ""), - _attachments(new Attachments(name)), _parseStamp(0) {} @@ -247,11 +53,6 @@ sigc::signal& Doom3EntityClass::changedSignal() return _changedSignal; } -void Doom3EntityClass::forEachAttachment(AttachmentFunc func) const -{ - _attachments->forEachAttachment(func); -} - bool Doom3EntityClass::isFixedSize() const { if (_fixedSize) { @@ -519,8 +320,6 @@ void Doom3EntityClass::clear() _inheritanceResolved = false; _modName = "base"; - - _attachments->clear(); } void Doom3EntityClass::parseEditorSpawnarg(const std::string& key, @@ -594,9 +393,6 @@ void Doom3EntityClass::parseFromTokens(parser::DefTokeniser& tokeniser) parseEditorSpawnarg(key, value); } - // Try parsing this key/value with the Attachments manager - _attachments->parseDefAttachKeys(key, value); - // Add the EntityClassAttribute for this key/val if (getAttribute(key).getType().empty()) { @@ -619,8 +415,6 @@ void Doom3EntityClass::parseFromTokens(parser::DefTokeniser& tokeniser) } } // while true - _attachments->validateAttachments(); - // Notify the observers _changedSignal.emit(); } diff --git a/radiantcore/eclass/Doom3EntityClass.h b/radiantcore/eclass/Doom3EntityClass.h index 898e9815ca..94b535a181 100644 --- a/radiantcore/eclass/Doom3EntityClass.h +++ b/radiantcore/eclass/Doom3EntityClass.h @@ -101,10 +101,6 @@ class Doom3EntityClass // The empty attribute EntityClassAttribute _emptyAttribute; - // Helper object to manage attached entities - class Attachments; - std::unique_ptr _attachments; - // The time this def has been parsed std::size_t _parseStamp; @@ -159,7 +155,6 @@ class Doom3EntityClass std::string getName() const override; const IEntityClass* getParent() const override; sigc::signal& changedSignal() override; - void forEachAttachment(AttachmentFunc func) const override; bool isFixedSize() const override; AABB getBounds() const override; bool isLight() const override; diff --git a/radiantcore/entity/AttachmentData.cpp b/radiantcore/entity/AttachmentData.cpp new file mode 100644 index 0000000000..e08241aa25 --- /dev/null +++ b/radiantcore/entity/AttachmentData.cpp @@ -0,0 +1,104 @@ +#include "AttachmentData.h" + +#include +#include + +namespace entity +{ + +namespace +{ + +// Constants +const std::string DEF_ATTACH = "def_attach"; +const std::string NAME_ATTACH = "name_attach"; +const std::string POS_ATTACH = "pos_attach"; + +const std::string ATTACH_POS_NAME = "attach_pos_name"; +const std::string ATTACH_POS_ORIGIN = "attach_pos_origin"; +const std::string ATTACH_POS_JOINT = "attach_pos_joint"; +const std::string ATTACH_POS_ANGLES = "attach_pos_angles"; + +// Extract and return the string suffix for a key (which might be the empty +// string if there is no suffix). Returns false if the key did not match +// the prefix. +bool tryGetSuffixedKey(const std::string& key, const std::string& prefix, std::string& suffixedOutput) +{ + if (string::istarts_with(key, prefix)) + { + suffixedOutput = key.substr(prefix.length()); + return true; + } + + suffixedOutput.clear(); + return false; +} + +} // namespace + +void AttachmentData::parseDefAttachKeys(const std::string& key, + const std::string& value) +{ + std::string suffix; + + if (tryGetSuffixedKey(key, DEF_ATTACH, suffix)) + { + _objects[suffix].className = value; + } + else if (tryGetSuffixedKey(key, NAME_ATTACH, suffix)) + { + _objects[suffix].name = value; + } + else if (tryGetSuffixedKey(key, POS_ATTACH, suffix)) + { + _objects[suffix].posName = value; + } + else if (tryGetSuffixedKey(key, ATTACH_POS_NAME, suffix)) + { + _positions[suffix].name = value; + } + else if (tryGetSuffixedKey(key, ATTACH_POS_ORIGIN, suffix)) + { + _positions[suffix].origin = string::convert(value); + } + else if (tryGetSuffixedKey(key, ATTACH_POS_ANGLES, suffix)) + { + _positions[suffix].angles = string::convert(value); + } + else if (tryGetSuffixedKey(key, ATTACH_POS_JOINT, suffix)) + { + _positions[suffix].joint = value; + } +} + +void AttachmentData::validateAttachments() +{ + // During parsing we indexed spawnargs by string suffix so that matching + // keys could be found. From now on we are no longer interested in the + // suffixes so we will re-build the maps indexed by name instead. + reindexMapByName(_objects); + reindexMapByName(_positions); + + // Drop any attached objects that specify a non-existent position (I + // assume new positions cannot be dynamically created in game). + for (AttachedObjects::iterator i = _objects.begin(); + i != _objects.end(); + /* in-loop increment */) + { + if (_positions.find(i->second.posName) == _positions.end()) + { + rWarning() + << "[eclassmgr] Entity class '" << _parentClassname + << "' tries to attach '" << i->first << "' at non-existent " + << "position '" << i->second.posName << "'\n"; + + _objects.erase(i++); + } + else + { + ++i; + } + } +} + +} \ No newline at end of file diff --git a/radiantcore/entity/AttachmentData.h b/radiantcore/entity/AttachmentData.h new file mode 100644 index 0000000000..9c81282eef --- /dev/null +++ b/radiantcore/entity/AttachmentData.h @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include "math/Vector3.h" + +#include + +namespace entity +{ + +/// Representation of parsed `def_attach` and related keys +class AttachmentData +{ + // Name of the entity class being parsed (for debug/error purposes) + std::string _parentClassname; + + // Any def_attached entities. Each attachment has an entity class, a + // position and optionally a name. + struct Attachment + { + // Class of entity that is attached + std::string className; + + // Name of the entity that is attached + std::string name; + + // Name of the position (AttachPos) at which the entity should be + // attached + std::string posName; + }; + + // Attached object map initially indexed by key suffix (e.g. "1" for + // "name_attach1"), then by name. + typedef std::map AttachedObjects; + AttachedObjects _objects; + + // Positions at which def_attached entities can be attached. + struct AttachPos + { + // Name of this attachment position (referred to in the + // Attachment::posName variable) + std::string name; + + // 3D offset position from our origin or the model joint, if a joint is + // specified + Vector3 origin; + + // Rotation of the attached entity + Vector3 angles; + + // Optional model joint relative to which the origin should be + // calculated + std::string joint; + }; + + // Attach position map initially indexed by key suffix (e.g. "_zhandr" for + // "attach_pos_name_zhandr"), then by name. It appears that only attachpos + // keys are using arbitrary strings instead of numeric suffixes, but we + // might as well treat everything the same way. + typedef std::map AttachPositions; + AttachPositions _positions; + +private: + + template void reindexMapByName(Map& inputMap) + { + Map copy(inputMap); + inputMap.clear(); + + // Take each item from the copied map, and insert it into the original + // map using the name as the key. + for (typename Map::value_type pair : copy) + { + if (!pair.second.name.empty()) // ignore empty names + { + inputMap.insert( + typename Map::value_type(pair.second.name, pair.second) + ); + } + } + } + +public: + + /// Initialise and set classname + AttachmentData(const std::string& name) + : _parentClassname(name) + { } + + /// Clear all data + void clear() + { + _objects.clear(); + _positions.clear(); + } + + /// Attempt to extract attachment data from the given key/value pair + void parseDefAttachKeys(const std::string& key, const std::string& value); + + /// Sanitise and resolve attachments and their named positions + void validateAttachments(); + + /// Invoke a functor for each attachment + template + void forEachAttachment(Functor func) const + { + for (auto i = _objects.begin(); i != _objects.end(); ++i) + { + Entity::Attachment a; + a.eclass = i->second.className; + func(a); + } + } +}; + +} \ No newline at end of file diff --git a/radiantcore/entity/SpawnArgs.cpp b/radiantcore/entity/SpawnArgs.cpp index 8092a32830..7d41ba591c 100644 --- a/radiantcore/entity/SpawnArgs.cpp +++ b/radiantcore/entity/SpawnArgs.cpp @@ -13,7 +13,8 @@ SpawnArgs::SpawnArgs(const IEntityClassPtr& eclass) : _undo(_keyValues, std::bind(&SpawnArgs::importState, this, std::placeholders::_1), "EntityKeyValues"), _instanced(false), _observerMutex(false), - _isContainer(!eclass->isFixedSize()) + _isContainer(!eclass->isFixedSize()), + _attachments(eclass->getName()) {} SpawnArgs::SpawnArgs(const SpawnArgs& other) : @@ -22,7 +23,8 @@ SpawnArgs::SpawnArgs(const SpawnArgs& other) : _undo(_keyValues, std::bind(&SpawnArgs::importState, this, std::placeholders::_1), "EntityKeyValues"), _instanced(false), _observerMutex(false), - _isContainer(other._isContainer) + _isContainer(other._isContainer), + _attachments(other._attachments) { for (KeyValues::const_iterator i = other._keyValues.begin(); i != other._keyValues.end(); @@ -193,6 +195,11 @@ bool SpawnArgs::isInherited(const std::string& key) const return (!definedLocally && !_eclass->getAttribute(key).getValue().empty()); } +void SpawnArgs::forEachAttachment(AttachmentFunc func) const +{ + _attachments.forEachAttachment(func); +} + Entity::KeyValuePairs SpawnArgs::getKeyValuePairs(const std::string& prefix) const { KeyValuePairs list; diff --git a/radiantcore/entity/SpawnArgs.h b/radiantcore/entity/SpawnArgs.h index a8b9107114..8512bea797 100644 --- a/radiantcore/entity/SpawnArgs.h +++ b/radiantcore/entity/SpawnArgs.h @@ -1,5 +1,7 @@ #pragma once +#include "AttachmentData.h" + #include #include "KeyValue.h" #include @@ -39,6 +41,9 @@ class SpawnArgs: public Entity bool _isContainer; + // Store attachment information + AttachmentData _attachments; + public: // Constructor, pass the according entity class SpawnArgs(const IEntityClassPtr& eclass); @@ -51,27 +56,15 @@ class SpawnArgs: public Entity /* Entity implementation */ void attachObserver(Observer* observer) override; void detachObserver(Observer* observer) override; - void connectUndoSystem(IMapFileChangeTracker& changeTracker); void disconnectUndoSystem(IMapFileChangeTracker& changeTracker); - - /** Return the EntityClass associated with this entity. - */ IEntityClassPtr getEntityClass() const override; - void forEachKeyValue(const KeyValueVisitFunctor& func) const override; void forEachEntityKeyValue(const EntityKeyValueVisitFunctor& visitor) override; - - /** Set a keyvalue on the entity. - */ void setKeyValue(const std::string& key, const std::string& value) override; - - /** Retrieve a keyvalue from the entity. - */ std::string getKeyValue(const std::string& key) const override; - - // Returns true if the given key is inherited bool isInherited(const std::string& key) const override; + void forEachAttachment(AttachmentFunc func) const override; // Get all KeyValues matching the given prefix. KeyValuePairs getKeyValuePairs(const std::string& prefix) const override; From e09b3c8dd6c930abff15514e4089e05bc3d1a39f Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sun, 24 Jan 2021 20:02:01 +0000 Subject: [PATCH 10/48] De-duplicate replace_all and replace_all_copy Avoid a duplicated function body by having replace_all_copy simply call replace_all on a local copy of the string and then return it. --- libs/string/replace.h | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/libs/string/replace.h b/libs/string/replace.h index bd26bfc78f..a9d66d8ff1 100644 --- a/libs/string/replace.h +++ b/libs/string/replace.h @@ -5,7 +5,7 @@ namespace string { -/** +/** * Replaces all occurrences of the given search string in the subject * with the given replacement, in-place. */ @@ -23,24 +23,15 @@ inline void replace_all(std::string& subject, const std::string& search, const s } /** - * Replaces all occurrences of of the given search string with - * the given replacement and returns a new string instance + * Replaces all occurrences of of the given search string with + * the given replacement and returns a new string instance * containing the result. The incoming subject is passed by value such * that the original string is not altered. */ inline std::string replace_all_copy(std::string subject, const std::string& search, const std::string& replacement) { - if (search.empty()) return subject; // nothing to do - - std::size_t pos = 0; - - while ((pos = subject.find(search, pos)) != std::string::npos) - { - subject.replace(pos, search.length(), replacement); - pos += replacement.length(); - } - - return subject; + replace_all(subject, search, replacement); + return subject; } /** @@ -52,7 +43,7 @@ inline void replace_first(std::string& subject, const std::string& search, const if (search.empty()) return; // nothing to do std::size_t pos = subject.find(search); - + if (pos != std::string::npos) { subject.replace(pos, search.length(), replacement); From 2625f2954cfe02bf31a3b8de025be1caea649a82 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sun, 24 Jan 2021 21:31:43 +0000 Subject: [PATCH 11/48] Add initial test suite for entity/entityclass So far this is a really trivial test; all it checks is that we can lookup the "light" entity class and confirm that it is indeed a light. --- test/CMakeLists.txt | 5 +++-- test/Entity.cpp | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 test/Entity.cpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index bff23e6166..4157cf119c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -2,6 +2,7 @@ add_executable(drtest Camera.cpp ColourSchemes.cpp CSG.cpp + Entity.cpp Face.cpp FacePlane.cpp Favourites.cpp @@ -27,9 +28,9 @@ add_executable(drtest Selection.cpp VFS.cpp WorldspawnColour.cpp) - + find_package(Threads REQUIRED) - + target_compile_options(drtest PUBLIC ${SIGC_CFLAGS}) get_filename_component(TESTRESOURCEDIR "./resources" ABSOLUTE) diff --git a/test/Entity.cpp b/test/Entity.cpp new file mode 100644 index 0000000000..c4ee914a22 --- /dev/null +++ b/test/Entity.cpp @@ -0,0 +1,33 @@ +#include "RadiantTest.h" + +#include "ieclass.h" + +namespace test +{ + +using EntityTest = RadiantTest; + +TEST_F(EntityTest, LookupEntityClass) +{ + // Nonexistent class should return null (but not throw or crash) + auto cls = GlobalEntityClassManager().findClass("notAnEntityClass"); + EXPECT_FALSE(cls); + + // Real entity class should return a valid pointer + auto lightCls = GlobalEntityClassManager().findClass("light"); + EXPECT_TRUE(lightCls); +} + +TEST_F(EntityTest, LightClassRecognisedAsLight) +{ + // The 'light' class should be recognised as an actual light + auto lightCls = GlobalEntityClassManager().findClass("light"); + EXPECT_TRUE(lightCls->isLight()); + + // Things which are not lights should also be correctly identified + auto notLightCls = GlobalEntityClassManager().findClass("dr:entity_using_modeldef"); + EXPECT_TRUE(notLightCls); + EXPECT_FALSE(notLightCls->isLight()); +} + +} \ No newline at end of file From 61dd80d59782fb1c4bb60e731aa9d7b2b54fde6f Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Mon, 25 Jan 2021 20:22:07 +0000 Subject: [PATCH 12/48] Further testing of light inheritance in EntityTest Check that entities which derive from light (directly or through multiple levels) all have isLight() set correctly, but torch_brazier is not considered a light (it is a func_static with a light attached). --- test/Entity.cpp | 15 +++- test/resources/tdm/def/lights.def | 102 +++++++++++++++++++++++ test/resources/tdm/def/lights_static.def | 56 +++++++++++++ 3 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 test/resources/tdm/def/lights_static.def diff --git a/test/Entity.cpp b/test/Entity.cpp index c4ee914a22..6baf44c889 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -18,7 +18,7 @@ TEST_F(EntityTest, LookupEntityClass) EXPECT_TRUE(lightCls); } -TEST_F(EntityTest, LightClassRecognisedAsLight) +TEST_F(EntityTest, LightEntitiesRecognisedAsLights) { // The 'light' class should be recognised as an actual light auto lightCls = GlobalEntityClassManager().findClass("light"); @@ -28,6 +28,19 @@ TEST_F(EntityTest, LightClassRecognisedAsLight) auto notLightCls = GlobalEntityClassManager().findClass("dr:entity_using_modeldef"); EXPECT_TRUE(notLightCls); EXPECT_FALSE(notLightCls->isLight()); + + // Anything deriving from the light class should also be a light + auto derived1 = GlobalEntityClassManager().findClass("atdm:light_base"); + EXPECT_TRUE(derived1->isLight()); + + // Second level derivations too + auto derived2 = GlobalEntityClassManager().findClass("light_extinguishable"); + EXPECT_TRUE(derived2->isLight()); + + // torch_brazier is not a light itself, but has a light attached, so it + // should not have isLight() == true + auto brazier = GlobalEntityClassManager().findClass("atdm:torch_brazier"); + EXPECT_FALSE(brazier->isLight()); } } \ No newline at end of file diff --git a/test/resources/tdm/def/lights.def b/test/resources/tdm/def/lights.def index 44df7c2cac..6728d409f4 100644 --- a/test/resources/tdm/def/lights.def +++ b/test/resources/tdm/def/lights.def @@ -9,3 +9,105 @@ entityDef light "editor_displayFolder" "Lights/Sources" "spawnclass" "idLight" } + +entityDef atdm:light_base +{ + "inherit" "light" + "editor_light" "1" + "editor_usage" "Base class for all TDM lights, extinguishable or not." + "editor_displayFolder" "Lights/Light Sources" + + // For AI context driven decision making + "AIUse" "AIUSE_LIGHTSOURCE" + "shouldBeOn" "0" + + "noshadows" "0" + "nospecular" "0" + "nodiffuse" "0" +} + +entitydef light_extinguishable +{ + "inherit" "atdm:light_base" + "editor_displayFolder" "Lights/Base Entities, DoNotUse" + "clipmodel_contents" "131072" // CONTENTS_RESPONSE + + "mins" "-6 -6 -6" + "maxs" "6 6 6" + + "scriptobject" "light_ext" + + "editor_usage" "Extinguishable light flame that is not randomly moving. Note this is to be attached to the top of a separate torch entity, because its bounds are used to determine the bounds of the torch flame, not the whole torch." + "editor_model model_lit" "Set to the model name the light should display when lit (usually a flame particle)." + "editor_model model_extinguished" "Set to the model name the light should display when just extinguished (usually a steam particle)." + "editor_bool extinguished" "Set to 1 or 0 based on whether the light starts out extinguished or lit. Default is lit (0)." + "editor_snd snd_lit" "Sound the light plays while lit (e.g., roaring torch sound)." + "editor_snd snd_extinguished" "Sound the light plays when just extinguished." + "editor_var ext_hide_delay" "Seconds to wait before hiding the model after light is extinguished. This should be long enough to insure that the extinguish particle and sound play to completion. Default is 10 seconds." + + "extinguished" "0" + "ext_hide_delay" "6" + "lightType" "AIUSE_LIGHTTYPE_TORCH" // grayman #2603 + + // stim/response: + + "sr_class_1" "R" + "sr_type_1" "STIM_WATER" + "sr_state_1" "1" + "sr_script_STIM_WATER" "response_extinguish" + + "sr_class_2" "R" + "sr_type_2" "STIM_GAS" + "sr_state_2" "1" + "sr_script_STIM_GAS" "response_extinguish" + + "sr_class_3" "R" + "sr_type_3" "STIM_FIRE" + "sr_state_3" "1" + "sr_script_STIM_FIRE" "response_ignite" + + "sr_class_4" "S" + "sr_type_4" "STIM_FIRE" + "sr_state_4" "1" + "sr_radius_4" "10" + "sr_magnitude_4" "1" + "sr_falloffexponent_4" "0" + // fire stim only every half second + "sr_time_interval_4" "518" + + // This stim can be turned on to have AI notice the light is off + "sr_class_5" "S" + "sr_type_5" "STIM_VISUAL" + "sr_state_5" "1" + "sr_radius_5" "500" + "sr_time_interval_5" "977" // every second or so + + // SteveL #4201 Tweaking blast-extinguish effects + "canBeBlownOut" "0" + "editor_bool canBeBlownOut" "Whether the light gets blown out by blasts, e.g. a fire arrow explosion." + "editor_setKeyValue canBeBlownOut" "0" +} + +entitydef light_torchflame_small +{ + "inherit" "light_extinguishable" + + "mins" "-6 -6 -6" + "maxs" "6 6 24" + + "editor_displayFolder" "Lights/Light Sources/Torch Flames" + "editor_usage" "A small, dimmer torch flame. Light pulses but is static. For moving light, add 'inherit' 'light_extinguishable_moving' to entity. " + + "model_lit" "torchflame_new01_small.prt" + "model_extinguished" "tdm_smoke_torchout.prt" + + "snd_lit" "fire_torch_small" + "snd_extinguished" "machine_steam01" + + "falloff" "0" + "texture" "lights/biground_torchflicker" + + "_color" "0.9 0.6 0.40" + "light_radius" "240 240 240" //larger than other similar flames to be consistent with older maps +} + diff --git a/test/resources/tdm/def/lights_static.def b/test/resources/tdm/def/lights_static.def new file mode 100644 index 0000000000..760e9024f9 --- /dev/null +++ b/test/resources/tdm/def/lights_static.def @@ -0,0 +1,56 @@ +entityDef atdm:static_light_lit_base +{ + "inherit" "func_static" + "editor_usage" "Base class for static lit light fixtures. Don't use." + "editor_displayFolder" "Lights/Base Entities, DoNotUse" + // use the tdm_light_holder script object to allow LightsOn(), LightsOff() and LightsToggle(): + "scriptobject" "tdm_light_holder" + "extinguished" "0" + "AIUse" "AIUSE_LIGHTSOURCE" + "lightType" "AIUSE_LIGHTTYPE_TORCH" +} + +entityDef atdm:torch_wall_base +{ + "inherit" "atdm:static_light_lit_base" + "model" "models/darkmod/lights/extinguishable/sq_torch.lwo" + "editor_usage" "Base class for lit wall-mounted static torches. Do not use." + "editor_displayFolder" "Lights/Base Entities, DoNotUse" + + "extinguished" "0" + "noshadows_lit" "1" // turn off shadow when lit + "noshadows" "1" // lit, so has no shadows + + // attach the light, so the torch can be re-lit by fire stims + "def_attach" "light_torchflame_small" + "pos_attach" "flame" // At the attach point called "flame"... + "attach_pos_name_1" "flame" // ... which is defined here. + "name_attach" "flame" // Give it a name to pass along spawnargs + "attach_pos_origin_1" "11 0 15" // Offset the flame + + "skin" "torch_lit" + "skin_lit" "torch_lit" + "skin_unlit" "torch_unlit" +} + +entityDef atdm:torch_brazier +{ + "inherit" "atdm:torch_wall_base" + "editor_usage" "floor mounted, lit brazier" + "editor_displayFolder" "Lights/Model Lights, Static/Fires" + + "model" "models/darkmod/lights/extinguishable/brazier.lwo" + + // attach the light, so the torch can be re-lit by fire stims + "noshadows_lit" "1" // turn off shadow when lit + "noshadows" "0" // unlit, so has shadows + + "attach_pos_origin_1" "0 0 10" // Offset the flame + + "def_attach" "light_cageflame_small" + +// "skin" "torch_lit" +// "skin_lit" "torch_lit" +// "skin_unlit" "torch_unlit" +} + From 9727ecf97ddc2944282cad9d734bc609f1df3f80 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Mon, 25 Jan 2021 20:48:48 +0000 Subject: [PATCH 13/48] Test creating a basic light entity Create an entity of the "light" class and confirm it has sensible initial values for the "name" and "classname" spawnargs. Also confirm that this entity does not have any attachments (but the forEachAttachment method does not crash or otherwise misbehave). --- test/Entity.cpp | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/Entity.cpp b/test/Entity.cpp index 6baf44c889..0813f00992 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -1,6 +1,7 @@ #include "RadiantTest.h" #include "ieclass.h" +#include "ientity.h" namespace test { @@ -43,4 +44,36 @@ TEST_F(EntityTest, LightEntitiesRecognisedAsLights) EXPECT_FALSE(brazier->isLight()); } +TEST_F(EntityTest, CannotCreateEntityWithoutClass) +{ + // Creating with a null entity class should throw an exception + EXPECT_THROW(GlobalEntityModule().createEntity({}), std::runtime_error); +} + +TEST_F(EntityTest, CreateBasicLightEntity) +{ + // Create a basic light + auto lightCls = GlobalEntityClassManager().findClass("light"); + auto light = GlobalEntityModule().createEntity(lightCls); + + // Light has a sensible autogenerated name + EXPECT_EQ(light->name(), "light_1"); + + // Entity should have a "classname" key matching the actual entity class we + // created + auto clsName = light->getEntity().getKeyValue("classname"); + EXPECT_EQ(clsName, "light"); + + // Entity should have an IEntityClass pointer which matches the one we + // looked up + EXPECT_EQ(light->getEntity().getEntityClass().get(), lightCls.get()); + + // This basic light entity should have no attachments + std::list attachments; + light->getEntity().forEachAttachment( + [&](const Entity::Attachment& a) { attachments.push_back(a); } + ); + EXPECT_EQ(attachments.size(), 0); +} + } \ No newline at end of file From 9e64adc46ba8695744144d0a177b357d42b67eb4 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Mon, 25 Jan 2021 21:08:36 +0000 Subject: [PATCH 14/48] Add failing test for creating an attached light entity Test successfully confirms that the relevant spawnargs are present, but fails because these are not yet parsed into results which can be obtained by forEachAttachment(). This test will therefore serve as the first milestone for getting this parsing working. --- test/Entity.cpp | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/test/Entity.cpp b/test/Entity.cpp index 0813f00992..521011e26c 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -8,6 +8,24 @@ namespace test using EntityTest = RadiantTest; +namespace +{ + +// Obtain entity attachments as a simple std::list +std::list getAttachments(const IEntityNodePtr node) +{ + std::list attachments; + if (node) + { + node->getEntity().forEachAttachment( + [&](const Entity::Attachment& a) { attachments.push_back(a); } + ); + } + return attachments; +} + +} + TEST_F(EntityTest, LookupEntityClass) { // Nonexistent class should return null (but not throw or crash) @@ -69,11 +87,26 @@ TEST_F(EntityTest, CreateBasicLightEntity) EXPECT_EQ(light->getEntity().getEntityClass().get(), lightCls.get()); // This basic light entity should have no attachments - std::list attachments; - light->getEntity().forEachAttachment( - [&](const Entity::Attachment& a) { attachments.push_back(a); } - ); + auto attachments = getAttachments(light); EXPECT_EQ(attachments.size(), 0); } +TEST_F(EntityTest, CreateAttachedLightEntity) +{ + // Create the torch entity which has an attached light + auto torchCls = GlobalEntityClassManager().findClass("atdm:torch_brazier"); + auto torch = GlobalEntityModule().createEntity(torchCls); + EXPECT_TRUE(torch); + + // Check that the attachment spawnargs are present + const Entity& spawnArgs = torch->getEntity(); + EXPECT_EQ(spawnArgs.getKeyValue("def_attach"), "light_cageflame_small"); + EXPECT_EQ(spawnArgs.getKeyValue("pos_attach"), "flame"); + EXPECT_EQ(spawnArgs.getKeyValue("name_attach"), "flame"); + + // Spawnargs should be parsed into a single attachment + auto attachments = getAttachments(torch); + EXPECT_EQ(attachments.size(), 1); +} + } \ No newline at end of file From 9db5809017e414217786178b8cd511d3b0a00950 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Tue, 26 Jan 2021 20:49:45 +0000 Subject: [PATCH 15/48] Add test for entity spawnarg enumeration Test the forEachKeyValue() and forEachEntityKeyValue() methods with default spawnargs and some additional spawnargs added with setKeyValue(). --- test/Entity.cpp | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/test/Entity.cpp b/test/Entity.cpp index 521011e26c..94eecac62f 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -11,6 +11,13 @@ using EntityTest = RadiantTest; namespace { +// Create an entity from a simple classname string +IEntityNodePtr createByClassName(const std::string& className) +{ + auto cls = GlobalEntityClassManager().findClass("light"); + return GlobalEntityModule().createEntity(cls); +} + // Obtain entity attachments as a simple std::list std::list getAttachments(const IEntityNodePtr node) { @@ -91,6 +98,47 @@ TEST_F(EntityTest, CreateBasicLightEntity) EXPECT_EQ(attachments.size(), 0); } +TEST_F(EntityTest, EnumerateEntitySpawnargs) +{ + auto light = createByClassName("light"); + auto& spawnArgs = light->getEntity(); + + // Visit spawnargs by key and value string + using StringMap = std::map; + StringMap keyValuesInit; + spawnArgs.forEachKeyValue([&](const std::string& k, const std::string& v) { + keyValuesInit.insert({k, v}); + }); + + // Initial entity should have a name and a classname value and no other + // properties + EXPECT_EQ(keyValuesInit.size(), 2); + EXPECT_EQ(keyValuesInit["name"], light->name()); + EXPECT_EQ(keyValuesInit["classname"], "light"); + + // Add some new properties of our own + spawnArgs.setKeyValue("origin", "128 256 -1024"); + spawnArgs.setKeyValue("_color", "0.5 0.5 0.5"); + + // Ensure that our new properties are also enumerated + StringMap keyValuesAll; + spawnArgs.forEachKeyValue([&](const std::string& k, const std::string& v) { + keyValuesAll.insert({k, v}); + }); + EXPECT_EQ(keyValuesAll.size(), 4); + EXPECT_EQ(keyValuesAll["origin"], "128 256 -1024"); + EXPECT_EQ(keyValuesAll["_color"], "0.5 0.5 0.5"); + + // Enumerate as full EntityKeyValue objects as well as strings + StringMap keyValuesByObj; + spawnArgs.forEachEntityKeyValue( + [&](const std::string& k, const EntityKeyValue& v) { + keyValuesByObj.insert({k, v.get()}); + } + ); + EXPECT_EQ(keyValuesAll, keyValuesByObj); +} + TEST_F(EntityTest, CreateAttachedLightEntity) { // Create the torch entity which has an attached light From 82fa3af58045174b868cd60430a7f5f24ce5ac64 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Wed, 27 Jan 2021 20:19:01 +0000 Subject: [PATCH 16/48] Add an 'includeInherited' flag to Entity::forEachKeyValue() If this new flag is set, the enumeration will include all spawnargs inherited from the entitydef rather than just the spawnargs set on this particular entity. --- include/ientity.h | 33 ++++++++++++++++--------- radiantcore/eclass/Doom3EntityClass.cpp | 8 +++--- radiantcore/entity/SpawnArgs.cpp | 18 ++++++++------ radiantcore/entity/SpawnArgs.h | 5 ++-- test/Entity.cpp | 27 ++++++++++++++++++-- 5 files changed, 64 insertions(+), 27 deletions(-) diff --git a/include/ientity.h b/include/ientity.h index 04e1a8b635..e284dd6baa 100644 --- a/include/ientity.h +++ b/include/ientity.h @@ -104,9 +104,6 @@ class Entity { } }; - // Function typedef to visit keyvalues - typedef std::function KeyValueVisitFunctor; - // Function typedef to visit actual EntityKeyValue objects, not just the string values typedef std::function EntityKeyValueVisitFunctor; @@ -117,11 +114,25 @@ class Entity */ virtual IEntityClassPtr getEntityClass() const = 0; + /// Functor to receive keys and values as strings + using KeyValueVisitFunc = std::function< + void(const std::string&, const std::string&) + >; + /** - * Enumerate key values on this entity using a function object taking - * key and value as string arguments. + * \brief Enumerate all keys and values on this entity, optionally including + * inherited spawnargs. + * + * \param func + * Functor to receive each key and its associated value. + * + * \param includeInherited + * true if the functor should be invoked for each inherited spawnarg (from + * the entity class), false if only explicit spawnargs on this particular + * entity should be visited. */ - virtual void forEachKeyValue(const KeyValueVisitFunctor& visitor) const = 0; + virtual void forEachKeyValue(KeyValueVisitFunc func, + bool includeInherited = false) const = 0; // Similar to above, visiting the EntityKeyValue objects itself, not just the string value. virtual void forEachEntityKeyValue(const EntityKeyValueVisitFunctor& visitor) = 0; @@ -205,7 +216,7 @@ class Entity virtual void detachObserver(Observer* observer) = 0; /** - * Returns true if this entity is of type or inherits from the + * Returns true if this entity is of type or inherits from the * given entity class name. className is treated case-sensitively. */ virtual bool isOfType(const std::string& className) = 0; @@ -303,13 +314,13 @@ class ITargetableObject typedef std::shared_ptr ITargetableObjectPtr; /** -* greebo: The TargetManager keeps track of all ITargetableObjects -* in the current scene/map. A TargetManager instance is owned +* greebo: The TargetManager keeps track of all ITargetableObjects +* in the current scene/map. A TargetManager instance is owned * by the RootNode. TargetManager instances can be acquired through * the EntityCreator interface. * * Clients acquire a named ITargetableObjectPtr by calling getTarget(). This -* always succeeds - if the named ITargetableObject is not found, +* always succeeds - if the named ITargetableObject is not found, * a new, empty one is created. * * ITargetableObject object (can be empty) @@ -391,7 +402,7 @@ class IEntitySettings virtual bool getFreeObjectRotation() const = 0; virtual void setFreeObjectRotation(bool value) = 0; - + virtual bool getShowEntityAngles() const = 0; virtual void setShowEntityAngles(bool value) = 0; diff --git a/radiantcore/eclass/Doom3EntityClass.cpp b/radiantcore/eclass/Doom3EntityClass.cpp index d016e21951..5ceeedc842 100644 --- a/radiantcore/eclass/Doom3EntityClass.cpp +++ b/radiantcore/eclass/Doom3EntityClass.cpp @@ -189,14 +189,12 @@ void Doom3EntityClass::forEachClassAttribute( std::function visitor, bool editorKeys) const { - for (EntityAttributeMap::const_iterator i = _attributes.begin(); - i != _attributes.end(); - ++i) + for (const auto& pair: _attributes) { // Visit if it is a non-editor key or we are visiting all keys - if (editorKeys || !string::istarts_with(*i->first, "editor_")) + if (editorKeys || !string::istarts_with(*pair.first, "editor_")) { - visitor(i->second); + visitor(pair.second); } } } diff --git a/radiantcore/entity/SpawnArgs.cpp b/radiantcore/entity/SpawnArgs.cpp index 7d41ba591c..ffe2de72ce 100644 --- a/radiantcore/entity/SpawnArgs.cpp +++ b/radiantcore/entity/SpawnArgs.cpp @@ -128,19 +128,27 @@ void SpawnArgs::disconnectUndoSystem(IMapFileChangeTracker& changeTracker) _instanced = false; } -/** Return the EntityClass associated with this entity. - */ IEntityClassPtr SpawnArgs::getEntityClass() const { return _eclass; } -void SpawnArgs::forEachKeyValue(const KeyValueVisitFunctor& func) const +void SpawnArgs::forEachKeyValue(KeyValueVisitFunc func, + bool includeInherited) const { + // Visit explicit spawnargs for (const KeyValuePair& pair : _keyValues) { func(pair.first, pair.second->get()); } + + // If requested, visit inherited spawnargs from the entitydef + if (includeInherited) + { + _eclass->forEachClassAttribute([&](const EntityClassAttribute& att) { + func(att.getName(), att.getValue()); + }); + } } void SpawnArgs::forEachEntityKeyValue(const EntityKeyValueVisitFunctor& func) @@ -151,8 +159,6 @@ void SpawnArgs::forEachEntityKeyValue(const EntityKeyValueVisitFunctor& func) } } -/** Set a keyvalue on the entity. - */ void SpawnArgs::setKeyValue(const std::string& key, const std::string& value) { if (value.empty()) @@ -167,8 +173,6 @@ void SpawnArgs::setKeyValue(const std::string& key, const std::string& value) } } -/** Retrieve a keyvalue from the entity. - */ std::string SpawnArgs::getKeyValue(const std::string& key) const { // Lookup the key in the map diff --git a/radiantcore/entity/SpawnArgs.h b/radiantcore/entity/SpawnArgs.h index 8512bea797..ccd4d9cdc9 100644 --- a/radiantcore/entity/SpawnArgs.h +++ b/radiantcore/entity/SpawnArgs.h @@ -8,7 +8,7 @@ namespace entity { -/** +/** * \brief Implementation of the class Entity. * * A SpawnArgs basically just keeps track of all the spawnargs and delivers @@ -59,7 +59,8 @@ class SpawnArgs: public Entity void connectUndoSystem(IMapFileChangeTracker& changeTracker); void disconnectUndoSystem(IMapFileChangeTracker& changeTracker); IEntityClassPtr getEntityClass() const override; - void forEachKeyValue(const KeyValueVisitFunctor& func) const override; + void forEachKeyValue(KeyValueVisitFunc func, + bool includeInherited) const override; void forEachEntityKeyValue(const EntityKeyValueVisitFunctor& visitor) override; void setKeyValue(const std::string& key, const std::string& value) override; std::string getKeyValue(const std::string& key) const override; diff --git a/test/Entity.cpp b/test/Entity.cpp index 94eecac62f..a88a5e9afb 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -14,7 +14,7 @@ namespace // Create an entity from a simple classname string IEntityNodePtr createByClassName(const std::string& className) { - auto cls = GlobalEntityClassManager().findClass("light"); + auto cls = GlobalEntityClassManager().findClass(className); return GlobalEntityModule().createEntity(cls); } @@ -33,6 +33,8 @@ std::list getAttachments(const IEntityNodePtr node) } +using StringMap = std::map; + TEST_F(EntityTest, LookupEntityClass) { // Nonexistent class should return null (but not throw or crash) @@ -104,7 +106,6 @@ TEST_F(EntityTest, EnumerateEntitySpawnargs) auto& spawnArgs = light->getEntity(); // Visit spawnargs by key and value string - using StringMap = std::map; StringMap keyValuesInit; spawnArgs.forEachKeyValue([&](const std::string& k, const std::string& v) { keyValuesInit.insert({k, v}); @@ -139,6 +140,28 @@ TEST_F(EntityTest, EnumerateEntitySpawnargs) EXPECT_EQ(keyValuesAll, keyValuesByObj); } +TEST_F(EntityTest, EnumerateInheritedSpawnargs) +{ + auto light = createByClassName("atdm:light_base"); + auto& spawnArgs = light->getEntity(); + + // Enumerate all keyvalues including the inherited ones + StringMap keyValues; + spawnArgs.forEachKeyValue( + [&](const std::string& k, const std::string& v) { + keyValues.insert({k, v}); + }, + true /* includeInherited */ + ); + + // Check we have some inherited properties from the entitydef (including + // spawnclass from the entitydef's own parent def) + EXPECT_EQ(keyValues["spawnclass"], "idLight"); + EXPECT_EQ(keyValues["shouldBeOn"], "0"); + EXPECT_EQ(keyValues["AIUse"], "AIUSE_LIGHTSOURCE"); + EXPECT_EQ(keyValues["noshadows"], "0"); +} + TEST_F(EntityTest, CreateAttachedLightEntity) { // Create the torch entity which has an attached light From 29dc4e5e6fa5785bc3d9de6569085d40d4a36816 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Thu, 28 Jan 2021 20:22:53 +0000 Subject: [PATCH 17/48] Entity::getKeyValuePairs() is no longer a virtual method This utility method is now implemented on Entity itself as a wrapper around the virtual forEachKeyValue() method. --- include/ientity.h | 28 ++++++++++++++++------- radiantcore/entity/SpawnArgs.cpp | 18 --------------- radiantcore/entity/SpawnArgs.h | 3 --- test/Entity.cpp | 38 +++++++++++++++++++++++++++++--- 4 files changed, 55 insertions(+), 32 deletions(-) diff --git a/include/ientity.h b/include/ientity.h index e284dd6baa..5a8783cac9 100644 --- a/include/ientity.h +++ b/include/ientity.h @@ -7,6 +7,8 @@ #include "inameobserver.h" #include +#include "string/predicate.h" + class IEntityClass; typedef std::shared_ptr IEntityClassPtr; typedef std::shared_ptr IEntityClassConstPtr; @@ -169,12 +171,12 @@ class Entity virtual bool isInherited(const std::string& key) const = 0; /** - * Return the list of Key/Value pairs matching the given prefix, case ignored. + * \brief Return the list of keyvalues matching the given prefix. * - * This method performs a search for all spawnargs whose key - * matches the given prefix, with a suffix consisting of zero or more - * arbitrary characters. For example, if "target" were specified as the - * prefix, the list would include "target", "target0", "target127" etc. + * This method performs a search for all spawnargs whose key matches the + * given prefix, with a suffix consisting of zero or more arbitrary + * characters. For example, if "target" were specified as the prefix, the + * list would include "target", "target0", "target127" etc. * * This operation may not have high performance, due to the need to scan * for matching names, therefore should not be used in performance-critical @@ -184,10 +186,20 @@ class Entity * The prefix to search for, interpreted case-insensitively. * * @return - * A list of KeyValue pairs matching the provided prefix. This - * list will be empty if there were no matches. + * A list of KeyValue pairs matching the provided prefix. This list will be + * empty if there were no matches. */ - virtual KeyValuePairs getKeyValuePairs(const std::string& prefix) const = 0; + KeyValuePairs getKeyValuePairs(const std::string& prefix) const + { + KeyValuePairs list; + + forEachKeyValue([&](const std::string& k, const std::string& v) { + if (string::istarts_with(k, prefix)) + list.push_back(std::make_pair(k, v)); + }); + + return list; + } /** greebo: Returns true if the entity is a model. For Doom3, this is * usually true when the classname == "func_static" and diff --git a/radiantcore/entity/SpawnArgs.cpp b/radiantcore/entity/SpawnArgs.cpp index ffe2de72ce..384d44f8ef 100644 --- a/radiantcore/entity/SpawnArgs.cpp +++ b/radiantcore/entity/SpawnArgs.cpp @@ -204,24 +204,6 @@ void SpawnArgs::forEachAttachment(AttachmentFunc func) const _attachments.forEachAttachment(func); } -Entity::KeyValuePairs SpawnArgs::getKeyValuePairs(const std::string& prefix) const -{ - KeyValuePairs list; - - for (KeyValues::const_iterator i = _keyValues.begin(); i != _keyValues.end(); ++i) - { - // If the prefix matches, add to list - if (string::istarts_with(i->first, prefix)) - { - list.push_back( - std::pair(i->first, i->second->get()) - ); - } - } - - return list; -} - EntityKeyValuePtr SpawnArgs::getEntityKeyValue(const std::string& key) { KeyValues::const_iterator found = find(key); diff --git a/radiantcore/entity/SpawnArgs.h b/radiantcore/entity/SpawnArgs.h index ccd4d9cdc9..678f42f1dd 100644 --- a/radiantcore/entity/SpawnArgs.h +++ b/radiantcore/entity/SpawnArgs.h @@ -67,9 +67,6 @@ class SpawnArgs: public Entity bool isInherited(const std::string& key) const override; void forEachAttachment(AttachmentFunc func) const override; - // Get all KeyValues matching the given prefix. - KeyValuePairs getKeyValuePairs(const std::string& prefix) const override; - bool isWorldspawn() const override; bool isContainer() const override; void setIsContainer(bool isContainer); diff --git a/test/Entity.cpp b/test/Entity.cpp index a88a5e9afb..09cf50bf7d 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -162,12 +162,44 @@ TEST_F(EntityTest, EnumerateInheritedSpawnargs) EXPECT_EQ(keyValues["noshadows"], "0"); } +TEST_F(EntityTest, GetKeyValuePairs) +{ + auto torch = createByClassName("atdm:torch_brazier"); + auto& spawnArgs = torch->getEntity(); + + using Pair = Entity::KeyValuePairs::value_type; + + // Retrieve single spawnargs as single-element lists of pairs + auto classNamePairs = spawnArgs.getKeyValuePairs("classname"); + EXPECT_EQ(classNamePairs.size(), 1); + EXPECT_EQ(classNamePairs[0], Pair("classname", "atdm:torch_brazier")); + + auto namePairs = spawnArgs.getKeyValuePairs("name"); + EXPECT_EQ(namePairs.size(), 1); + EXPECT_EQ(namePairs[0], Pair("name", "atdm_torch_brazier_1")); + + // Add some spawnargs with a common prefix + const StringMap SR_KEYS{ + {"sr_type_1", "blah"}, + {"sr_type_2", "bleh"}, + {"sR_tYpE_a", "123"}, + {"SR_type_1a", "0 123 -120"}, + }; + for (const auto& pair: SR_KEYS) + spawnArgs.setKeyValue(pair.first, pair.second); + + // Confirm all added prefix keys are found regardless of case + auto srPairs = spawnArgs.getKeyValuePairs("sr_type"); + EXPECT_EQ(srPairs.size(), SR_KEYS.size()); + for (const auto& pair: srPairs) + EXPECT_EQ(SR_KEYS.at(pair.first), pair.second); +} + TEST_F(EntityTest, CreateAttachedLightEntity) { // Create the torch entity which has an attached light - auto torchCls = GlobalEntityClassManager().findClass("atdm:torch_brazier"); - auto torch = GlobalEntityModule().createEntity(torchCls); - EXPECT_TRUE(torch); + auto torch = createByClassName("atdm:torch_brazier"); + ASSERT_TRUE(torch); // Check that the attachment spawnargs are present const Entity& spawnArgs = torch->getEntity(); From ff596aca9ff0c09cc503d19619dca7dc0539861f Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Thu, 28 Jan 2021 21:26:36 +0000 Subject: [PATCH 18/48] Add test for cloning an entity and its spawnargs scene::Cloneable interface moved up one level, from EntityNode to IEntityNode, so it can be accessed from outside the radiantcore module. --- include/ientity.h | 4 +++- radiantcore/entity/EntityNode.cpp | 1 - radiantcore/entity/EntityNode.h | 7 +++---- test/Entity.cpp | 28 ++++++++++++++++++++++++++++ 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/include/ientity.h b/include/ientity.h index 5a8783cac9..137e9e593c 100644 --- a/include/ientity.h +++ b/include/ientity.h @@ -5,6 +5,7 @@ #include "imodule.h" #include "irender.h" #include "inameobserver.h" +#include "iscenegraph.h" #include #include "string/predicate.h" @@ -265,7 +266,8 @@ class Entity /// Interface for a INode subclass that contains an Entity class IEntityNode : public IRenderEntity, - public virtual scene::INode + public virtual scene::INode, + public scene::Cloneable { public: virtual ~IEntityNode() {} diff --git a/radiantcore/entity/EntityNode.cpp b/radiantcore/entity/EntityNode.cpp index 27a22f8ead..ee3a871cd6 100644 --- a/radiantcore/entity/EntityNode.cpp +++ b/radiantcore/entity/EntityNode.cpp @@ -32,7 +32,6 @@ EntityNode::EntityNode(const EntityNode& other) : TargetableNode(_spawnArgs, *this), Transformable(other), MatrixTransform(other), - scene::Cloneable(other), _eclass(other._eclass), _spawnArgs(other._spawnArgs), _namespaceManager(_spawnArgs), diff --git a/radiantcore/entity/EntityNode.h b/radiantcore/entity/EntityNode.h index a7559f5970..3701ec9665 100644 --- a/radiantcore/entity/EntityNode.h +++ b/radiantcore/entity/EntityNode.h @@ -19,10 +19,10 @@ namespace entity { - + class EntityNode; typedef std::shared_ptr EntityNodePtr; - + /** * greebo: This is the common base class of all map entities. */ @@ -33,8 +33,7 @@ class EntityNode : public Namespaced, public TargetableNode, public Transformable, - public MatrixTransform, // influences local2world of child nodes - public scene::Cloneable // all entities are cloneable, to be implemented in subclasses + public MatrixTransform // influences local2world of child nodes { protected: // The entity class diff --git a/test/Entity.cpp b/test/Entity.cpp index 09cf50bf7d..8cbc5f412d 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -195,6 +195,34 @@ TEST_F(EntityTest, GetKeyValuePairs) EXPECT_EQ(SR_KEYS.at(pair.first), pair.second); } +TEST_F(EntityTest, CopySpawnargs) +{ + auto light = createByClassName("atdm:light_base"); + auto& spawnArgs = light->getEntity(); + + // Add some custom spawnargs to copy + const StringMap EXTRA_SPAWNARGS{{"first", "1"}, + {"second", "two"}, + {"THIRD", "3333"}, + {"_color", "1 0 1"}}; + + for (const auto& pair: EXTRA_SPAWNARGS) + spawnArgs.setKeyValue(pair.first, pair.second); + + // Clone the entity node + auto lightCopy = light->clone(); + const Entity* clonedEnt = Node_getEntity(lightCopy); + ASSERT_TRUE(clonedEnt); + + // Clone should have all the same spawnargs + std::size_t count = 0; + clonedEnt->forEachKeyValue([&](const std::string& k, const std::string& v) { + EXPECT_EQ(spawnArgs.getKeyValue(k), v); + ++count; + }); + EXPECT_EQ(count, EXTRA_SPAWNARGS.size() + 2 /* name and classname */); +} + TEST_F(EntityTest, CreateAttachedLightEntity) { // Create the torch entity which has an attached light From fd57c69321f4c959ac7a6cacc403b476359eb680 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sat, 30 Jan 2021 20:20:12 +0000 Subject: [PATCH 19/48] CopySpawnargs test checks EntityKeyValue pointer identities Copied SpawnArgs object should have same key value strings but different EntityKeyValue object pointers. --- test/Entity.cpp | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/test/Entity.cpp b/test/Entity.cpp index 8cbc5f412d..d936269d4e 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -211,16 +211,37 @@ TEST_F(EntityTest, CopySpawnargs) // Clone the entity node auto lightCopy = light->clone(); - const Entity* clonedEnt = Node_getEntity(lightCopy); + Entity* clonedEnt = Node_getEntity(lightCopy); ASSERT_TRUE(clonedEnt); - // Clone should have all the same spawnargs + // Clone should have all the same spawnarg strings std::size_t count = 0; clonedEnt->forEachKeyValue([&](const std::string& k, const std::string& v) { EXPECT_EQ(spawnArgs.getKeyValue(k), v); ++count; }); EXPECT_EQ(count, EXTRA_SPAWNARGS.size() + 2 /* name and classname */); + + // Clone should NOT have the same actual KeyValue object pointers, although + // the count should be the same + std::set origPointers; + std::set copiedPointers; + spawnArgs.forEachEntityKeyValue( + [&](const std::string& k, EntityKeyValue& v) { + origPointers.insert(&v); + }); + clonedEnt->forEachEntityKeyValue( + [&](const std::string& k, EntityKeyValue& v) { + copiedPointers.insert(&v); + }); + EXPECT_EQ(origPointers.size(), count); + EXPECT_EQ(copiedPointers.size(), count); + + std::vector overlap; + std::set_intersection(origPointers.begin(), origPointers.end(), + copiedPointers.begin(), copiedPointers.end(), + std::back_inserter(overlap)); + EXPECT_EQ(overlap.size(), 0); } TEST_F(EntityTest, CreateAttachedLightEntity) From 2534e09386dfa575612f20e1a9c9efb16dc20473 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sat, 30 Jan 2021 20:46:41 +0000 Subject: [PATCH 20/48] Initial attachment parsing in SpawnArgs class SpawnArgs now calls a new parseAttachments() method in its constructor, which iterates over all keys (using the new 'includeInherited' parameter to forEachKeyValue()) and parses them for possible attachment information. Current version of CreateAttachedLightEntity test now passes. --- radiantcore/entity/AttachmentData.cpp | 7 ++++--- radiantcore/entity/AttachmentData.h | 4 ++-- radiantcore/entity/SpawnArgs.cpp | 25 ++++++++++++++++++------- radiantcore/entity/SpawnArgs.h | 4 ++++ 4 files changed, 28 insertions(+), 12 deletions(-) diff --git a/radiantcore/entity/AttachmentData.cpp b/radiantcore/entity/AttachmentData.cpp index e08241aa25..e5604631f5 100644 --- a/radiantcore/entity/AttachmentData.cpp +++ b/radiantcore/entity/AttachmentData.cpp @@ -22,14 +22,15 @@ const std::string ATTACH_POS_ANGLES = "attach_pos_angles"; // Extract and return the string suffix for a key (which might be the empty // string if there is no suffix). Returns false if the key did not match // the prefix. -bool tryGetSuffixedKey(const std::string& key, const std::string& prefix, std::string& suffixedOutput) +bool tryGetSuffixedKey(const std::string& key, const std::string& prefix, + std::string& suffixedOutput) { if (string::istarts_with(key, prefix)) { suffixedOutput = key.substr(prefix.length()); return true; } - + suffixedOutput.clear(); return false; } @@ -88,7 +89,7 @@ void AttachmentData::validateAttachments() if (_positions.find(i->second.posName) == _positions.end()) { rWarning() - << "[eclassmgr] Entity class '" << _parentClassname + << "[AttachmentData] Entity '" << _entityName << "' tries to attach '" << i->first << "' at non-existent " << "position '" << i->second.posName << "'\n"; diff --git a/radiantcore/entity/AttachmentData.h b/radiantcore/entity/AttachmentData.h index 9c81282eef..1b8c012a52 100644 --- a/radiantcore/entity/AttachmentData.h +++ b/radiantcore/entity/AttachmentData.h @@ -13,7 +13,7 @@ namespace entity class AttachmentData { // Name of the entity class being parsed (for debug/error purposes) - std::string _parentClassname; + std::string _entityName; // Any def_attached entities. Each attachment has an entity class, a // position and optionally a name. @@ -85,7 +85,7 @@ class AttachmentData /// Initialise and set classname AttachmentData(const std::string& name) - : _parentClassname(name) + : _entityName(name) { } /// Clear all data diff --git a/radiantcore/entity/SpawnArgs.cpp b/radiantcore/entity/SpawnArgs.cpp index 384d44f8ef..3fb48b32ce 100644 --- a/radiantcore/entity/SpawnArgs.cpp +++ b/radiantcore/entity/SpawnArgs.cpp @@ -15,7 +15,10 @@ SpawnArgs::SpawnArgs(const IEntityClassPtr& eclass) : _observerMutex(false), _isContainer(!eclass->isFixedSize()), _attachments(eclass->getName()) -{} +{ + // Parse attachment keys + parseAttachments(); +} SpawnArgs::SpawnArgs(const SpawnArgs& other) : Entity(other), @@ -26,12 +29,20 @@ SpawnArgs::SpawnArgs(const SpawnArgs& other) : _isContainer(other._isContainer), _attachments(other._attachments) { - for (KeyValues::const_iterator i = other._keyValues.begin(); - i != other._keyValues.end(); - ++i) - { - insert(i->first, i->second->get()); - } + // Copy keyvalue strings, not actual KeyValue pointers + for (const KeyValuePair& p : other._keyValues) + { + insert(p.first, p.second->get()); + } +} + +void SpawnArgs::parseAttachments() +{ + forEachKeyValue( + [this](const std::string& k, const std::string& v) { + _attachments.parseDefAttachKeys(k, v); + }, + true /* includeInherited */); } bool SpawnArgs::isModel() const diff --git a/radiantcore/entity/SpawnArgs.h b/radiantcore/entity/SpawnArgs.h index 678f42f1dd..05fe0c10de 100644 --- a/radiantcore/entity/SpawnArgs.h +++ b/radiantcore/entity/SpawnArgs.h @@ -82,6 +82,10 @@ class SpawnArgs: public Entity private: + // Parse attachment information from def_attach and related keys (which are + // most likely on the entity class, not the entity itself) + void parseAttachments(); + // Notification functions void notifyInsert(const std::string& key, KeyValue& value); void notifyChange(const std::string& k, const std::string& v); From 5c98a884564b7bae51eccaac6a72ee18044d9f94 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sat, 30 Jan 2021 20:58:59 +0000 Subject: [PATCH 21/48] Add a new failing test for attachment offset atdm:torch_brazier should have its flame at (0, 0, 10) based on the offset value in the entityDef. --- test/Entity.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/Entity.cpp b/test/Entity.cpp index d936269d4e..03b94af60e 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -259,6 +259,11 @@ TEST_F(EntityTest, CreateAttachedLightEntity) // Spawnargs should be parsed into a single attachment auto attachments = getAttachments(torch); EXPECT_EQ(attachments.size(), 1); + + // Examine the properties of the single attachment + Entity::Attachment attachment = attachments.front(); + EXPECT_EQ(attachment.eclass, "light_cageflame_small"); + EXPECT_EQ(attachment.offset, Vector3(0, 0, 10)); } } \ No newline at end of file From 6ed581fb686a45c88f4bf27755e20ddb9ec5f2ac Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sat, 30 Jan 2021 21:29:03 +0000 Subject: [PATCH 22/48] AttachmentData now returns attachment offsets SpawnArgs now calls validateAttachments() after the initial parse step, and the forEachAttachment() method on AttachmentData now sets a valid offset vector on the Entity::Attachment object, derived from looking up the named position in the parsed key data. --- radiantcore/entity/AttachmentData.cpp | 8 ++++---- radiantcore/entity/AttachmentData.h | 1 + radiantcore/entity/SpawnArgs.cpp | 4 ++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/radiantcore/entity/AttachmentData.cpp b/radiantcore/entity/AttachmentData.cpp index e5604631f5..42bf7d63e5 100644 --- a/radiantcore/entity/AttachmentData.cpp +++ b/radiantcore/entity/AttachmentData.cpp @@ -88,10 +88,10 @@ void AttachmentData::validateAttachments() { if (_positions.find(i->second.posName) == _positions.end()) { - rWarning() - << "[AttachmentData] Entity '" << _entityName - << "' tries to attach '" << i->first << "' at non-existent " - << "position '" << i->second.posName << "'\n"; + rWarning() << "[AttachmentData] Entity '" << _entityName + << "' tries to attach '" << i->first + << "' at non-existent position '" << i->second.posName + << "'\n"; _objects.erase(i++); } diff --git a/radiantcore/entity/AttachmentData.h b/radiantcore/entity/AttachmentData.h index 1b8c012a52..c737402a98 100644 --- a/radiantcore/entity/AttachmentData.h +++ b/radiantcore/entity/AttachmentData.h @@ -109,6 +109,7 @@ class AttachmentData { Entity::Attachment a; a.eclass = i->second.className; + a.offset = _positions.at(i->second.posName).origin; func(a); } } diff --git a/radiantcore/entity/SpawnArgs.cpp b/radiantcore/entity/SpawnArgs.cpp index 3fb48b32ce..8d918d3798 100644 --- a/radiantcore/entity/SpawnArgs.cpp +++ b/radiantcore/entity/SpawnArgs.cpp @@ -38,11 +38,15 @@ SpawnArgs::SpawnArgs(const SpawnArgs& other) : void SpawnArgs::parseAttachments() { + // Parse the keys forEachKeyValue( [this](const std::string& k, const std::string& v) { _attachments.parseDefAttachKeys(k, v); }, true /* includeInherited */); + + // Post validation step + _attachments.validateAttachments(); } bool SpawnArgs::isModel() const From a4a3ace8023e85ced9466b87c2d8898ace879837 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sun, 31 Jan 2021 14:25:51 +0000 Subject: [PATCH 23/48] Add a simple test for rendering a light entity Render the light entity in wireframe mode and confirm we receive the expected number of renderables (origin diamond, radius and light_center depending on selection status). --- include/irenderable.h | 14 ++++------ test/Entity.cpp | 62 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/include/irenderable.h b/include/irenderable.h index 032f9e06c1..a130a0dde2 100644 --- a/include/irenderable.h +++ b/include/irenderable.h @@ -16,8 +16,7 @@ class RendererLight; class LitObject; /** - * \brief - * Class which accepts OpenGLRenderable objects during the first pass of + * \brief Class which accepts OpenGLRenderable objects during the first pass of * rendering. * * Each Renderable in the scenegraph is passed a reference to a @@ -33,8 +32,7 @@ class RenderableCollector virtual ~RenderableCollector() {} /** - * \brief - * Submit a renderable object + * \brief Submit a renderable object. * * This method allows renderable geometry to be submitted under the control * of a LitObject which will determine whether and how the renderable is @@ -74,8 +72,7 @@ class RenderableCollector const IRenderEntity* entity = nullptr) = 0; /** - * \brief - * Submit a light source for the render operation. + * \brief Submit a light source for the render operation. * * This is the entry point for lights into the render front-end. Each light * in the scene graph must be submitted through this method in order to @@ -85,9 +82,8 @@ class RenderableCollector virtual void addLight(const RendererLight& light) = 0; /** - * \brief - * Determine if this RenderableCollector can accept renderables for full - * materials rendering, or just wireframe rendering. + * \brief Determine if this RenderableCollector can accept renderables for + * full materials rendering, or just wireframe rendering. * * \return * true if full materials are supported, false if only wireframe rendering diff --git a/test/Entity.cpp b/test/Entity.cpp index 03b94af60e..87c94ce8a0 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -2,6 +2,10 @@ #include "ieclass.h" #include "ientity.h" +#include "irendersystemfactory.h" +#include "iselectable.h" + +#include "render/NopVolumeTest.h" namespace test { @@ -244,6 +248,64 @@ TEST_F(EntityTest, CopySpawnargs) EXPECT_EQ(overlap.size(), 0); } +namespace +{ + // A simple RenderableCollector which just logs the number of renderables + // and lights submitted + struct TestRenderableCollector: public RenderableCollector + { + int renderables = 0; + int lights = 0; + + void addRenderable(Shader& shader, const OpenGLRenderable& renderable, + const Matrix4& localToWorld, + const LitObject* litObject = nullptr, + const IRenderEntity* entity = nullptr) override + { + ++renderables; + } + + void addLight(const RendererLight& light) + { + ++lights; + } + + bool supportsFullMaterials() const override { return true; } + void setHighlightFlag(Highlight::Flags flags, bool enabled) override + {} + }; +} + +TEST_F(EntityTest, RenderLightEntity) +{ + auto light = createByClassName("light"); + auto& spawnArgs = light->getEntity(); + + // Rendering requires a backend and a volume test + auto backend = GlobalRenderSystemFactory().createRenderSystem(); + render::NopVolumeTest volumeTest; + + // Render the light in wireframe mode. This should render just the origin + // diamond. + { + TestRenderableCollector wireframeRenderer; + light->setRenderSystem(backend); + light->renderWireframe(wireframeRenderer, volumeTest); + EXPECT_EQ(wireframeRenderer.renderables, 1); + } + + // With the light selected, we should get the origin diamond, the radius and + // the center vertex. + { + TestRenderableCollector wireframeRenderer; + Node_getSelectable(light)->setSelected(true); + light->setRenderSystem(backend); + light->renderWireframe(wireframeRenderer, volumeTest); + EXPECT_EQ(wireframeRenderer.renderables, 3); + Node_getSelectable(light)->setSelected(false); + } +} + TEST_F(EntityTest, CreateAttachedLightEntity) { // Create the torch entity which has an attached light From c9cbda76adefa60887a6d42c63995bbf0fb3fd9c Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sun, 31 Jan 2021 14:47:38 +0000 Subject: [PATCH 24/48] Add two new tests for entity selection Test that selecting an entity propagates the selected node to the selection system (which passes), and test that we can destroy a selected entity (which currently fails due to trying to call shared_from_this() in a destructor). --- test/Entity.cpp | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/test/Entity.cpp b/test/Entity.cpp index 87c94ce8a0..f1c5a9c24a 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -4,6 +4,7 @@ #include "ientity.h" #include "irendersystemfactory.h" #include "iselectable.h" +#include "iselection.h" #include "render/NopVolumeTest.h" @@ -276,10 +277,35 @@ namespace }; } +TEST_F(EntityTest, SelectEntity) +{ + auto light = createByClassName("light"); + + // Confirm that setting entity node's selection status propagates to the + // selection system + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 0); + Node_getSelectable(light)->setSelected(true); + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 1); + Node_getSelectable(light)->setSelected(false); + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 0); +} + +TEST_F(EntityTest, DestroySelectedEntity) +{ + auto light = createByClassName("light"); + + // Confirm that setting entity node's selection status propagates to the + // selection system + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 0); + Node_getSelectable(light)->setSelected(true); + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 1); + + // Destructor called here and should not crash +} + TEST_F(EntityTest, RenderLightEntity) { auto light = createByClassName("light"); - auto& spawnArgs = light->getEntity(); // Rendering requires a backend and a volume test auto backend = GlobalRenderSystemFactory().createRenderSystem(); @@ -292,6 +318,7 @@ TEST_F(EntityTest, RenderLightEntity) light->setRenderSystem(backend); light->renderWireframe(wireframeRenderer, volumeTest); EXPECT_EQ(wireframeRenderer.renderables, 1); + EXPECT_EQ(wireframeRenderer.lights, 0); } // With the light selected, we should get the origin diamond, the radius and @@ -302,6 +329,12 @@ TEST_F(EntityTest, RenderLightEntity) light->setRenderSystem(backend); light->renderWireframe(wireframeRenderer, volumeTest); EXPECT_EQ(wireframeRenderer.renderables, 3); + EXPECT_EQ(wireframeRenderer.lights, 0); + + // Destroying a selected entity causes a crash (destructor tries to + // unselect the entity in the RadiantSelectionSystem which requires a + // call to Node::self() which crashes due to trying to create a new + // shared_ptr in the destructor). Node_getSelectable(light)->setSelected(false); } } From bf77456d91c4d247c5f13f2114c506b7d5e3c626 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sun, 31 Jan 2021 17:28:55 +0000 Subject: [PATCH 25/48] Fix possible crash in shutting down selection system When the selection system shuts down it clears out any remaining selected nodes, which causes the SelectableNode destructor to unselect the node, which then tries to call back into the selection system using Node::getSelf() which finally results in a shared_from_this() being called from within the node's own destructor, which shared_ptr really doesn't like. This is resolved by having the selection system instruct any remaining selected nodes to unselect themselves before clearing out the list, thus ensuring that the same sequence happens before the node's destructor is invoked. --- .../selection/RadiantSelectionSystem.cpp | 59 ++++++++++++------- test/Entity.cpp | 6 -- 2 files changed, 38 insertions(+), 27 deletions(-) diff --git a/radiantcore/selection/RadiantSelectionSystem.cpp b/radiantcore/selection/RadiantSelectionSystem.cpp index 62444be694..b5152f77cd 100644 --- a/radiantcore/selection/RadiantSelectionSystem.cpp +++ b/radiantcore/selection/RadiantSelectionSystem.cpp @@ -177,7 +177,7 @@ bool RadiantSelectionSystem::nothingSelected() const (Mode() == eGroupPart && _countPrimitive == 0); } -void RadiantSelectionSystem::pivotChanged() +void RadiantSelectionSystem::pivotChanged() { _pivot.setNeedsRecalculation(true); SceneChangeNotify(); @@ -345,7 +345,7 @@ void RadiantSelectionSystem::onSelectedChanged(const scene::INodePtr& node, cons int delta = isSelected ? +1 : -1; _countPrimitive += delta; - + _selectionInfo.totalCount += delta; if (Node_isPatch(node)) @@ -356,7 +356,7 @@ void RadiantSelectionSystem::onSelectedChanged(const scene::INodePtr& node, cons { _selectionInfo.brushCount += delta; } - else + else { _selectionInfo.entityCount += delta; } @@ -381,7 +381,7 @@ void RadiantSelectionSystem::onSelectedChanged(const scene::INodePtr& node, cons ASSERT_MESSAGE(_selection.size() == _countPrimitive, "selection-tracking error"); _requestWorkZoneRecalculation = true; - + // When everything is deselected, release the pivot user lock if (_selection.empty()) { @@ -396,7 +396,7 @@ void RadiantSelectionSystem::onComponentSelection(const scene::INodePtr& node, c int delta = selectable.isSelected() ? +1 : -1; _countComponent += delta; - + _selectionInfo.totalCount += delta; _selectionInfo.componentCount += delta; @@ -648,11 +648,11 @@ void RadiantSelectionSystem::performPointSelection(const SelectablesList& candid if (candidates.empty()) return; // Yes, now determine how we should interpret the click - switch (modifier) + switch (modifier) { // If we are in toggle mode (Shift-Left-Click by default), just toggle the // selection of the "topmost" item - case eToggle: + case eToggle: { ISelectable* best = candidates.front(); // toggle selection of the object with least depth (=first in the list) @@ -691,7 +691,7 @@ void RadiantSelectionSystem::performPointSelection(const SelectablesList& candid // check if there is a "next" item in the list, if not: select the first item ++i; - if (i != candidates.end()) + if (i != candidates.end()) { algorithm::setSelectionStatus(*i, true); //(*i)->setSelected(true); @@ -709,7 +709,7 @@ void RadiantSelectionSystem::performPointSelection(const SelectablesList& candid } break; - default: + default: break; }; } @@ -717,7 +717,7 @@ void RadiantSelectionSystem::performPointSelection(const SelectablesList& candid void RadiantSelectionSystem::selectArea(SelectionTest& test, SelectionSystem::EModifier modifier, bool face) { // If we are in replace mode, deselect all the components or previous selections - if (modifier == SelectionSystem::eReplace) + if (modifier == SelectionSystem::eReplace) { if (face) { @@ -746,13 +746,13 @@ void RadiantSelectionSystem::selectArea(SelectionTest& test, SelectionSystem::EM candidates.push_back(i->second); } } - else + else { testSelectScene(candidates, test, test.getVolume(), Mode(), ComponentMode()); } // Since toggling a selectable might trigger a group-selection - // we need to keep track of the desired state of each selectable + // we need to keep track of the desired state of each selectable typedef std::map SelectablesMap; SelectablesMap selectableStates; @@ -856,7 +856,7 @@ void RadiantSelectionSystem::onManipulationCancelled() pivotChanged(); } -void RadiantSelectionSystem::renderWireframe(RenderableCollector& collector, const VolumeTest& volume) const +void RadiantSelectionSystem::renderWireframe(RenderableCollector& collector, const VolumeTest& volume) const { renderSolid(collector, volume); } @@ -977,7 +977,7 @@ const StringSet& RadiantSelectionSystem::getDependencies() const return _dependencies; } -void RadiantSelectionSystem::initialiseModule(const IApplicationContext& ctx) +void RadiantSelectionSystem::initialiseModule(const IApplicationContext& ctx) { rMessage() << getName() << "::initialiseModule called." << std::endl; @@ -1011,10 +1011,10 @@ void RadiantSelectionSystem::initialiseModule(const IApplicationContext& ctx) GlobalCommandSystem().addCommand("ToggleManipulatorMode", std::bind(&RadiantSelectionSystem::toggleManipulatorModeCmd, this, std::placeholders::_1), { cmd::ARGTYPE_STRING }); - + GlobalCommandSystem().addCommand("ToggleEntitySelectionMode", std::bind(&RadiantSelectionSystem::toggleEntityMode, this, std::placeholders::_1)); GlobalCommandSystem().addCommand("ToggleGroupPartSelectionMode", std::bind(&RadiantSelectionSystem::toggleGroupPartMode, this, std::placeholders::_1)); - + GlobalCommandSystem().addCommand("ToggleComponentSelectionMode", std::bind(&RadiantSelectionSystem::toggleComponentModeCmd, this, std::placeholders::_1), { cmd::ARGTYPE_STRING }); @@ -1028,7 +1028,7 @@ void RadiantSelectionSystem::initialiseModule(const IApplicationContext& ctx) IPreferencePage& page = GlobalPreferenceSystem().getPage(_("Settings/Selection")); - page.appendCheckBox(_("Ignore light volume bounds when calculating default rotation pivot location"), + page.appendCheckBox(_("Ignore light volume bounds when calculating default rotation pivot location"), ManipulationPivot::RKEY_DEFAULT_PIVOT_LOCATION_IGNORES_LIGHT_VOLUMES); // Connect the bounds changed caller @@ -1042,14 +1042,31 @@ void RadiantSelectionSystem::initialiseModule(const IApplicationContext& ctx) sigc::mem_fun(*this, &RadiantSelectionSystem::onMapEvent)); } -void RadiantSelectionSystem::shutdownModule() +void RadiantSelectionSystem::shutdownModule() { // greebo: Unselect everything so that no references to scene::Nodes // are kept after shutdown, causing destruction issues. setSelectedAll(false); setSelectedAllComponents(false); - // In pathological cases this list might contain remnants, clear it + // In pathological cases this list might contain remnants. First, give each + // selectable node a chance to remove itself from the container by setting + // its own selected state to false (rather than waiting for this to happen + // in its destructor). + for (auto i = _selection.begin(); i != _selection.end(); ) + { + // Take a reference to the node and increment the iterator while the + // iterator is still valid. + scene::INodePtr node = (i++)->first; + + // If this is a selectable node, unselect it (which might remove it from + // the map and invalidate the original iterator) + auto selectable = Node_getSelectable(node); + if (selectable) + selectable->setSelected(false); + } + + // Clear the list of anything which remains. _selection.clear(); _activeManipulator.reset(); @@ -1250,7 +1267,7 @@ void RadiantSelectionSystem::toggleEntityMode(const cmd::ArgumentList& args) { activateDefaultMode(); } - else + else { SetMode(eEntity); SetComponentMode(eDefault); @@ -1286,7 +1303,7 @@ void RadiantSelectionSystem::toggleGroupPartMode(const cmd::ArgumentList& args) // Now deselect everything and select all child primitives instead setSelectedAll(false); - + std::for_each(groupEntityNodes.begin(), groupEntityNodes.end(), [&](const scene::INodePtr& node) { node->foreachNode([&] (const scene::INodePtr& child)->bool diff --git a/test/Entity.cpp b/test/Entity.cpp index f1c5a9c24a..8669b6b7a5 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -330,12 +330,6 @@ TEST_F(EntityTest, RenderLightEntity) light->renderWireframe(wireframeRenderer, volumeTest); EXPECT_EQ(wireframeRenderer.renderables, 3); EXPECT_EQ(wireframeRenderer.lights, 0); - - // Destroying a selected entity causes a crash (destructor tries to - // unselect the entity in the RadiantSelectionSystem which requires a - // call to Node::self() which crashes due to trying to create a new - // shared_ptr in the destructor). - Node_getSelectable(light)->setSelected(false); } } From b14de84b8ed7456466b7491400bcdae113c65e29 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sun, 31 Jan 2021 20:08:52 +0000 Subject: [PATCH 26/48] Split RenderLightEntity into separate tests for selected/unselected Split tests for better organisation and to avoid one test operating on an object which has already been modified by an earlier test. --- test/Entity.cpp | 92 ++++++++++++++++++++++++++----------------------- 1 file changed, 49 insertions(+), 43 deletions(-) diff --git a/test/Entity.cpp b/test/Entity.cpp index 8669b6b7a5..333a05660c 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -249,6 +249,32 @@ TEST_F(EntityTest, CopySpawnargs) EXPECT_EQ(overlap.size(), 0); } +TEST_F(EntityTest, SelectEntity) +{ + auto light = createByClassName("light"); + + // Confirm that setting entity node's selection status propagates to the + // selection system + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 0); + Node_getSelectable(light)->setSelected(true); + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 1); + Node_getSelectable(light)->setSelected(false); + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 0); +} + +TEST_F(EntityTest, DestroySelectedEntity) +{ + auto light = createByClassName("light"); + + // Confirm that setting entity node's selection status propagates to the + // selection system + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 0); + Node_getSelectable(light)->setSelected(true); + EXPECT_EQ(GlobalSelectionSystem().countSelected(), 1); + + // Destructor called here and should not crash +} + namespace { // A simple RenderableCollector which just logs the number of renderables @@ -275,62 +301,42 @@ namespace void setHighlightFlag(Highlight::Flags flags, bool enabled) override {} }; -} - -TEST_F(EntityTest, SelectEntity) -{ - auto light = createByClassName("light"); - // Confirm that setting entity node's selection status propagates to the - // selection system - EXPECT_EQ(GlobalSelectionSystem().countSelected(), 0); - Node_getSelectable(light)->setSelected(true); - EXPECT_EQ(GlobalSelectionSystem().countSelected(), 1); - Node_getSelectable(light)->setSelected(false); - EXPECT_EQ(GlobalSelectionSystem().countSelected(), 0); + // Collection of objects needed for rendering + struct RenderFixture + { + RenderSystemPtr backend = GlobalRenderSystemFactory().createRenderSystem(); + render::NopVolumeTest volumeTest; + TestRenderableCollector collector; + }; } -TEST_F(EntityTest, DestroySelectedEntity) +TEST_F(EntityTest, RenderUnselectedLightEntity) { auto light = createByClassName("light"); + RenderFixture renderF; - // Confirm that setting entity node's selection status propagates to the - // selection system - EXPECT_EQ(GlobalSelectionSystem().countSelected(), 0); - Node_getSelectable(light)->setSelected(true); - EXPECT_EQ(GlobalSelectionSystem().countSelected(), 1); - - // Destructor called here and should not crash + // Render the light in wireframe mode. This should render just the origin + // diamond. + light->setRenderSystem(renderF.backend); + light->renderWireframe(renderF.collector, renderF.volumeTest); + EXPECT_EQ(renderF.collector.renderables, 1); + EXPECT_EQ(renderF.collector.lights, 0); } -TEST_F(EntityTest, RenderLightEntity) +TEST_F(EntityTest, RenderSelectedLightEntity) { auto light = createByClassName("light"); - - // Rendering requires a backend and a volume test - auto backend = GlobalRenderSystemFactory().createRenderSystem(); - render::NopVolumeTest volumeTest; - - // Render the light in wireframe mode. This should render just the origin - // diamond. - { - TestRenderableCollector wireframeRenderer; - light->setRenderSystem(backend); - light->renderWireframe(wireframeRenderer, volumeTest); - EXPECT_EQ(wireframeRenderer.renderables, 1); - EXPECT_EQ(wireframeRenderer.lights, 0); - } + RenderFixture renderF; // With the light selected, we should get the origin diamond, the radius and // the center vertex. - { - TestRenderableCollector wireframeRenderer; - Node_getSelectable(light)->setSelected(true); - light->setRenderSystem(backend); - light->renderWireframe(wireframeRenderer, volumeTest); - EXPECT_EQ(wireframeRenderer.renderables, 3); - EXPECT_EQ(wireframeRenderer.lights, 0); - } + Node_getSelectable(light)->setSelected(true); + + light->setRenderSystem(renderF.backend); + light->renderWireframe(renderF.collector, renderF.volumeTest); + EXPECT_EQ(renderF.collector.renderables, 3); + EXPECT_EQ(renderF.collector.lights, 0); } TEST_F(EntityTest, CreateAttachedLightEntity) From 65a2ef3e8a03589f655e67cfb6eda89e8513576e Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sun, 31 Jan 2021 20:59:40 +0000 Subject: [PATCH 27/48] Add a test for light source rendering This test checks that the RenderableCollector::addLight() method has been invoked after rendering a light entity in full materials mode. --- radiantcore/entity/light/LightNode.cpp | 22 +++++++------------ test/Entity.cpp | 30 ++++++++++++++++++++------ 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/radiantcore/entity/light/LightNode.cpp b/radiantcore/entity/light/LightNode.cpp index 7e26cee7cd..d0e2082630 100644 --- a/radiantcore/entity/light/LightNode.cpp +++ b/radiantcore/entity/light/LightNode.cpp @@ -69,7 +69,7 @@ void LightNode::snapto(float snap) { _light.snapto(snap); } -AABB LightNode::getSelectAABB() const +AABB LightNode::getSelectAABB() const { // Use the light origin as select AAB centerpoint Vector3 extents; @@ -168,7 +168,7 @@ void LightNode::testSelectComponents(Selector& selector, SelectionTest& test, Se // Use the full rotation matrix for the test test.BeginMesh(localToWorld()); - if (_light.isProjected()) + if (_light.isProjected()) { // Test the projection components for selection _lightTargetInstance.testSelect(selector, test); @@ -177,7 +177,7 @@ void LightNode::testSelectComponents(Selector& selector, SelectionTest& test, Se _lightStartInstance.testSelect(selector, test); _lightEndInstance.testSelect(selector, test); } - else + else { // Test if the light center is hit by the click _lightCenterInstance.testSelect(selector, test); @@ -276,21 +276,16 @@ void LightNode::selectedChangedComponent(const ISelectable& selectable) { GlobalSelectionSystem().onComponentSelection(Node::getSelf(), selectable); } -/* greebo: This is the method that gets called by renderer.h. It passes the call - * on to the Light class render methods. - */ void LightNode::renderSolid(RenderableCollector& collector, const VolumeTest& volume) const { // Submit self to the renderer as an actual light source collector.addLight(_light); - // Render the visible representation of the light entity (origin, bounds etc) EntityNode::renderSolid(collector, volume); - // Re-use the same method as in wireframe rendering for the moment + // Render the visible representation of the light entity (origin, bounds etc) const bool lightIsSelected = isSelected(); renderLightVolume(collector, localToWorld(), lightIsSelected); - renderInactiveComponents(collector, volume, lightIsSelected); } @@ -300,7 +295,6 @@ void LightNode::renderWireframe(RenderableCollector& collector, const VolumeTest const bool lightIsSelected = isSelected(); renderLightVolume(collector, localToWorld(), lightIsSelected); - renderInactiveComponents(collector, volume, lightIsSelected); } @@ -371,7 +365,7 @@ void LightNode::renderComponents(RenderableCollector& collector, const VolumeTes if (_light.isProjected()) { // A projected light - + EntitySettings& settings = *EntitySettings::InstancePtr(); const Vector3& colourStartEndSelected = settings.getLightVertexColour(LightEditVertexType::StartEndSelected); @@ -497,8 +491,8 @@ void LightNode::evaluateTransform() { // When the user is mouse-moving a vertex in the orthoviews he/she is operating // in world space. It's expected that the selected vertex follows the mouse. - // Since the editable light vertices are measured in local coordinates - // we have to calculate the new position in world space first and then transform + // Since the editable light vertices are measured in local coordinates + // we have to calculate the new position in world space first and then transform // the point back into local space. if (_lightCenterInstance.isSelected()) @@ -507,7 +501,7 @@ void LightNode::evaluateTransform() Vector3 newWorldPos = localToWorld().transformPoint(_light.getDoom3Radius().m_center) + getTranslation(); _light.getDoom3Radius().m_centerTransformed = localToWorld().getFullInverse().transformPoint(newWorldPos); } - + if (_lightTargetInstance.isSelected()) { Vector3 newWorldPos = localToWorld().transformPoint(_light.target()) + getTranslation(); diff --git a/test/Entity.cpp b/test/Entity.cpp index 333a05660c..648abdb431 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -302,7 +302,9 @@ namespace {} }; - // Collection of objects needed for rendering + // Collection of objects needed for rendering. Since not all tests require + // rendering, these objects are in an auxiliary fixture created when needed + // rather than part of the EntityTest fixture used by every test. struct RenderFixture { RenderSystemPtr backend = GlobalRenderSystemFactory().createRenderSystem(); @@ -316,10 +318,11 @@ TEST_F(EntityTest, RenderUnselectedLightEntity) auto light = createByClassName("light"); RenderFixture renderF; - // Render the light in wireframe mode. This should render just the origin - // diamond. + // Render the light in wireframe mode. light->setRenderSystem(renderF.backend); light->renderWireframe(renderF.collector, renderF.volumeTest); + + // Only the light origin diamond should be rendered EXPECT_EQ(renderF.collector.renderables, 1); EXPECT_EQ(renderF.collector.lights, 0); } @@ -329,16 +332,31 @@ TEST_F(EntityTest, RenderSelectedLightEntity) auto light = createByClassName("light"); RenderFixture renderF; - // With the light selected, we should get the origin diamond, the radius and - // the center vertex. + // Select the light then render it in wireframe mode Node_getSelectable(light)->setSelected(true); - light->setRenderSystem(renderF.backend); light->renderWireframe(renderF.collector, renderF.volumeTest); + + // With the light selected, we should get the origin diamond, the radius and + // the center vertex. EXPECT_EQ(renderF.collector.renderables, 3); EXPECT_EQ(renderF.collector.lights, 0); } +TEST_F(EntityTest, RenderLightAsLightSource) +{ + auto light = createByClassName("light"); + RenderFixture renderF; + + // Render the light in full materials mode + light->setRenderSystem(renderF.backend); + light->renderSolid(renderF.collector, renderF.volumeTest); + + // We should get one renderable for the origin diamond, and one light source + EXPECT_EQ(renderF.collector.renderables, 1); + EXPECT_EQ(renderF.collector.lights, 1); +} + TEST_F(EntityTest, CreateAttachedLightEntity) { // Create the torch entity which has an attached light From df60c5f8beca37f7e86718141b692de4236b8d48 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sun, 31 Jan 2021 21:18:44 +0000 Subject: [PATCH 28/48] Test rendered light entity has correct world origin TestRenderableCollector now stores a list of RendererLight pointers rather than just counting the lights submitted, and this list is used to confirm that the "origin" key set on the test light entity appears correctly as the result of worldOrigin(). --- include/irender.h | 8 +++----- test/Entity.cpp | 22 +++++++++++++++++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/include/irender.h b/include/irender.h index cbd96eee1a..5203d249dc 100644 --- a/include/irender.h +++ b/include/irender.h @@ -149,8 +149,7 @@ typedef std::shared_ptr IRenderEntityPtr; typedef std::weak_ptr IRenderEntityWeakPtr; /** - * \brief - * Interface for a light source in the renderer. + * \brief Interface for a light source in the renderer. */ class RendererLight { @@ -171,8 +170,7 @@ class RendererLight virtual const ShaderPtr& getShader() const = 0; /** - * \brief - * Return the origin of the light volume in world space. + * \brief Return the origin of the light volume in world space. * * This corresponds to the "origin" key of the light object, i.e. the center * of the bounding box for an omni light and the tip of the pyramid for a @@ -573,7 +571,7 @@ class RenderSystem // Returns true if openGL supports ARB or GLSL lighting virtual bool shaderProgramsAvailable() const = 0; - // Sets the flag whether shader programs are available. + // Sets the flag whether shader programs are available. virtual void setShaderProgramsAvailable(bool available) = 0; // Subscription to get notified as soon as the openGL extensions have been initialised diff --git a/test/Entity.cpp b/test/Entity.cpp index 648abdb431..2bf1d3445b 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -7,6 +7,7 @@ #include "iselection.h" #include "render/NopVolumeTest.h" +#include "string/convert.h" namespace test { @@ -277,13 +278,16 @@ TEST_F(EntityTest, DestroySelectedEntity) namespace { - // A simple RenderableCollector which just logs the number of renderables - // and lights submitted + // A simple RenderableCollector which just logs/stores whatever is submitted struct TestRenderableCollector: public RenderableCollector { + // Count of submitted renderables and lights int renderables = 0; int lights = 0; + // List of actual RendererLight objects + std::list lightPtrs; + void addRenderable(Shader& shader, const OpenGLRenderable& renderable, const Matrix4& localToWorld, const LitObject* litObject = nullptr, @@ -295,6 +299,7 @@ namespace void addLight(const RendererLight& light) { ++lights; + lightPtrs.push_back(&light); } bool supportsFullMaterials() const override { return true; } @@ -346,15 +351,26 @@ TEST_F(EntityTest, RenderSelectedLightEntity) TEST_F(EntityTest, RenderLightAsLightSource) { auto light = createByClassName("light"); - RenderFixture renderF; + auto& spawnArgs = light->getEntity(); + + // Set a non-default origin for the light + static const Vector3 ORIGIN(-64, 128, 963); + spawnArgs.setKeyValue("origin", string::to_string(ORIGIN)); // Render the light in full materials mode + RenderFixture renderF; light->setRenderSystem(renderF.backend); light->renderSolid(renderF.collector, renderF.volumeTest); // We should get one renderable for the origin diamond, and one light source EXPECT_EQ(renderF.collector.renderables, 1); EXPECT_EQ(renderF.collector.lights, 1); + + // Confirm properties of the submitted RendererLight + ASSERT_EQ(renderF.collector.lightPtrs.size(), 1); + const RendererLight* rLight = renderF.collector.lightPtrs.front(); + ASSERT_TRUE(rLight); + EXPECT_EQ(rLight->worldOrigin(), ORIGIN); } TEST_F(EntityTest, CreateAttachedLightEntity) From c9fba2dda797714c037e2c19eace7c0a5d5c50c8 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Mon, 1 Feb 2021 21:11:38 +0000 Subject: [PATCH 29/48] Test more RendererLight properties Construct a light_torchflame_small instead of just a basic light, and confirm that the AABB extents and origin match the values from the entity def, along with the light shader. --- include/irender.h | 1 - test/Entity.cpp | 10 +++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/include/irender.h b/include/irender.h index 5203d249dc..d9519e279f 100644 --- a/include/irender.h +++ b/include/irender.h @@ -465,7 +465,6 @@ class Shader * \return * An Material subclass with information about the shader definition */ - virtual const MaterialPtr& getMaterial() const = 0; virtual unsigned int getFlags() const = 0; diff --git a/test/Entity.cpp b/test/Entity.cpp index 2bf1d3445b..c1334055c4 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -5,6 +5,7 @@ #include "irendersystemfactory.h" #include "iselectable.h" #include "iselection.h" +#include "ishaders.h" #include "render/NopVolumeTest.h" #include "string/convert.h" @@ -350,7 +351,7 @@ TEST_F(EntityTest, RenderSelectedLightEntity) TEST_F(EntityTest, RenderLightAsLightSource) { - auto light = createByClassName("light"); + auto light = createByClassName("light_torchflame_small"); auto& spawnArgs = light->getEntity(); // Set a non-default origin for the light @@ -371,6 +372,13 @@ TEST_F(EntityTest, RenderLightAsLightSource) const RendererLight* rLight = renderF.collector.lightPtrs.front(); ASSERT_TRUE(rLight); EXPECT_EQ(rLight->worldOrigin(), ORIGIN); + EXPECT_EQ(rLight->lightAABB().origin, ORIGIN); + + // Default light properties from the entitydef + EXPECT_EQ(rLight->lightAABB().extents, Vector3(240, 240, 240)); + ASSERT_TRUE(rLight->getShader() && rLight->getShader()->getMaterial()); + EXPECT_EQ(rLight->getShader()->getMaterial()->getName(), + "lights/biground_torchflicker"); } TEST_F(EntityTest, CreateAttachedLightEntity) From 859ed64557385c1e126dbdba904e1af9f702a92e Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Tue, 2 Feb 2021 20:05:35 +0000 Subject: [PATCH 30/48] Remove Light::worldOrigin() For some reason this was a public virtual exposed on the RendererLight interface despite not being used anywhere outside Light itself. Perhaps other calls to this method were removed during recent renderer refactoring. --- include/irender.h | 23 +++++---------------- radiantcore/entity/light/Light.cpp | 33 +++++++++++++----------------- radiantcore/entity/light/Light.h | 5 ++--- test/Entity.cpp | 2 +- 4 files changed, 22 insertions(+), 41 deletions(-) diff --git a/include/irender.h b/include/irender.h index d9519e279f..ff9262694b 100644 --- a/include/irender.h +++ b/include/irender.h @@ -157,8 +157,7 @@ class RendererLight virtual ~RendererLight() {} /** - * \brief - * Return the render entity associated with this light + * \brief Return the render entity associated with this light * * The IRenderEntity is used to evaluate possible shader expressions in the * shader returned by getShader(). The light object itself may be its own @@ -170,17 +169,7 @@ class RendererLight virtual const ShaderPtr& getShader() const = 0; /** - * \brief Return the origin of the light volume in world space. - * - * This corresponds to the "origin" key of the light object, i.e. the center - * of the bounding box for an omni light and the tip of the pyramid for a - * projected light. - */ - virtual const Vector3& worldOrigin() const = 0; - - /** - * \brief - * Return the world-space to light-texture-space transformation matrix. + * \brief Return the world-space to light-texture-space transformation matrix. * * The light texture space is a box, with coordinates [0..1] on each * dimension, representing the texture (UV) coordinates of the light falloff @@ -193,8 +182,7 @@ class RendererLight virtual Matrix4 getLightTextureTransformation() const = 0; /** - * \brief - * Return the AABB of the illuminated volume. + * \brief Return the AABB of the illuminated volume. * * This AABB represents the boundaries of the volume which are illuminated * by this light. Anything outside of this volume does not need to be @@ -208,8 +196,7 @@ class RendererLight virtual AABB lightAABB() const = 0; /** - * \brief - * Return the light origin in world space. + * \brief Return the light origin in world space. * * The light origin is the point from which the light rays are considered to * be projected, i.e. the direction from which bump maps will be illuminated @@ -227,7 +214,7 @@ typedef std::shared_ptr RendererLightPtr; /// Debug stream insertion for RendererLight inline std::ostream& operator<< (std::ostream& os, const RendererLight& l) { - return os << "RendererLight(origin=" << l.worldOrigin().pp() + return os << "RendererLight(origin=" << l.getLightOrigin().pp() << ", lightAABB=" << l.lightAABB() << ")"; } diff --git a/radiantcore/entity/light/Light.cpp b/radiantcore/entity/light/Light.cpp index 231de8c12d..3bf8e62f0b 100644 --- a/radiantcore/entity/light/Light.cpp +++ b/radiantcore/entity/light/Light.cpp @@ -161,7 +161,7 @@ void Light::updateOrigin() { // Update the transformation matrix _owner.localToParent() = Matrix4::getIdentity(); - _owner.localToParent().translateBy(worldOrigin()); + _owner.localToParent().translateBy(_originTransformed); _owner.localToParent().multiplyBy(m_rotation.getMatrix4()); // Notify all child nodes @@ -292,7 +292,7 @@ void Light::rotationChanged() // Update the transformation matrix _owner.localToParent() = Matrix4::getIdentity(); - _owner.localToParent().translateBy(worldOrigin()); + _owner.localToParent().translateBy(_originTransformed); _owner.localToParent().multiplyBy(m_rotation.getMatrix4()); // Notify owner about this @@ -449,7 +449,7 @@ Doom3LightRadius& Light::getDoom3Radius() { void Light::renderProjectionPoints(RenderableCollector& collector, const VolumeTest& volume, - const Matrix4& localToWorld) const + const Matrix4& localToWorld) const { // Add the renderable light target collector.setHighlightFlag(RenderableCollector::Highlight::Primitives, false); @@ -473,7 +473,7 @@ void Light::renderProjectionPoints(RenderableCollector& collector, // Adds the light centre renderable to the given collector void Light::renderLightCentre(RenderableCollector& collector, const VolumeTest& volume, - const Matrix4& localToWorld) const + const Matrix4& localToWorld) const { collector.addRenderable(*_rCentre.getShader(), _rCentre, localToWorld); } @@ -557,7 +557,7 @@ Matrix4 Light::getLightTextureTransformation() const // into texture coordinates that span the range [0..1] within the light volume. // Example: - // For non-rotated point lights the world point [origin - light_radius] will be + // For non-rotated point lights the world point [origin - light_radius] will be // transformed to [0,0,0], whereas [origin + light_radius] will be [1,1,1] if (isProjected()) @@ -589,9 +589,9 @@ Matrix4 Light::getLightTextureTransformation() const 1.0f / lightBounds.extents.y(), 1.0f / lightBounds.extents.z()) )); - // To get texture coordinates in the range of [0..1], we need to scale down + // To get texture coordinates in the range of [0..1], we need to scale down // one more time. [-1..1] is 2 units wide, so scale down by factor 2. - // By this time, points within the light volume have been mapped + // By this time, points within the light volume have been mapped // into a [-0.5..0.5] cube around the origin. worldTolight.premultiplyBy(Matrix4::getScale(Vector3(0.5f, 0.5f, 0.5f))); @@ -629,15 +629,16 @@ const Matrix4& Light::rotation() const { * the centerTransformed variable as the lighting should be updated as soon as the light center * is dragged. */ -Vector3 Light::getLightOrigin() const { +Vector3 Light::getLightOrigin() const +{ if (isProjected()) { - return worldOrigin(); + return _originTransformed; } else { // AABB origin + light_center, i.e. center in world space - return worldOrigin() + m_doom3Radius.m_centerTransformed; + return _originTransformed + m_doom3Radius.m_centerTransformed; } } @@ -769,8 +770,8 @@ void Light::updateProjection() const //{ // rMessage() << " Plane " << i << ": " << lightProject[i].normal() << ", dist: " << lightProject[i].dist() << std::endl; //} - - // greebo: Comparing this to the engine sources, all frustum planes in TDM + + // greebo: Comparing this to the engine sources, all frustum planes in TDM // appear to be negated, their normals are pointing outwards. // we want the planes of s=0, s=q, t=0, and t=q @@ -796,7 +797,7 @@ void Light::updateProjection() const // Normalise all frustum planes _frustum.normalisePlanes(); - // TDM uses an array of 6 idPlanes, these relate to DarkRadiant like this: + // TDM uses an array of 6 idPlanes, these relate to DarkRadiant like this: // 0 = left, 1 = top, 2 = right, 3 = bottom, 4 = front, 5 = back //rMessage() << " Frustum Plane " << 0 << ": " << _frustum.left.normal() << ", dist: " << _frustum.left.dist() << std::endl; //rMessage() << " Frustum Plane " << 1 << ": " << _frustum.top.normal() << ", dist: " << _frustum.top.dist() << std::endl; @@ -873,10 +874,4 @@ const IRenderEntity& Light::getLightEntity() const return _owner; } -const Vector3& Light::worldOrigin() const -{ - // return the absolute world origin - return _originTransformed; -} - } // namespace entity diff --git a/radiantcore/entity/light/Light.h b/radiantcore/entity/light/Light.h index d445e470ca..3118b09d7d 100644 --- a/radiantcore/entity/light/Light.h +++ b/radiantcore/entity/light/Light.h @@ -140,11 +140,11 @@ class Light: public RendererLight KeyObserverDelegate _lightEndObserver; KeyObserverDelegate _lightTextureObserver; +private: + void construct(); void destroy(); -private: - // Ensure the start and end points are set to sensible values void checkStartEnd(); @@ -246,7 +246,6 @@ class Light: public RendererLight // RendererLight implementation const IRenderEntity& getLightEntity() const override; - const Vector3& worldOrigin() const override; Matrix4 getLightTextureTransformation() const override; Vector3 getLightOrigin() const override; const ShaderPtr& getShader() const override; diff --git a/test/Entity.cpp b/test/Entity.cpp index c1334055c4..0a6da6dab6 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -371,7 +371,7 @@ TEST_F(EntityTest, RenderLightAsLightSource) ASSERT_EQ(renderF.collector.lightPtrs.size(), 1); const RendererLight* rLight = renderF.collector.lightPtrs.front(); ASSERT_TRUE(rLight); - EXPECT_EQ(rLight->worldOrigin(), ORIGIN); + EXPECT_EQ(rLight->getLightOrigin(), ORIGIN); EXPECT_EQ(rLight->lightAABB().origin, ORIGIN); // Default light properties from the entitydef From b4b79b00938eadabb9b87a31c5c9b72154aa1cb4 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Tue, 2 Feb 2021 20:48:54 +0000 Subject: [PATCH 31/48] Test that entity updates when its entity class is modified Confirm that a light entity captures a new wireframe shader in response to a colour change on its entity class. Currently we can't confirm that it's the *right* shader because the Shader interface doesn't expose that much (the result of getMaterial() will be null because this is a built-in single-colour shader rather than one derived from a material). --- test/Entity.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/test/Entity.cpp b/test/Entity.cpp index 0a6da6dab6..490ec94dd3 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -319,6 +319,31 @@ namespace }; } +TEST_F(EntityTest, ModifyEntityClass) +{ + auto cls = GlobalEntityClassManager().findClass("light"); + auto light = GlobalEntityModule().createEntity(cls); + auto& spawnArgs = light->getEntity(); + + // Light doesn't initially have a colour set + RenderFixture rf; + light->setRenderSystem(rf.backend); + const ShaderPtr origWireShader = light->getWireShader(); + ASSERT_TRUE(origWireShader); + + // The shader shouldn't just change by itself (this would invalidate the + // test) + EXPECT_EQ(light->getWireShader(), origWireShader); + + // Set a new colour value on the entity *class* (not the entity) + cls->setColour(Vector3(0.5, 0.24, 0.87)); + + // Shader should have changed due to the entity class update (although there + // aren't currently any public Shader properties that we can examine to + // confirm its contents) + EXPECT_NE(light->getWireShader(), origWireShader); +} + TEST_F(EntityTest, RenderUnselectedLightEntity) { auto light = createByClassName("light"); From 3f2c43cfb2f4ba59d7663f72761d864b532f719b Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Wed, 3 Feb 2021 21:10:12 +0000 Subject: [PATCH 32/48] Add initial test for rendering attached light entity Confirm that we can create an EclassModelNode from the torch_brazier model, traverse its children (which consists of 1 StaticModelNode) and receive renderables for the model surfaces. This does not test for the presence of any attached entities, since this functionality isn't implemented yet. --- test/Entity.cpp | 42 ++++- test/resources/tdm/def/base.def | 206 +++++++++++++++++++++++ test/resources/tdm/def/lights_static.def | 13 +- test/resources/tdm/models/torch.lwo | Bin 0 -> 8842 bytes 4 files changed, 248 insertions(+), 13 deletions(-) create mode 100644 test/resources/tdm/def/base.def create mode 100644 test/resources/tdm/models/torch.lwo diff --git a/test/Entity.cpp b/test/Entity.cpp index 490ec94dd3..988691899c 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -310,12 +310,25 @@ namespace // Collection of objects needed for rendering. Since not all tests require // rendering, these objects are in an auxiliary fixture created when needed - // rather than part of the EntityTest fixture used by every test. - struct RenderFixture + // rather than part of the EntityTest fixture used by every test. This class + // also implements scene::NodeVisitor enabling it to visit trees of nodes + // for rendering. + struct RenderFixture: public scene::NodeVisitor { RenderSystemPtr backend = GlobalRenderSystemFactory().createRenderSystem(); render::NopVolumeTest volumeTest; TestRenderableCollector collector; + + // Keep track of nodes visited + int nodesVisited = 0; + + // NodeVisitor implementation + bool pre(const scene::INodePtr& node) override + { + ++nodesVisited; + node->renderWireframe(collector, volumeTest); + return true; + } }; } @@ -428,4 +441,29 @@ TEST_F(EntityTest, CreateAttachedLightEntity) EXPECT_EQ(attachment.offset, Vector3(0, 0, 10)); } +TEST_F(EntityTest, RenderAttachedLightEntity) +{ + auto torch = createByClassName("atdm:torch_brazier"); + ASSERT_TRUE(torch); + + // Confirm that def has the right model + auto& spawnArgs = torch->getEntity(); + EXPECT_EQ(spawnArgs.getKeyValue("model"), "models/torch.lwo"); + + RenderFixture rf; + torch->setRenderSystem(rf.backend); + + // The entity node itself does not render the model; it is a parent node + // with the model as a child (e.g. as a StaticModelNode). Therefore we must + // traverse our "mini-scenegraph" to render the model. + torch->traverseChildren(rf); + + // The node visitor should have visited one child node (a static model) and + // collected 3 renderables in total (because the torch model has several + // surfaces). + EXPECT_EQ(rf.nodesVisited, 1); + EXPECT_EQ(rf.collector.renderables, 3); + EXPECT_EQ(rf.collector.lights, 0); +} + } \ No newline at end of file diff --git a/test/resources/tdm/def/base.def b/test/resources/tdm/def/base.def new file mode 100644 index 0000000000..18d2349328 --- /dev/null +++ b/test/resources/tdm/def/base.def @@ -0,0 +1,206 @@ +entityDef atdm:entity_base +{ + "editor_usage" "Do not use, base class for all TDM entities, mainly there to set common spawnargs and document them." + "editor_var inherit" "Entitity inherits all properties from this parent entity" + "editor_color editor_color" "The entity or model will be drawn in this color inside the editor." + "editor_string editor_displayfolder" "When selecting entities in the editor to add, they will be shown as a tree and this sets the path inside that tree where to display the entity. Can be used to logically sort them into groups and subgroups." + "editor_vector editor_maxs" "Together with 'editor_mins', this defines a box that will represent the entity inside the editor." + "editor_vector editor_mins" "Together with 'editor_maxs', this defines a box that will represent the entity inside the editor." + + "editor_int team" "Lets AI determine to which team this actor, item or path node (f.i. flee point) belongs to, to distinguish between friendly / neutral / hostile entities." + + "editor_var def_absence_marker" "You can specify a custom absence marker entitydef here. By default, atdm:absence_marker is used." + "editor_float absence_noticeability" "This is the chance for AI to notice when this entity has gone missing." + "editor_float absence_noticeability_recheck_interval" "After a failed probability check, the AI will ignore the missing item for this amount of time (in seconds) before another probability check can be performed. Set to 0 if the check should only be performed once." + "editor_var absence_location" "Uses the bounds of the entity named here (generate brush textured with clip, convert to func_static) instead of the entity's own bounds for checking whether the entity is absent. Multiple locations (eg absence_location_1 etc) are possible." + "editor_float absence_bounds_tolerance" "Expands the bounds used for checking whether the entity is absent." + "editor_float absence_alert" "If set, the alert increase of the AI when this entity is missing. Otherwise, the resulting alert level is just below agitated searching." + + "editor_string set" "Spawnargs of the format 'set XYZ on ABC' are used to pass along and set spawnargs on entities that are spawned during runtime and then attached via 'def_attach' to the base entity. XYZ stands for the name of the spawnarg, and ABC the name of the attachement or the attachement position. For instance, 'set _color on candle' '0.5 0.5 1' would try to find an entity attached at the point candle and set the spawnarg '_color' on it. This is mainly useful for the pre-made combo entities like moveable and static light sources, so the mapper can influence their appearance." + + "editor_material editor_material" "The material that is used on this entity in Dark Radiant" + "editor_bool editor_showangle" "If set to 1 this entity will show an arrow in Dark Radiant to show its current angle" + "editor_bool nodraw" "If set to 1 this entity is not drawn in game, meaning it is invisible." + "editor_vector origin" "Origin of the entity" + "editor_snd snd_respawn" "Sound when this entity is respawned" + "editor_bool editor_rotatable" "applies only to DoomEdit, not supported by DR" + + "editor_bool inv_map_start" "If set to 1, this item will be in the players inventory on map start." + "editor_string scriptobject" "Points to the used script that defines the behaviour of this object." + "editor_bool networksync" "Applies to multiplayer, if nonzero, this entity is synchronised over the network." + + "editor_var spawnclass" "This spawnarg defines which C++ class this entity is using. The value refers to the actual type name, like idStaticEntity. The spawnclass is the most fundamental spawnarg of an entity, as it defines the behaviour, the code and how possible spawnargs are interpreted. Use this spawnarg to override an inherited spawnclass." + + "editor_model model" "This defines the visual representation model of this entity. Usually refers to LWO or ASE files. If the visual mesh contains a collision mesh, it will be used as clipmodel, but only if its complexity is not too high and 'noclipmodel' is not set to '1'." + + "editor_var clipmodel" "The 'clipmodel' spawnarg can be used to define which model file should be used as collision mesh for this entity. A collision mesh must be very simple in terms of geometry and must not exceed certain poly/edge/vertex limits. If the 'clipmodel' spawnarg is missing or invalid, the code will either attempt to generate a simple one with 'mins'/'maxs'/'size' or to retrieve a clipmodel from the visual mesh as defined in 'model' (in this order), but only if 'noclipmodel' is '0'." + "editor_bool noclipmodel" "When this is set to '1' (default is '0'), the code will not attempt to generate a clipmodel for this entity or to create one from its visual model (see 'model' spawnarg). This is only effective if 'clipmodel' is empty/invalid, as this spawnarg cannot be used to override a valid 'clipmodel' spawnarg (the latter will still create a valid clipmodel even if 'noclipmodel' is set to '1')." + "editor_var mins" "Together with 'maxs' this can be used to use a primitive code-generated clipmodel for this entity. Mins/Maxs are only applied if the 'clipmodel' is empty/invalid and 'noclipmodel' is not set to 1. Mins defines the 'lower right' corner of the rectangular clipmodel bounds, measured from the entity's origin. Note that this will prevent the code from load a clipmodel from the visual mesh." + "editor_var maxs" "Together with 'mins' this can be used to use a primitive code-generated clipmodel for this entity. Mins/Maxs are only applied if the 'clipmodel' is empty/invalid and 'noclipmodel' is not set to 1. Maxs defines the 'upper left' corner of the rectangular clipmodel bounds, measured from the entity's origin. Note that this will prevent the code from load a clipmodel from the visual mesh." + + "editor_var size" "Similar to 'mins' and 'maxs' this can be used to use a primitive code-generated clipmodel for this entity, one of cubic shape and with the entity origin in its center. The same prerequisites as 'mins' and 'maxs' apply here, but 'size' is only considered when 'mins' and 'maxs' are empty. Note that this will prevent the code from load a clipmodel from the visual mesh." + + "editor_int cylinder" "This only applies if 'mins'/'maxs' or 'size' is used. When this is set to a value greater than 0, the code-generated clipmodel is not rectangular/cubic, but a cylinder where the spawnarg value defines the number of sides. The number is forced to be in the range [3..10] by the code." + "editor_int cone" "This only applies if 'mins'/'maxs' or 'size' is used and 'cylinder' is set to '0'. When this is set to a value greater than 0, the code-generated clipmodel is not rectangular/cubic, but a cone where the spawnarg value defines the number of sides. The number is forced to be in the range [3..16] by the code." + + "editor_var AIUse" "This spawnarg is a hint for AI about what type of item this is. The value is evaluated whenever the AI is receiving a visual stim from this entity. Possible values are enumerated in the C++ code. AIUSE_WEAPON, AIUSE_LIGHTSOURCE, AIUSE_BLOOD_EVIDENCE, AIUSE_DOOR, AIUSE_MISSING_ITEM_MARKER, etc. Consult a coder about this." + + "editor_float move_to_position_tolerance" "The distance at which the AI will consider the position as reached. If < 0, the default bounding box check is used (which equals a distance of 16 at AAS32)." + + "editor_bool is_mantleable" "If set to 1, this entity is not mantleable by the player. Defaults to 0." + "editor_bool nomoss" "If set to true, moss blobs will not spawn on this entity. Defaults to false, but is usually set to true for movers and AI." + + "editor_bool notPushable" "When set to 1, this entity cannot be pushed by the player. If this entity is a moveable, this spawnarg will prevent it from being affected by external impulses as dealt by arrows, for instance." + "editor_var inv_target" "Specifies which entity the inventory item is added to when 'inv_map_start' is set to 1." + + "editor_bool frobable" "If set to 1 the entity is frobable. It will use the default distance unless 'frob_distance' is set with a different value. frobable doesn't have to be set if 'frob_distance' is also set." + "editor_int frob_distance" "Specifes the distance when the frob highlight should become active. This setting will override the default frob distance." + "editor_var frob_peer" "Name of other entity that should also highlight when this entity is frobbed. Multiple names can be given with 'frob_peer1' and so on." + "editor_var frob_action_script" "Script to call when this entity is frobbed." + "editor_bool frob_simple" "Set this to 1 to let the player interact with this entity even when frobbing of more complex items is forbidden (e.g., when carrying a body over their shoulder)." + + "editor_bool immune_to_target_setfrobable" "Set this to 1 to prevent a 'target_set_frobable' entity from changing this entity's frobable status" + + "editor_var used_by" "Contains an entity name of an inventory item that can be used on this object. Multiple used_by keys are possible by numbering them (like 'used_by_3'). A used_by object calls a corresponding used_action_script_, or, if a specific script is not available, it calls the default that is set in the used_action_script key. Doors and locks use this for keys that open them as well." + "editor_var used_by_inv_name" "Same as used_by, but contains the inventory names of the item(s) that be used on this object." + "editor_var used_by_category" "Same as used_by, but contains the inventory categories of the item(s) that be used on this object." + "editor_var used_by_classname" "Same as used_by, but contains the entityDef class names of the item(s) that be used on this object. For example atdm:moveable_dagger." + "editor_var used_action_script" "Contains the script name to call when this object is used by an object on the used_by lists. Different scripts may be called for different items by adding another used_action_script spawnarg followed by the item specifier as shown: item specifier as follows: used_action_script_ . The specifier may be the item's entity name, inventory name, inventory category or entityDef classname." + + "editor_string equip_action_script" "If defined, this script will be run when the user holds the item in his hands and presses the 'Use/Equip' key." + "editor_bool equippable" "When set to true, this entity can be equipped by the player." + "editor_bool grabable" "When set to 1, and this entity is one of the grabable classes (idMoveable, idAFEntity_Base and children), then this entity can be picked up and carried around in the player's hands. Default is 1." + "editor_float frobbox_size" "Determines how much the basic 10 unit frobbox should be increased" + "editor_vector frobbox_mins" "'frobbox_mins' and 'frobbox_maxs' define opposite corners of the frobbox, in coordinates relative to the origin and in whatever orientation the model was modeled in." + "editor_vector frobbox_maxs" "'frobbox_mins' and 'frobbox_maxs' define opposite corners of the frobbox, in coordinates relative to the origin and in whatever orientation the model was modeled in." + "editor_bool no_frob_box" "If true, a frob box sep. from the collision model will NOT be generated, to allow easier frobbing. Default is false, e.g. a frob box is constructed." + + "editor_var editor_replacement" "Points to an entity class that should be used instead of this one. Makes automatically replacing deprecated entity classes possible." + "editor_string editor_usage" "The description of that entity, displayed in the editor to help the mapper to decide whether to use this entity. Can also contain other helpful information." + + "editor_var ragdoll" "Defines the ragdoll (articulated figure) that is to be used when this entity dies." + + "editor_bool drop_set_frobable" "If true, the item becomes frobable after being dropped by the AI that carried it. Occurs f.i. when a guard dies and drops his hammer." + "editor_bool drop_when_ragdoll" "If true, the item will be dropped when the AI it is bound to enters ragdoll state (e.g. either dies or becomes unconscious)." + "editor_bool drop_add_contents_solid" "If true, the item will become solid when being dropped." + "editor_var add_link" "Adds a link from the source entity to the target entity. The format is 'SOURCE to TARGET', where both source and target can either be a plain entity name, or a special string of the form 'ENTITY_NAME:attached:ATTACHMENT_NAME'. The attachment name is usually something like 'flame' or 'candle'. Example adding a link from an attached flame to a light object: 'atdm_candle_holder:attached:flame to my_light'. Note that the attached entity (here: flame) can also attached indirectly, e.g. in this example the candle holder has an attached candle (named 'candle') and this candle has an attached flame (named 'flame'). If you do want the link to go either from or to the entity the spawnarg is set on, you can replace the entity name with 'self', this is f.i. necessary for defining links in entityDefs where the names of entities are not yet known. The 'add_link' spawnarg is useful for adding links to or from entities that are attached via 'def_attach', as these do not exist prior to the map loading and can thus not be targets in the 'link' spawnarg. Multiple add_link spawnargs can be given by adding a suffix like _X where X stands for any number." + + "editor_var def_attach" "Defines an entity that is attached to this entity at spawntime. The offset, name of the attachment position an name of the attachment entity can be set with 'attach_pos_origin', 'pos_attach' and 'name_attach', respectively." + "editor_var def_flinder" "For objects that have a 'health' set, multiple flinder objects (e.g. def_flinder, def_flinder_1 etc.) can be defined with this spawnarg. When the object is destroyed, these flinders are then spawned. The offset for each flinder objects can be set with 'flinder_offset'." + "editor_vector flinder_offset" "Sets the offset for the corrosponding flinder object defined with 'def_flinder'." + + "editor_material mtr_wound" "The decal to splat when this entity is wounded. Suffixes with a surface type name like 'flesh' can be used to limit this decal to the appropriate surface type. Example: 'mtr_wound_flesh'" + "editor_material mtr_killed_splat" "The decal to splat when this entity is killed. Multiple spawnargs with suffices can be given, one of them will then be randomly choosen." + + "editor_vector teleport_offset" "When this entity is teleported via teleportTo(), this offset is added to the location of the teleport target origin. See also 'teleport_random_offset'." + "editor_vector teleport_random_offset" "When this entity is teleported via teleportTo(), a random offset with that magnitude is added to the location of the teleport target origin. See also 'teleport_offset'." + + "editor_float hide_distance" "If set greater than zero, this entity will hide itself when it is greater than this distance from the player, but only if 'dist_check_period' > 0." + "editor_float lod_normal_distance" "If set greater than zero, this entity ignore any LOD BIAS under 1.0f when closer than this distane. E.g. the menu setting Object Detail will be ignored if it is lower than Normal. This stops the entity from vanishing just because the player has set a too low quality. Default is 500 units, but can be lower for very small items like grass and bigger for big items like statues, trees, or portals." + "editor_float lod_hide_probability" "Between 0 and 1.0, default is 1.0. If 'hide_distance' is set, this is the probability that this entity is hidden. F.i. a value of 0.2 means 20% of all entities of this class will hide after the hide_distance." + "editor_float dist_check_period" "If hide_distance is used, this sets the interval between distance checks, in seconds. Default is 0. Set to > 0 to turn on the LOD system, see 'hide_distance', 'lod_model_1' etc." + "editor_bool dist_check_xy" "If true, the LOD distance check is only done orthogonal to gravity, i.e., vertical distances aren't counted in it. Useful for things like rain clouds turning off/on." + + "editor_float lod_fadeout_range" "Does not work yet. When > 0, the entity being further away than 'hide_distance' but less than 'hide_distance' + 'lod_fadeout_range' fades away inside that distance." + "editor_float lod_fadein_range" "Does not work yet. When > 0, the entity being nearer than lod_1_distance fades slowly in from 'lod_1_distance' - 'lod_fadein_range' to 'lod_1_distance'." + + "editor_float lod_1_distance" "LOD System: Distance where to switch from the normal stage to stage 1." + "editor_float lod_2_distance" "LOD System: Distance where to switch from the normal stage to stage 2." + "editor_float lod_3_distance" "LOD System: Distance where to switch from the normal stage to stage 3." + "editor_float lod_4_distance" "LOD System: Distance where to switch from the normal stage to stage 4." + "editor_float lod_5_distance" "LOD System: Distance where to switch from the normal stage to stage 5." + "editor_float lod_6_distance" "LOD System: Distance where to switch from the normal stage to stage 6." + + "editor_model model_lod_1" "LOD System: Use the specified model for LOD stage 1." + "editor_model model_lod_2" "LOD System: Use the specified model for LOD stage 2." + "editor_model model_lod_3" "LOD System: Use the specified model for LOD stage 3." + "editor_model model_lod_4" "LOD System: Use the specified model for LOD stage 4." + "editor_model model_lod_5" "LOD System: Use the specified model for LOD stage 5." + "editor_model model_lod_6" "LOD System: Use the specified model for LOD stage 6." + "editor_bool noshadows_lod_1" "LOD System: If set to 1, disable shadows in LOD stage 1." + "editor_bool noshadows_lod_2" "LOD System: If set to 1, disable shadows in LOD stage 2." + "editor_bool noshadows_lod_3" "LOD System: If set to 1, disable shadows in LOD stage 3." + "editor_bool noshadows_lod_4" "LOD System: If set to 1, disable shadows in LOD stage 4." + "editor_bool noshadows_lod_5" "LOD System: If set to 1, disable shadows in LOD stage 5." + "editor_bool noshadows_lod_6" "LOD System: If set to 1, disable shadows in LOD stage 6." + "editor_skin skin_lod_1" "LOD System: Use the specified skin for LOD stage 1." + "editor_skin skin_lod_2" "LOD System: Use the specified skin for LOD stage 2" + "editor_skin skin_lod_3" "LOD System: Use the specified skin for LOD stage 3." + "editor_skin skin_lod_4" "LOD System: Use the specified skin for LOD stage 4." + "editor_skin skin_lod_5" "LOD System: Use the specified skin for LOD stage 5." + "editor_skin skin_lod_6" "LOD System: Use the specified skin for LOD stage 6." + + "editor_vector offset_lod_1" "At each LOD stage, the visual model for this entity can have an offset applied, so it matches exactly the position at LOD stage 0 to correct for models where different stages have different origins." + "editor_vector offset_lod_2" "At each LOD stage, the visual model for this entity can have an offset applied, so it matches exactly the position at LOD stage 0 to correct for models where different stages have different origins." + "editor_vector offset_lod_3" "At each LOD stage, the visual model for this entity can have an offset applied, so it matches exactly the position at LOD stage 0 to correct for models where different stages have different origins." + "editor_vector offset_lod_4" "At each LOD stage, the visual model for this entity can have an offset applied, so it matches exactly the position at LOD stage 0 to correct for models where different stages have different origins." + "editor_vector offset_lod_5" "At each LOD stage, the visual model for this entity can have an offset applied, so it matches exactly the position at LOD stage 0 to correct for models where different stages have different origins." + "editor_vector offset_lod_6" "At each LOD stage, the visual model for this entity can have an offset applied, so it matches exactly the position at LOD stage 0 to correct for models where different stages have different origins." + "editor_vector offset_lod_7" "At each LOD stage, the visual model for this entity can have an offset applied, so it matches exactly the position at LOD stage 0 to correct for models where different stages have different origins." + + "editor_bool seed_watch_brethren" "When this entity is a target of a SEED and this is true, the SEED will watch all entities of the same class inside itself and remove this entity. Can be used to add SEED to already placed entities in the map." + "editor_bool seed_floor" "When set to true, entities created via SEED will be floored, that is, put down along the gravity axis until they collide with something." + "editor_bool seed_floating" "When set to true, entities created and floored via SEED will be float in air if they hit the bottom of the SEED entity. Default is false, which means these entities will be removed." + "editor_float seed_base_density" "Is multiplied by the density set by the mapper, used to set a base line density for various entities, and a seperate setting so the mapper can always assume the default 'density'/'seed_density' are 1.0" + + "editor_float seed_sink_min" "The minimum number of units entities of this class sink into the floor." + "editor_float seed_sink_max" "The maximum number of units entities of this class sink into the floor. Must be greater than 'sink_min'." + + "editor_vector seed_scale_min" "Minimum scaling factor for each rendermodel. Default is '1.0 1.0 1.0'. Works ONLY on non-solids. Three floats, one for each axis in the order of X Y Z. Alternatively, only a single float, this will be the minimum scale for all three axes equally." + "editor_vector seed_scale_max" "Maximum scaling factor for each rendermodel. Default is '1.0 1.0 1.0'. Works ONLY on non-solids. Three floats, one for each axis in the order of X Y Z. Alternatively, only a single float, this will be the minimum scale for all three axes equally." + + "editor_int seed_score" "Score for this class. All scores together will form the distribution for the SEED entity." + + "editor_bool seed_noinhibit" "SEED System: If set to 1, inhibitors will not inhibit (aka ignore) this entity class." + + "editor_float seed_probability" "SEED System: Default probability (0..1.0) that an entity will spawn on a given material (define these with 'seed_material_N' where xyz is grass, stone etc.). If no 'seed_material_N' spawnargs are set, seed_probability is ignored." + + "editor_float seed_material_N" "SEED System: For the given surface type N (grass, stone, metal etc.), gives the probability that the entity will spawn there. 0.0 to disable, and 1.0 to always spawn, values in between act accordingly." + + "editor_float seed_color_min" "Used to randomly color entities generated by the SEED system. Sets the minimum color. Example: 0.5 0 0.5" + "editor_float seed_color_max" "The maximum color value used to randomly color entities generated by the SEED system. Example: 1 1 1" + + "editor_float seed_density" "A multiplier for the density for this class. Default: 1.0. See 'densitiy' on the SEED entity." + + "editor_float seed_rotate_min" "Used to randomly rotate entities generated by the SEED system. Sets the minimum angles as pitch, yaw, roll (x z y)." + "editor_float seed_rotate_max" "Used to randomly rotate entities generated by the SEED system. Sets the maximum angles as pitch, yaw, roll (x z y)." + + "editor_string seed_map" "Path to image file (f.i. textures/seed/grassland.tga) that specifies the distribution as greyscale image, white = 100%, black = 0% spawn probability. The probabiliy is combined with the one from falloff, so you can load an image and let the values falloff towards the edge. The image map is stretched to fit the SEED size. The string 'textures/seed' as well as the extension are optional. If the extension is missing, tries first .png and then .tga." + "editor_bool seed_map_invert" "If true, inverts the image loaded via 'map'." + + "editor_skin random_skin" "Specify a list of skins seperated by ',' (use '' for the default skin). At spawn time, one of them will be choosen randomly" + "editor_bool is_torch" "If true, this object can be used to relight doused flames." + + "editor_float min_lod_bias" "The entity is NOT visible (unless 'lod_hidden_skin' is set) if the LOD Bias (Object details setting in the menu) is lower than this value or higher than 'max_lod_bias'. The menu settings are Low (0.5), Lower (0.75), Normal (1.0), High (1.5), Higher (2.0) and Very High (3.0)." + "editor_float max_lod_bias" "The entity is NOT visible (unless 'lod_hidden_skin' is set) if the LOD Bias (Object details setting in the menu) is higher than this value or lower than 'min_lod_bias'. The menu settings are Low (0.5), Lower (0.75), Normal (1.0), High (1.5), Higher (2.0) and Very High (3.0)." + "editor_skin lod_hidden_skin" "If set, the entity will switch to this skin if 'min_lod_bias' and 'max_lod_bias' are set and the menu setting Object Detail is not between these two values. Can be used to make entities switch skins on lower or higher detail settings." + + "editor_float random_remove" "Defaults to 1.0. Must be between 0 and 1.0. Give the chance that this entity will appear at map start. So setting it to 0.3 will make it appear with a chance of 30% (e.g. be removed with a 70% chance), setting it to 0.8 means the entity has a 80% chance to appear in the map. Used every time the map is loaded, but ignored upon loading a map from a save game." +} + +entityDef func_static +{ + "inherit" "atdm:entity_base" + "editor_color" "0 .5 .8" + "editor_mins" "?" + "editor_maxs" "?" + "inline" "0" + + "editor_displayFolder" "Func" + "editor_usage" "A brush model that just sits there, doing nothing. Can be used for conditional walls and models." + "editor_usage1" "When triggered, toggles between visible and hidden (must add 'hide' value to entity manually). Entity is non-solid when hidden." + + "editor_bool solid" "Whether the object should be solid or not." + "editor_bool hide" "Whether the object should be visible when spawned. you need to set a value for triggering on/off to work" + "editor_gui gui" "GUI attached to this static, gui2 and gui3 also work" + "editor_gui gui_demonic" "Demonic gui attached to this statit, gui_demonic2 and gui_demonic3 also work" + "editor_bool gui_noninteractive" "Any gui attached will not be interactive." + "editor_bool noclipmodel" "0 by default. Sets whether or not to generate a collision model for an ASE/LWO func_static at level load. (Set to 1 for stuff the player can't interact with. This will save memory.)" + "editor_bool inline" "If true, turns an FS model into map geometry at dmap time, saving entity count. The entity will be non-solid and inlining won't preserve texture blending on a single mesh so check your model looks ok in game before doing 100 of them." + "editor_bool is_mantleable" "Whether this entity can be mantled." + + "spawnclass" "idStaticEntity" + + "solid" "1" + "noclipmodel" "0" +} diff --git a/test/resources/tdm/def/lights_static.def b/test/resources/tdm/def/lights_static.def index 760e9024f9..dbe3ecf9bc 100644 --- a/test/resources/tdm/def/lights_static.def +++ b/test/resources/tdm/def/lights_static.def @@ -38,19 +38,10 @@ entityDef atdm:torch_brazier "inherit" "atdm:torch_wall_base" "editor_usage" "floor mounted, lit brazier" "editor_displayFolder" "Lights/Model Lights, Static/Fires" - - "model" "models/darkmod/lights/extinguishable/brazier.lwo" - - // attach the light, so the torch can be re-lit by fire stims + "model" "models/torch.lwo" "noshadows_lit" "1" // turn off shadow when lit "noshadows" "0" // unlit, so has shadows - "attach_pos_origin_1" "0 0 10" // Offset the flame - - "def_attach" "light_cageflame_small" - -// "skin" "torch_lit" -// "skin_lit" "torch_lit" -// "skin_unlit" "torch_unlit" + "def_attach" "light_cageflame_small" } diff --git a/test/resources/tdm/models/torch.lwo b/test/resources/tdm/models/torch.lwo new file mode 100644 index 0000000000000000000000000000000000000000..759d5c971ebc60ca9a5eca44f8506bb46cd54435 GIT binary patch literal 8842 zcmai)2YeOP^2g`o1_%j|0HL?gJ4oo|?#WF^NFbyK2+|=!fRI4wpi%?@0g)yky-E{- zhZVDl`c#@Cq97_DJ`q7tda$Q;?H4jwa?!7(R;rXFYojE*Ld*MBbF) zLklM6j2lUbzN|FFt^gkRNlD5;t3mlXDD}Ij0~u5G*cumf&f(J{m;82`aeIAeaoFZ4 zWATpZ#-g2x#bI|R89#Qprx#fM98L%CyTEpqO;;S&CC>csidg12y3CI0g|0ZJD~{=E zx6swD`5)q&=%?dvqMwd`A^PdWFXVpO{b7IZ1NX;t^~XeioNb^N(qCys$Q`u@?;zJPl>*{08LJIAt~UcU?LQFp$+%%x*DVb?RwgW_}H z%XqF6`L{;1O_$wt#W7v+9XrQy{7qLJ6LFmQYaaN_e`a>m8}PJejpPJeihIpY$4d+aj* zu9!ME`I4{k{R{fk?2`J#$Sop|zVMnJbE-qsr_l>^&=2i(kf(>p`FpbU_X%n2!|mvW z7e}-G_5FJ{nXWj}AC7tbqB@%U%qV$w;pWC>XxFi1ck_W z#~xbkYj-nP2kojYF(Xe7a@$SVb-isR*k3RHWQ+K)|AKAjOqbns#W7v+9XrQy{7qLJ z6LFmQY}d;+B=|bbd{qB78N_!ce5=0arsCb_K&z* zy2#~I|hEA_ju%rTsp z+q4p%pZh5?KYQs+gwr?nH(mZtKe;W_)s}mVrmJ1YpZo0io9LSppU3XR zH(mYV@jCr6(I2P(?CK;AhPfce=S{ zw~r$myW8J(eDt}OrvIJY>AzOn$Z0pK#o;`+?DmO{PVR@ftIBJ_XC{xs87n{6r?)SK zs8scgewlqJAhS~IN>#^t!Rw? z=3|kib8-_0s zsfLkXa%s0Gj$Edi#)|8Q#jlnz=dI&XTU*-TXX_X#!;-`nVYCXLA%1m@8udODTRrcU zIXMw7pbvGj~of1CPw4UcXfWdan#g1 z%7~WQX5O&XN5$5>+puccn*CacZD6+!)KdJ?kA6h0BnnHdr45!Il6dUbhHpLcY^1Dt z%Bat&t*kjq?fk3X-oGO{$eMF)NB`<~k~L>rXIXQWy7*VWtA9sylQrjBudF#shJW?7 ze@8^gnscpB)|{ni|LVv1cSLttbFPh*HD@W#zxqA=J0f1zoNE*OtKUK%1L&!1_y?ssjM&d^tBgpNHjQEb^_GX^ZDM|9ea zntJf1t~63=2e(R~QM%LoEV1S2$T>cFcC>Dua!{TfBQ}0^toTK~(T#HD*|sCM&^U?5 zwmhCw9e8v!F2Ojx?Ch)9@t$=4zq=`I3B2D616KOKfhDcL*hD4gmBNu5J z&xT0TdE_F^kaokL+etH}U6vk|c3FB%+GXi+X_uv0(k@H0rCpZhNV_b}m3CQrLfU0% zp0vx-d})`ZC-sbhCuN2f=-k^c=^D$Jb)JK zeqqD(sQvqBiOe6{mWq_yWC=a3o8gAfwq?3)|M}wgjIK2$QX((cjdZv~UZKOv){w|6 zb;O^~inL1nB8SeT)#BH2a6UaN@oJ2pOlu@w)0-!0t+d9rbs{w^xIxcJ|Jb%(`p147 zWJDZqql}2-ZQ_$d&%(!MU`cXA6x&w6ZJS@(?w5A3BtJWi#FO&77wuw6o_(HcMB43_ z_82Ga+Py5vv-|wD`~A`jy4u=xQhR_UiSnYq_Ml%nq@C6dv&8n4Jv`1A6H6Wn zAO$0r{K;hr3Pc$QdjQHHDn%txmZZ{TQ+3rsXs)G6|@U#3Yi(XZF$vv+| zRqJuycJ)JZ05ahs=5{MD&0vwqD)#uO)hI^unsIMYZ8qo9dv4 z9uRKpwQT=7qDNpry@V)uZ~DK@TWn>ve7a z`l2_$3Rv~^sR8^NVB9=B561^R@rAX+J)SlX*Y)2ib1IiAtv)+X|Qzw+2s4HaF+uQz~MK{Px z-C*xVnhcZ%{49U3=+We(DA=PYhN4kM!_V^fi5^F>)E)Nj)Pv$s#=+0>j}<+E;_d!) zqy*F_z|ZoJ7d?@BQBT-=Qg2E`nMlc$gfaO_o95tgPw;`_d?kVy%~>kJnXEeQUOgsIRT?i6}_kJUm$uWO`?ggPsFG* zQD(x=@}DGn7EPhauusOQvruNi&+?xl`aqgSQ(>QqQ4d5p5Pp{bG|`99bh|%Em?;k! zVEa!OJ)dUM4A^I&SNSOO;r%GeM`35ZfF7sEP(FrU6^Nc^`#&!FB$`dLV4sCvO+q;d zewP1i(WlT{ngjbB^lA#qDe$xW=ZZd!=FtF+GLyDcCK2xV_$sZGDlwABKy*l$O~3i)ksITT095X_QaH zZs{ZK{jto}7u)ehioTqlvHh3Pay+-3R?-TTD`2d(bv&h+kXwM!*lCsJw1o=IoK^d*WRz|ZGDX$FIV)9 zw88dYPaE;vM$Dr%pBrpF+n%pQw!Yqumo54h+#@#Ox$U%#wxZmL&mB_k5`7Cj4-XIR zM*Vh_duSJYJhT@Qms@BbJUp}?^}A8N0Q+7k4?yDj7vbTdgQ(w+@(>+>kB1IJ;&KaG ze-Y(Y)E`8-1NOsGZlyyh_+tW5V3`zOS_ZPcl7D%7C(< z9N<$W43q~IKt&J^DuK$N3aARIf$E?Js0nI;+Mo`I0Chn7sC+D4Z$^r-Q=Tp>TRAoD~Y$Tj5MmI29Dm5`|Mk;fzo?1r*K> zg;PS|Oi;-B3TK7Fsi1HMC}eqsvq9k$P{{cT=YT@SSIA=unM)zNE97#8e5R1|74nio z#!|@Y3OQUM4=H3Qg)FX+vla4=LPk=^+6p;ZA z3K>HoD=1_rh5V(EITW&yLJm{NItsZ;A-gE#B!w)Zkb4yJi^7gq$QKG3L1F(ZWCVpg zqL2d=-g1RETp=$gyweJAt->3t@SZEYqY7`K!W*dY-YUFv3U8Id8>R3bD!f|?Z;8Sl zRd@>&cB{fpRoJTvZ-c^aRd@>&-U)@bK;aEg*!2o;fWod<*zpRxTw%{E>~e)2uCTuq zcDSN{fFs}~@G^J>yb4|euY;rD4NwS7Py~v>o8T?*Huxua2fPd31INH|Z~~kJr@;H* z18^FA2+n|yz*+Dy_yn8-=fS7o0{9Gk4!!^v!6k4RdVpQLA!q~|gC?LUXa<^t7N8|)1zLlLKpPMV+JbhVJ?H>Bf=-|_ z=mNTeZomr+pg|PyfoKo|x`SB2H^m+x9`LP@Z-9K8;TvCXkOYze-{kmKlLpcO-`w~X z(+Bhgd{g6F3*YP>27EK)TMFOg1_Qp4@U4w+Xu|>Dz_J0~&_)8jndJb!k&OX-6UznT z0N=X!hQhZkzA^Fbgl|TC3*j3O-!}MW#J38*Iq)rKCg9r*-%uV0d=r@s_{K38@J(bM zm=E|ATmbkMu?X<3Vlh|(mIA&dECbH~KI!??UkO$LKDqf6Ujx zyb4|euY;rD4NwS7Py~v>o8T?*Huxua2fPd31INH|Z~~kJr@;H*18^FA2+n|yz*+Dy z_yn8-=fS7o0{9Gk4!!^v!6k4RdW_%Z2O>0G9yCHF*G!q+c> z^#Js<2iyCmp?%J~s&86KUuXUx$2+q8)uW92uN^*LdIsOrxNj*5{bW4$swK*NKJY(1 X{IeVX-Z5=M^mnss&8U9&*46(5G~A`^ literal 0 HcmV?d00001 From 7ffc528b5c364761bbeb6d466f00ee1ad3965093 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Fri, 5 Feb 2021 19:54:24 +0000 Subject: [PATCH 33/48] Refactor overridden SetUp() and TearDown() Since any non-trivial test must rely on RadiantTest's standard SetUp() and TearDown() code to correctly start and stop the core module, test subclasses must use the fragile CallSuper anti-pattern if they want to add any custom code to this phase. RadiantTest now defines empty virtual methods preStartup(), preShutdown() and postShutdown() which subclasses can override to inject custom code at the appropriate time, without having to remember to call methods on the base class to avoid breaking the setup procedure. --- test/ColourSchemes.cpp | 16 +++++----------- test/Favourites.cpp | 10 +++------- test/MapSavingLoading.cpp | 34 ++++++++++++++++------------------ test/RadiantTest.h | 18 ++++++++++++++++++ 4 files changed, 42 insertions(+), 36 deletions(-) diff --git a/test/ColourSchemes.cpp b/test/ColourSchemes.cpp index ec69e50b11..f3cd76343c 100644 --- a/test/ColourSchemes.cpp +++ b/test/ColourSchemes.cpp @@ -41,11 +41,9 @@ class ColourSchemeTestWithIncompleteScheme : public ColourSchemeTest { public: - void SetUp() override + void preStartup() override { copySchemeFileToSettingsPath("colours_incomplete.xml"); - - RadiantTest::SetUp(); } }; @@ -56,15 +54,13 @@ class ColourSchemeTestWithUserColours : fs::path _userDefinedSchemePath; public: - void SetUp() override + void preStartup() override { // Store the value locally, the test case wants to have it _userDefinedSchemePath = _context.getTestResourcePath(); _userDefinedSchemePath /= "settings/colours_userdefined.xml"; copySchemeFileToSettingsPath("colours_userdefined.xml"); - - RadiantTest::SetUp(); } }; @@ -72,15 +68,13 @@ class ColourSchemeTestWithEmptySettings : public ColourSchemeTest { public: - void SetUp() override + void preStartup() override { // Kill any colours.xml file present in the settings folder before regular SetUp fs::path coloursFile = _context.getSettingsPath(); coloursFile /= "colours.xml"; - - fs::remove(coloursFile); - RadiantTest::SetUp(); + fs::remove(coloursFile); } }; @@ -457,7 +451,7 @@ TEST_F(ColourSchemeTestWithUserColours, ForeachScheme) }; EXPECT_EQ(expectedSchemeNames.size(), visitedSchemes.size()); - + for (auto expectedScheme : expectedSchemeNames) { EXPECT_EQ(std::count(visitedSchemes.begin(), visitedSchemes.end(), expectedScheme), 1); diff --git a/test/Favourites.cpp b/test/Favourites.cpp index 9dcd890f5a..0690cabcf0 100644 --- a/test/Favourites.cpp +++ b/test/Favourites.cpp @@ -26,10 +26,8 @@ class FavouritesTest : fs::copy(sourcePath, targetPath); } - void TearDown() override + void postShutdown() override { - RadiantTest::TearDown(); - if (checkAfterShutdown) { checkAfterShutdown(); @@ -41,11 +39,9 @@ class FavouritesTestWithLegacyFavourites : public FavouritesTest { public: - void SetUp() override + void preStartup() override { copyUserXmlFileToSettingsPath("old_mediabrowser_favourites.xml"); - - RadiantTest::SetUp(); } }; @@ -55,7 +51,7 @@ TEST_F(FavouritesTest, AddingAndRemovingFavourites) // Add caulk GlobalFavouritesManager().addFavourite(decl::Type::Material, "textures/common/caulk"); - + EXPECT_EQ(GlobalFavouritesManager().getFavourites(decl::Type::Material).size(), 1); EXPECT_EQ(GlobalFavouritesManager().getFavourites(decl::Type::Material).count("textures/common/caulk"), 1); EXPECT_TRUE(GlobalFavouritesManager().isFavourite(decl::Type::Material, "textures/common/caulk")); diff --git a/test/MapSavingLoading.cpp b/test/MapSavingLoading.cpp index 98848c54a4..9ff5049480 100644 --- a/test/MapSavingLoading.cpp +++ b/test/MapSavingLoading.cpp @@ -96,21 +96,19 @@ class FileOverwriteHelper } -class MapFileTestBase : +class MapFileTestBase : public RadiantTest { private: std::list _pathsToCleanupAfterTest; protected: - virtual void TearDown() override + void preShutdown() override { for (const auto& path : _pathsToCleanupAfterTest) { fs::remove(path); } - - RadiantTest::TearDown(); } // Creates a copy of the given map (including the .darkradiant file) in the temp data path @@ -186,7 +184,7 @@ void checkAltarSceneGeometry() { auto root = GlobalMapModule().getRoot(); auto worldspawn = GlobalMapModule().findOrInsertWorldspawn(); - + // Check a specific spawnarg on worldspawn EXPECT_EQ(Node_getEntity(worldspawn)->getKeyValue("_color"), "0.286 0.408 0.259"); @@ -194,7 +192,7 @@ void checkAltarSceneGeometry() auto knownEntities = { "func_static_153", "func_static_154", "func_static_156", "func_static_155", "func_static_63", "func_static_66", "func_static_70", "func_static_164", "func_static_165", "light_torchflame_13", "religious_symbol_1" }; - + for (auto entity : knownEntities) { EXPECT_TRUE(algorithm::getEntityByName(root, entity)); @@ -209,7 +207,7 @@ void checkAltarSceneGeometry() // Check number of brushes auto isBrush = [](const scene::INodePtr& node) { return Node_isBrush(node); }; - + EXPECT_EQ(algorithm::getChildCount(root, isBrush), 37); // 37 brushes in total EXPECT_EQ(algorithm::getChildCount(worldspawn, isBrush), 21); // 21 worldspawn brushes EXPECT_EQ(algorithm::getChildCount(algorithm::getEntityByName(root, "func_static_66"), isBrush), 4); // 4 child brushes @@ -262,7 +260,7 @@ TEST_F(MapLoadingTest, openMapFromAbsolutePath) { // Generate an absolute path to a map in a temporary folder auto temporaryMap = createMapCopyInTempDataPath("altar.map", "temp_altar.map"); - + GlobalCommandSystem().executeCommand("OpenMap", temporaryMap.string()); // Check if the scene contains what we expect @@ -279,7 +277,7 @@ TEST_F(MapLoadingTest, openMapFromModRelativePath) fs::path mapPath = _context.getTestProjectPath(); mapPath /= modRelativePath; EXPECT_TRUE(os::fileOrDirExists(mapPath)); - + GlobalCommandSystem().executeCommand("OpenMap", modRelativePath); // Check if the scene contains what we expect @@ -343,7 +341,7 @@ TEST_F(MapLoadingTest, openMapFromArchiveWithoutInfoFile) GlobalCommandSystem().executeCommand("OpenMapFromArchive", pakPath.string(), archiveRelativePath); - // Check if the scene contains what we expect, just the geometry since the map + // Check if the scene contains what we expect, just the geometry since the map // is lacking its .darkradiant file checkAltarSceneGeometry(); } @@ -384,13 +382,13 @@ TEST_F(MapLoadingTest, openWithInvalidPathInsideMod) TEST_F(MapLoadingTest, openMapWithoutInfoFile) { auto tempPath = createMapCopyInTempDataPath("altar.map", "altar_openMapWithoutInfoFile.map"); - + fs::remove(fs::path(tempPath).replace_extension("darkradiant")); EXPECT_FALSE(os::fileOrDirExists(fs::path(tempPath).replace_extension("darkradiant"))); GlobalCommandSystem().executeCommand("OpenMap", tempPath.string()); - + checkAltarSceneGeometry(); EXPECT_EQ(GlobalMapModule().getMapName(), tempPath.string()); @@ -440,7 +438,7 @@ TEST_F(MapLoadingTest, loadingCanBeCancelled) TEST_F(MapSavingTest, saveMapWithoutModification) { auto tempPath = createMapCopyInTempDataPath("altar.map", "altar_saveMapWithoutModification.map"); - + GlobalCommandSystem().executeCommand("OpenMap", tempPath.string()); checkAltarScene(); @@ -528,7 +526,7 @@ TEST_F(MapSavingTest, saveMapCreatesInfoFile) fs::path tempPath = _context.getTemporaryDataPath(); tempPath /= "just_a_worldspawn.map"; auto infoFilePath = fs::path(tempPath).replace_extension("darkradiant"); - + auto format = GlobalMapFormatManager().getMapFormatForFilename(tempPath.string()); FileSelectionHelper responder(tempPath.string(), format); @@ -643,7 +641,7 @@ TEST_F(MapSavingTest, saveAs) tempPath /= "altar_copy.map"; EXPECT_FALSE(os::fileOrDirExists(tempPath)); - + // Respond to the event asking for the target path FileSelectionHelper responder(tempPath.string(), format); @@ -825,7 +823,7 @@ TEST_F(MapSavingTest, saveMapxCreatesBackup) // Mapx backup should be there now EXPECT_TRUE(os::fileOrDirExists(fs::path(mapxPath).replace_extension("bak"))); - + fs::remove(mapxPath); fs::remove(fs::path(mapxPath).replace_extension("bak")); } @@ -924,7 +922,7 @@ TEST_F(MapSavingTest, saveArchivedMapWillAskForFilename) // the map saving algorithm should detect this and ask for a new file location auto pakPath = fs::path(_context.getTestResourcePath()) / "map_loading_test.pk4"; std::string archiveRelativePath = "maps/altar_packed.map"; - + GlobalCommandSystem().executeCommand("OpenMapFromArchive", pakPath.string(), archiveRelativePath); checkAltarScene(); @@ -957,7 +955,7 @@ TEST_F(MapSavingTest, saveArchivedMapWillAskForFilename) // Save again, this should no longer ask for a location GlobalCommandSystem().executeCommand("SaveMap"); - + EXPECT_FALSE(eventFired); GlobalRadiantCore().getMessageBus().removeListener(msgSubscription); diff --git a/test/RadiantTest.h b/test/RadiantTest.h index ad24f77c32..8604c13c49 100644 --- a/test/RadiantTest.h +++ b/test/RadiantTest.h @@ -67,8 +67,14 @@ class RadiantTest : } } + /// Override this to perform custom actions before the core module starts + virtual void preStartup() {} + void SetUp() override { + // Invoke any custom actions needed by subclasses + preStartup(); + // Set up the test game environment setupGameFolder(); setupOpenGLContext(); @@ -101,15 +107,27 @@ class RadiantTest : GlobalMapModule().createNewMap(); } + /// Override this to perform custom actions before the main module shuts down + virtual void preShutdown() {} + void TearDown() override { + // Invoke any pre-shutdown custom code + preShutdown(); + _coreModule->get()->getMessageBus().removeListener(_notificationListener); _coreModule->get()->getMessageBus().removeListener(_gameSetupListener); // Issue a shutdown() call to all the modules module::GlobalModuleRegistry().shutdownModules(); + + // Invoke any post-shutdown custom code + postShutdown(); } + /// Override this to perform custom actions after the main module shuts down + virtual void postShutdown() {} + ~RadiantTest() { _coreModule->get()->getLogWriter().detach(_testLogFile.get()); From 3e8f58a2e8b3b026d961b9c4b1421a4d69db0e2e Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sat, 6 Feb 2021 13:05:40 +0000 Subject: [PATCH 34/48] Improve readability of show light/speaker radius icons These original icons are quite difficult to distinguish at 16x16 on high-res displays. Replace with flat designs based on a large orange light bulb and a large purple loudspeaker symbol to make the difference clearer. --- install/bitmaps/view_show_lightradii.png | Bin 375 -> 571 bytes install/bitmaps/view_show_speakerradii.png | Bin 377 -> 462 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/install/bitmaps/view_show_lightradii.png b/install/bitmaps/view_show_lightradii.png index 99533d139bd579d0e6e821c6bfbc0e4abf338590..447f039a8f9aa250272fe07eacfa9a4272a2895c 100644 GIT binary patch delta 559 zcmV+~0?_^U0=oo|7k>~41^@s6AM^iV00009a7bBm000id000id0mpBsWB>pF8FWQh zbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10mw;2K~y-6jgw7l6Hyd~pEHx0 zDMly;g+f%is|&l(ji`&(g1C|Vj)(+Tb|!8tg6&MzRlxiNyMGb!V<&>uP0^jA)rv@5 z6tb~yLgyOi9v7LECbrSLy1eIoAI>?KE4qNgHuDHoI!s9e!-%6b0*ZV>XcBO@KuU

blS9FQTz`vN541Ap)ct7FdA@Aba!{^)LC2@oF2 z5+6s60Pp~y@cq~L4xnYm^G;TV&3^-2)&ShCZh1%gnH|gBYySp#v;o9uDOao*dk6rZ z_OM+vq6HjBsYud$g=qdpsK~&wP;Mr7Zr=jXIu(@1K+8dC_E0?08UaAOyqE|Y?HR5S x{31^@s6?Q>5r00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru-U1pE8Uo?RwYmTR0R%}zK~yNum6F?T+dvEj=TOdH zH!hdX{u-(UXJwJs)&W1Lu zas^H7$y5uAVLgP@ST4vwBnc>6qzEy)frvMYvYXJ0LJEMPPOv5d=kMu+VMi!x{6q`5OP36QlI@(n(%hmn_00000NkvXX Hu0mjf7yF*+ diff --git a/install/bitmaps/view_show_speakerradii.png b/install/bitmaps/view_show_speakerradii.png index 1a1ff1c97d60c643c57d68160ab2955514a79949..afe455cb4338a6c2626ba2bdaa7069d396e6b3fe 100644 GIT binary patch delta 449 zcmV;y0Y3iu0?q@F7k>~41^@s6AM^iV00009a7bBm000id000id0mpBsWB>pF8FWQh zbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H10b5B#K~y-6?U7F?g8>-EfA80{ zl7kBehs|ZVtQ;&inH?O~?DS1a2^XV=6IUli)L6DC3dy9`S+2t=k)%4p6BfqolzrqU6>Np=wn@dOEBSi!Uow`(OLU&e*v26bo&+@M_2i2 zL1yii08-uCjpd4!00uytfw|*SK@5;6fMrPNJN0-V9Vu6=bHEHHJWr^G0aSF<0k{AH z>%RSui_0CG4S!lANmR^g$pY|iv}bZdc>uTLJInDvI-)8oC=GU}y(3ua+S0c~Kh1s# z<9B2K1a^4$g^5E`UgNkw9TH&`r0Gu%CUcR!no8LVto=lA4Uii=>-pFmJMF)lI5bB@ z2m?2-^`qf@xEfv=Dv|;~WH%ctI=@mGKQJ@$Y_$rcvpTV&Q{31^@s6?Q>5r00006VoOIv0RI600RN!9r;`8x010qNS#tmY z3ljhU3ljkVnw%H_000McNliru-U1pE8Uo?RwYmTR0R~A#K~yNuos+#z0znYRe{x2w=*7l2fQH6c;DZ?7LpnC9Yn!IU94YMe!&A4`;F|I_{&YTArPh2Islaw$%OXSqfGe3T_MP`Zf|+ zLz-}uCLG(~`TVU!;3kOOw~jsl_yTx-Dww^z5+_OUZ(^VF>iPzNZnsAi3E!(#FhLYD zs49!aC#u`K>|{kSn@&nO%g%{ft-m8!Dz~FkSSrK2f#LEZ<7d4NL-2J0Km-mCIH|VL z*M-I$*u*0BS?HuY=D Date: Thu, 4 Feb 2021 20:08:36 +0000 Subject: [PATCH 35/48] Initial list of attached entities in EntityNode EntityNode maintains a list of entity node pointers representing the attached entities, which are submitted at render time. The basic rendering is working and covered by a unit test, but the attached entity appears at the origin since there is not yet any handling of the localToWorld matrix. --- radiantcore/entity/EntityNode.cpp | 49 ++++++++++++++++--- radiantcore/entity/EntityNode.h | 47 +++++++++++++++++- .../rendersystem/backend/OpenGLShaderPass.cpp | 14 +++--- test/Entity.cpp | 49 ++++++++++++++----- test/resources/tdm/def/lights.def | 22 +++++++++ 5 files changed, 156 insertions(+), 25 deletions(-) diff --git a/radiantcore/entity/EntityNode.cpp b/radiantcore/entity/EntityNode.cpp index ee3a871cd6..e17155fc4d 100644 --- a/radiantcore/entity/EntityNode.cpp +++ b/radiantcore/entity/EntityNode.cpp @@ -22,7 +22,9 @@ EntityNode::EntityNode(const IEntityClassPtr& eclass) : _keyObservers(_spawnArgs), _shaderParms(_keyObservers, _colourKey), _direction(1,0,0) -{} +{ + createAttachedEntities(); +} EntityNode::EntityNode(const EntityNode& other) : IEntityNode(other), @@ -41,7 +43,9 @@ EntityNode::EntityNode(const EntityNode& other) : _keyObservers(_spawnArgs), _shaderParms(_keyObservers, _colourKey), _direction(1,0,0) -{} +{ + createAttachedEntities(); +} EntityNode::~EntityNode() { @@ -112,6 +116,25 @@ void EntityNode::destruct() TargetableNode::destruct(); } +void EntityNode::createAttachedEntities() +{ + _spawnArgs.forEachAttachment( + [this](const Entity::Attachment& a) + { + auto cls = GlobalEntityClassManager().findClass(a.eclass); + if (!cls) + { + rWarning() << "EntityNode [" << _eclass->getName() + << "]: cannot attach non-existent entity class '" + << a.eclass << "'\n"; + return; + } + + _attachedEnts.push_back(GlobalEntityModule().createEntity(cls)); + } + ); +} + void EntityNode::onEntityClassChanged() { // By default, we notify the KeyObservers attached to this entity @@ -256,9 +279,13 @@ scene::INode::Type EntityNode::getNodeType() const return Type::Entity; } -void EntityNode::renderSolid(RenderableCollector& collector, const VolumeTest& volume) const +void EntityNode::renderSolid(RenderableCollector& collector, + const VolumeTest& volume) const { - // Nothing here + // Render any attached entities + renderAttachments( + [&](const scene::INodePtr& n) { n->renderSolid(collector, volume); } + ); } void EntityNode::renderWireframe(RenderableCollector& collector, @@ -267,8 +294,14 @@ void EntityNode::renderWireframe(RenderableCollector& collector, // Submit renderable text name if required if (EntitySettings::InstancePtr()->getRenderEntityNames()) { - collector.addRenderable(*getWireShader(), _renderableName, localToWorld()); - } + collector.addRenderable(*getWireShader(), _renderableName, + localToWorld()); + } + + // Render any attached entities + renderAttachments( + [&](const scene::INodePtr& n) { n->renderWireframe(collector, volume); } + ); } void EntityNode::acquireShaders() @@ -298,6 +331,10 @@ void EntityNode::setRenderSystem(const RenderSystemPtr& renderSystem) // The colour key is maintaining a shader object as well _colourKey.setRenderSystem(renderSystem); + + // Make sure any attached entities have a render system too + for (IEntityNodePtr node: _attachedEnts) + node->setRenderSystem(renderSystem); } std::size_t EntityNode::getHighlightFlags() diff --git a/radiantcore/entity/EntityNode.h b/radiantcore/entity/EntityNode.h index 3701ec9665..eda54d010b 100644 --- a/radiantcore/entity/EntityNode.h +++ b/radiantcore/entity/EntityNode.h @@ -75,7 +75,20 @@ class EntityNode : sigc::connection _eclassChangedConn; -protected: + // List of attached sub-entities that we will submit for rendering (but are + // otherwise non-interactable). + // + // Although scene::Node already has the ability to store children, this is a + // separate list of entity nodes for two reasons: (1) there is a lot of + // other code which walks the scene graph for various reasons (e.g. map + // saving), and I don't want to have to audit the entire codebase to make + // sure that everything will play nicely with entities as children of other + // entities, and (2) storing entity node pointers instead of generic node + // pointers avoids some extra dynamic_casting. + using AttachedEntities = std::list; + AttachedEntities _attachedEnts; + + protected: // The Constructor needs the eclass EntityNode(const IEntityClassPtr& eclass); @@ -166,6 +179,36 @@ class EntityNode : void acquireShaders(); void acquireShaders(const RenderSystemPtr& renderSystem); -}; + + // Create entity nodes for all attached entities + void createAttachedEntities(); + + // Render all attached entities + template void renderAttachments(RenderFunc func) const + { + for (const IEntityNodePtr& ent: _attachedEnts) + { + // Attached entities might themselves have child nodes (e.g. func_static + // which has its model as a child node), so we must traverse() the + // attached entities, not just render them alone + struct ChildRenderer : public scene::NodeVisitor + { + RenderFunc _func; + ChildRenderer(RenderFunc f): _func(f) + {} + + bool pre(const scene::INodePtr& node) + { + _func(node); + return true; + } + }; + + ChildRenderer cr(func); + ent->traverse(cr); + } + } + + }; } // namespace entity diff --git a/radiantcore/rendersystem/backend/OpenGLShaderPass.cpp b/radiantcore/rendersystem/backend/OpenGLShaderPass.cpp index ce85733bd4..f7d5d92632 100644 --- a/radiantcore/rendersystem/backend/OpenGLShaderPass.cpp +++ b/radiantcore/rendersystem/backend/OpenGLShaderPass.cpp @@ -472,7 +472,7 @@ void OpenGLShaderPass::applyState(OpenGLState& current, } // end of changingBitsMask-dependent changes // Set depth function - if (requiredState & RENDER_DEPTHTEST + if (requiredState & RENDER_DEPTHTEST && _glState.getDepthFunc() != current.getDepthFunc()) { glDepthFunc(_glState.getDepthFunc()); @@ -649,11 +649,13 @@ void OpenGLShaderPass::setUpLightingCalculation(OpenGLState& current, const Matrix4& objTransform, std::size_t time) { + // Get the light shader and examine its first (and only valid) layer assert(light); + ShaderPtr shader = light->getShader(); + assert(shader); - // Get the light shader and examine its first (and only valid) layer - const MaterialPtr& lightShader = light->getShader()->getMaterial(); - ShaderLayer* layer = lightShader ? lightShader->firstLayer() : nullptr; + const MaterialPtr& lightMat = shader->getMaterial(); + ShaderLayer* layer = lightMat ? lightMat->firstLayer() : nullptr; if (!layer) return; // Calculate viewer location in object space @@ -665,7 +667,7 @@ void OpenGLShaderPass::setUpLightingCalculation(OpenGLState& current, // Get the XY and Z falloff texture numbers. GLuint attenuation_xy = layer->getTexture()->getGLTexNum(); - GLuint attenuation_z = lightShader->lightFalloffImage()->getGLTexNum(); + GLuint attenuation_z = lightMat->lightFalloffImage()->getGLTexNum(); // Bind the falloff textures assert(current.testRenderFlag(RENDER_TEXTURE_2D)); @@ -689,7 +691,7 @@ void OpenGLShaderPass::setUpLightingCalculation(OpenGLState& current, GLProgram::Params parms( light->getLightOrigin(), layer->getColour(), world2light ); - parms.ambientFactor = lightShader->isAmbientLight() ? 1.0f : 0.0f; + parms.ambientFactor = lightMat->isAmbientLight() ? 1.0f : 0.0f; parms.invertVertexColour = _glState.isColourInverted(); assert(current.glProgram); diff --git a/test/Entity.cpp b/test/Entity.cpp index 988691899c..b83f3c1fe5 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -319,14 +319,29 @@ namespace render::NopVolumeTest volumeTest; TestRenderableCollector collector; + // Whether to render solid or wireframe + const bool renderSolid; + // Keep track of nodes visited int nodesVisited = 0; + // Construct + RenderFixture(bool solid = false): renderSolid(solid) + {} + // NodeVisitor implementation bool pre(const scene::INodePtr& node) override { + // Count the node itself ++nodesVisited; - node->renderWireframe(collector, volumeTest); + + // Render the node in appropriate mode + if (renderSolid) + node->renderSolid(collector, volumeTest); + else + node->renderWireframe(collector, volumeTest); + + // Continue traversing return true; } }; @@ -450,20 +465,32 @@ TEST_F(EntityTest, RenderAttachedLightEntity) auto& spawnArgs = torch->getEntity(); EXPECT_EQ(spawnArgs.getKeyValue("model"), "models/torch.lwo"); - RenderFixture rf; + // We must render in solid mode to get the light source + RenderFixture rf(true); torch->setRenderSystem(rf.backend); // The entity node itself does not render the model; it is a parent node // with the model as a child (e.g. as a StaticModelNode). Therefore we must - // traverse our "mini-scenegraph" to render the model. - torch->traverseChildren(rf); - - // The node visitor should have visited one child node (a static model) and - // collected 3 renderables in total (because the torch model has several - // surfaces). - EXPECT_EQ(rf.nodesVisited, 1); - EXPECT_EQ(rf.collector.renderables, 3); - EXPECT_EQ(rf.collector.lights, 0); + // traverse our "mini-scenegraph" to render the model as well as the + // attached entities. + torch->traverse(rf); + + // The node visitor should have visited the entity itself and one child node (a + // static model) + EXPECT_EQ(rf.nodesVisited, 2); + + // There should be 3 renderables from the torch (because the entity has a + // shadowmesh and a collision mesh as well as the main model) and one from + // the light (the origin diamond). + EXPECT_EQ(rf.collector.renderables, 4); + + // The attached light should have been submitted as a light source + EXPECT_EQ(rf.collector.lights, 1); + + // The submitted light should be fully realised with a light shader + const RendererLight* rLight = rf.collector.lightPtrs.front(); + ASSERT_TRUE(rLight); + EXPECT_TRUE(rLight->getShader()); } } \ No newline at end of file diff --git a/test/resources/tdm/def/lights.def b/test/resources/tdm/def/lights.def index 6728d409f4..48cf88dfea 100644 --- a/test/resources/tdm/def/lights.def +++ b/test/resources/tdm/def/lights.def @@ -111,3 +111,25 @@ entitydef light_torchflame_small "light_radius" "240 240 240" //larger than other similar flames to be consistent with older maps } +entitydef light_cageflame_small // casts shadows with bars +{ + "inherit" "light_extinguishable" + + "mins" "-6 -6 -6" + "maxs" "6 6 24" + + "editor_displayFolder" "Lights/Light Sources/Torch Flames" + "editor_usage" "Torch-sized flame for cage-lights, casts faint bar shadows. Light pulses but is static. For moving light, add 'inherit' 'light_extinguishable_moving' to entity. " + + "model_lit" "torchflame_new01_small.prt" + "model_extinguished" "tdm_smoke_torchout.prt" + + "snd_lit" "fire_torch_small" + "snd_extinguished" "machine_steam01" + + "falloff" "0" + "texture" "lights/8pt_cageflicker" + + "_color" "0.9 0.6 0.40" + "light_radius" "230 230 250" +} \ No newline at end of file From b77588dd0e323d3fd7aa8a9f6091aec1634f187b Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sat, 6 Feb 2021 12:18:40 +0000 Subject: [PATCH 36/48] Add unit tests for func_static creation and rendering Confirm that func_static renders nothing without a "model" key, then the expected number of renderables once a model is set. --- include/irender.h | 4 ++-- test/Entity.cpp | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/include/irender.h b/include/irender.h index ff9262694b..3da395b6ba 100644 --- a/include/irender.h +++ b/include/irender.h @@ -219,8 +219,8 @@ inline std::ostream& operator<< (std::ostream& os, const RendererLight& l) } /** - * \brief - * Interface for an object which can test its intersection with a RendererLight. + * \brief Interface for an object which can test its intersection with a + * RendererLight. * * Objects which implement this interface define a intersectsLight() function * which determines whether the given light intersects the object. diff --git a/test/Entity.cpp b/test/Entity.cpp index b83f3c1fe5..2a1808c79a 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -329,6 +329,14 @@ namespace RenderFixture(bool solid = false): renderSolid(solid) {} + // Convenience method to set render backend and traverse a node and its + // children for rendering + void renderSubGraph(const scene::INodePtr& node) + { + node->setRenderSystem(backend); + node->traverse(*this); + } + // NodeVisitor implementation bool pre(const scene::INodePtr& node) override { @@ -434,6 +442,43 @@ TEST_F(EntityTest, RenderLightAsLightSource) "lights/biground_torchflicker"); } +TEST_F(EntityTest, RenderEmptyFuncStatic) +{ + auto funcStatic = createByClassName("func_static"); + + // Func static without a model key is empty + RenderFixture rf; + funcStatic->traverse(rf); + EXPECT_EQ(rf.collector.lights, 0); + EXPECT_EQ(rf.collector.renderables, 0); +} + +TEST_F(EntityTest, RenderFuncStaticWithModel) +{ + // Create a func_static with a model key + auto funcStatic = createByClassName("func_static"); + funcStatic->getEntity().setKeyValue("model", "models/moss_patch.ase"); + + // We should get one renderable + RenderFixture rf; + rf.renderSubGraph(funcStatic); + EXPECT_EQ(rf.collector.lights, 0); + EXPECT_EQ(rf.collector.renderables, 1); +} + +TEST_F(EntityTest, RenderFuncStaticWithMultiSurfaceModel) +{ + // Create a func_static with a model key + auto funcStatic = createByClassName("func_static"); + funcStatic->getEntity().setKeyValue("model", "models/torch.lwo"); + + // This torch model has 3 renderable surfaces + RenderFixture rf; + rf.renderSubGraph(funcStatic); + EXPECT_EQ(rf.collector.lights, 0); + EXPECT_EQ(rf.collector.renderables, 3); +} + TEST_F(EntityTest, CreateAttachedLightEntity) { // Create the torch entity which has an attached light From 3b486271f874609d4338e31bb6623663d0223acf Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sat, 6 Feb 2021 13:35:51 +0000 Subject: [PATCH 37/48] Add failing test for attached light position Test confirms observed behaviour in GUI, attached light is appearing at the origin rather than offset relative to the parent entity position. --- test/Entity.cpp | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/test/Entity.cpp b/test/Entity.cpp index 2a1808c79a..215b9a5e2b 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -448,7 +448,8 @@ TEST_F(EntityTest, RenderEmptyFuncStatic) // Func static without a model key is empty RenderFixture rf; - funcStatic->traverse(rf); + rf.renderSubGraph(funcStatic); + EXPECT_EQ(rf.nodesVisited, 1); EXPECT_EQ(rf.collector.lights, 0); EXPECT_EQ(rf.collector.renderables, 0); } @@ -459,9 +460,15 @@ TEST_F(EntityTest, RenderFuncStaticWithModel) auto funcStatic = createByClassName("func_static"); funcStatic->getEntity().setKeyValue("model", "models/moss_patch.ase"); - // We should get one renderable RenderFixture rf; rf.renderSubGraph(funcStatic); + + // The entity node itself does not render the model; it is a parent node + // with the model as a child (e.g. as a StaticModelNode). Therefore we + // should have visited two nodes in total: the entity and its model child. + EXPECT_EQ(rf.nodesVisited, 2); + + // Only one of the nodes should have submitted renderables EXPECT_EQ(rf.collector.lights, 0); EXPECT_EQ(rf.collector.renderables, 1); } @@ -511,14 +518,8 @@ TEST_F(EntityTest, RenderAttachedLightEntity) EXPECT_EQ(spawnArgs.getKeyValue("model"), "models/torch.lwo"); // We must render in solid mode to get the light source - RenderFixture rf(true); - torch->setRenderSystem(rf.backend); - - // The entity node itself does not render the model; it is a parent node - // with the model as a child (e.g. as a StaticModelNode). Therefore we must - // traverse our "mini-scenegraph" to render the model as well as the - // attached entities. - torch->traverse(rf); + RenderFixture rf(true /* solid mode */); + rf.renderSubGraph(torch); // The node visitor should have visited the entity itself and one child node (a // static model) @@ -538,4 +539,27 @@ TEST_F(EntityTest, RenderAttachedLightEntity) EXPECT_TRUE(rLight->getShader()); } +TEST_F(EntityTest, AttachedLightAtCorrectPosition) +{ + const Vector3 ORIGIN(256, -128, 635); + const Vector3 EXPECTED_OFFSET(0, 0, 10); // attach offset in def + + // Create a torch node and set a non-zero origin + auto torch = createByClassName("atdm:torch_brazier"); + torch->getEntity().setKeyValue("origin", string::to_string(ORIGIN)); + + // Render the torch + RenderFixture rf(true /* solid mode */); + rf.renderSubGraph(torch); + + // Access the submitted light source + ASSERT_FALSE(rf.collector.lightPtrs.empty()); + const RendererLight* rLight = rf.collector.lightPtrs.front(); + ASSERT_TRUE(rLight); + + // Check the light source's position + EXPECT_EQ(rLight->getLightOrigin(), ORIGIN + EXPECTED_OFFSET); + EXPECT_EQ(rLight->lightAABB().origin, ORIGIN + EXPECTED_OFFSET); +} + } \ No newline at end of file From affe686d12feb2712f53aa35aca20fa21685212b Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sun, 7 Feb 2021 14:29:18 +0000 Subject: [PATCH 38/48] Attach entities at correct position Attached light entities are now correctly parented to the main entity, and their attachment offset is encoded into the attached entity's localToParent() matrix. Some changes were required to Light which has previously always assumed that its "origin" key IS the light position - we now transform the local origin by localToWorld() which takes into account both the "origin" key and any transformation applied to the light entity or its parent. Attached lights are now appearing in the correct position and tests pass, but the attached light does not move when the parent entity is dragged. --- include/inode.h | 30 +++++++++------ radiantcore/entity/EntityNode.cpp | 20 ++++++++-- radiantcore/entity/light/Light.cpp | 15 ++++++-- test/Entity.cpp | 60 ++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 17 deletions(-) diff --git a/include/inode.h b/include/inode.h index 6b4fb23449..ef06dd0ad4 100644 --- a/include/inode.h +++ b/include/inode.h @@ -78,10 +78,12 @@ class NodeVisitor virtual void post(const INodePtr& node) {} }; -/** - * greebo: Abstract definition of a Node, a basic element - * of the scenegraph. All nodes share a certain set of - * functionality, like Layer functionality or being a Renderable. +/** + * \brief Main interface for a Node, a basic element of the scenegraph. + * + * All nodes share a certain set of functionality, such as being placed in + * layers, being able to render themselves, and being able to hold and + * transform a list of child nodes. */ class INode : public Layered, @@ -116,7 +118,7 @@ class INode : * Set the scenegraph this node is belonging to. This is usually * set by the scenegraph itself during insertion. */ - virtual void setSceneGraph(const GraphPtr& sceneGraph) = 0; + virtual void setSceneGraph(const GraphPtr& sceneGraph) = 0; /** greebo: Returns true, if the node is the root element * of the scenegraph. @@ -166,7 +168,7 @@ class INode : virtual bool hasChildNodes() const = 0; /** - * greebo: Traverses this node and all child nodes (recursively) + * greebo: Traverses this node and all child nodes (recursively) * using the given visitor. * * Note: replaces the legacy Node_traverseSubgraph() method. @@ -187,11 +189,11 @@ class INode : /** * Call the given functor for each child node, depth first - * This is a simpler alternative to the usual traverse() method + * This is a simpler alternative to the usual traverse() method * which provides pre() and post() methods and more control about - * which nodes to traverse and. This forEachNode() routine simply + * which nodes to traverse and. This forEachNode() routine simply * hits every child node including their children. - * + * * @returns: true if the functor returned false on any of the * visited nodes. The return type is used to pass the stop signal * up the stack during traversal. @@ -236,7 +238,13 @@ class INode : // Returns the bounds in world coordinates virtual const AABB& worldAABB() const = 0; - // Returns the transformation from local to world coordinates + /** + * \brief Return the transformation from local to world coordinates + * + * This represents the final transformation from this node's own coordinate + * space into world space, including any transformations inherited from + * parent nodes. + */ virtual const Matrix4& localToWorld() const = 0; // Undo/Redo events - some nodes need to do extra legwork after undo or redo @@ -244,7 +252,7 @@ class INode : // not by the UndoSystem itself, at least not yet. virtual void onPostUndo() {} virtual void onPostRedo() {} - + // Called during recursive transform changed, but only by INodes themselves virtual void transformChangedLocal() = 0; }; diff --git a/radiantcore/entity/EntityNode.cpp b/radiantcore/entity/EntityNode.cpp index e17155fc4d..5efc70ca40 100644 --- a/radiantcore/entity/EntityNode.cpp +++ b/radiantcore/entity/EntityNode.cpp @@ -23,7 +23,6 @@ EntityNode::EntityNode(const IEntityClassPtr& eclass) : _shaderParms(_keyObservers, _colourKey), _direction(1,0,0) { - createAttachedEntities(); } EntityNode::EntityNode(const EntityNode& other) : @@ -44,7 +43,6 @@ EntityNode::EntityNode(const EntityNode& other) : _shaderParms(_keyObservers, _colourKey), _direction(1,0,0) { - createAttachedEntities(); } EntityNode::~EntityNode() @@ -72,6 +70,9 @@ void EntityNode::construct() addKeyObserver("skin", _skinKeyObserver); _shaderParms.addKeyObservers(); + + // Construct all attached entities + createAttachedEntities(); } void EntityNode::constructClone(const EntityNode& original) @@ -121,6 +122,7 @@ void EntityNode::createAttachedEntities() _spawnArgs.forEachAttachment( [this](const Entity::Attachment& a) { + // Check this is a valid entity class auto cls = GlobalEntityClassManager().findClass(a.eclass); if (!cls) { @@ -130,7 +132,19 @@ void EntityNode::createAttachedEntities() return; } - _attachedEnts.push_back(GlobalEntityModule().createEntity(cls)); + // Construct and store the attached entity + auto attachedEnt = GlobalEntityModule().createEntity(cls); + assert(attachedEnt); + _attachedEnts.push_back(attachedEnt); + + // Set ourselves as the parent of the attached entity (for + // localToParent transforms) + attachedEnt->setParent(shared_from_this()); + + // Set the attached entity's transform matrix according to the + // required offset + MatrixTransform& mt = dynamic_cast(*attachedEnt); + mt.localToParent() = Matrix4::getTranslation(a.offset); } ); } diff --git a/radiantcore/entity/light/Light.cpp b/radiantcore/entity/light/Light.cpp index 3bf8e62f0b..67ff5cb130 100644 --- a/radiantcore/entity/light/Light.cpp +++ b/radiantcore/entity/light/Light.cpp @@ -616,7 +616,11 @@ AABB Light::lightAABB() const } else { - return AABB(_originTransformed, m_doom3Radius.m_radiusTransformed); + // AABB ignores light_center so we can't call getLightOrigin() here. + // Just transform (0, 0, 0) by localToWorld to get the world origin for + // the AABB. + return AABB(_owner.localToWorld().transformPoint(Vector3(0, 0, 0)), + m_doom3Radius.m_radiusTransformed); } } @@ -637,8 +641,13 @@ Vector3 Light::getLightOrigin() const } else { - // AABB origin + light_center, i.e. center in world space - return _originTransformed + m_doom3Radius.m_centerTransformed; + // Since localToWorld() takes into account our own origin as well as the + // transformation of any parent entity, just transform a null origin + + // light_center by the localToWorld matrix to get the light origin in + // world space. + return _owner.localToWorld().transformPoint( + /* (0, 0, 0) + */ m_doom3Radius.m_centerTransformed + ); } } diff --git a/test/Entity.cpp b/test/Entity.cpp index 215b9a5e2b..f302c378d4 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -9,6 +9,7 @@ #include "render/NopVolumeTest.h" #include "string/convert.h" +#include "transformlib.h" namespace test { @@ -380,6 +381,65 @@ TEST_F(EntityTest, ModifyEntityClass) EXPECT_NE(light->getWireShader(), origWireShader); } +TEST_F(EntityTest, LightLocalToWorldFromOrigin) +{ + auto light = createByClassName("light"); + + // Initial localToWorld should be identity + EXPECT_EQ(light->localToWorld(), Matrix4::getIdentity()); + + // Set an origin + const Vector3 ORIGIN(123, 456, -10); + light->getEntity().setKeyValue("origin", string::to_string(ORIGIN)); + + // localToParent should reflect the new origin + auto transformNode = std::dynamic_pointer_cast(light); + ASSERT_TRUE(transformNode); + EXPECT_EQ(transformNode->localToParent(), Matrix4::getTranslation(ORIGIN)); + + // Since there is no parent, the final localToWorld should be the same as + // localToParent + EXPECT_EQ(light->localToWorld(), Matrix4::getTranslation(ORIGIN)); +} + +TEST_F(EntityTest, LightTransformedByParent) +{ + // Parent a light to another entity (this isn't currently how the attachment + // system is implemented, but it should validate that a light node can + // inherit the transformation of its parent). + auto light = createByClassName("light"); + auto parentModel = createByClassName("func_static"); + parentModel->addChildNode(light); + + // Parenting should automatically set the parent pointer of the child + EXPECT_EQ(light->getParent(), parentModel); + + // Set an offset for the parent model + const Vector3 ORIGIN(1024, 512, -320); + parentModel->getEntity().setKeyValue("origin", string::to_string(ORIGIN)); + + // Parent entity should have a transform matrix corresponding to its + // translation + EXPECT_EQ(parentModel->localToWorld(), Matrix4::getTranslation(ORIGIN)); + + // The light itself should have the same transformation as the parent (since + // the method is localToWorld not localToParent). + EXPECT_EQ(light->localToWorld(), Matrix4::getTranslation(ORIGIN)); + + // Render the light to obtain the RendererLight pointer + RenderFixture renderF(true /* solid */); + renderF.renderSubGraph(parentModel); + EXPECT_EQ(renderF.nodesVisited, 2); + EXPECT_EQ(renderF.collector.lights, 1); + ASSERT_FALSE(renderF.collector.lightPtrs.empty()); + + // Check the rendered light's geometry + const RendererLight* rLight = renderF.collector.lightPtrs.front(); + EXPECT_EQ(rLight->getLightOrigin(), ORIGIN); + EXPECT_EQ(rLight->lightAABB().origin, ORIGIN); + EXPECT_EQ(rLight->lightAABB().extents, Vector3(320, 320, 320)); +} + TEST_F(EntityTest, RenderUnselectedLightEntity) { auto light = createByClassName("light"); From 42cb0d2c5478b4b713a2cabb59a067887c20d339 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Sun, 7 Feb 2021 20:53:33 +0000 Subject: [PATCH 39/48] Move MatrixTransform up to IEntityNode interface Avoid a new dynamic_cast in EntityNode's attachment handling by migrating the MatrixTransform interface up to IEntityNode, so that localToParent() is accessible as soon as the IEntityNodePtr has been created by the entity module. --- include/ientity.h | 17 ++++++++++++----- include/itransformnode.h | 13 +++++++++++-- libs/transformlib.h | 22 ---------------------- radiantcore/entity/EntityNode.cpp | 5 ++--- radiantcore/entity/EntityNode.h | 10 ++++++++-- 5 files changed, 33 insertions(+), 34 deletions(-) diff --git a/include/ientity.h b/include/ientity.h index 137e9e593c..a37e55159c 100644 --- a/include/ientity.h +++ b/include/ientity.h @@ -6,6 +6,7 @@ #include "irender.h" #include "inameobserver.h" #include "iscenegraph.h" +#include "itransformnode.h" #include #include "string/predicate.h" @@ -263,11 +264,17 @@ class Entity virtual void forEachAttachment(AttachmentFunc func) const = 0; }; -/// Interface for a INode subclass that contains an Entity -class IEntityNode : - public IRenderEntity, - public virtual scene::INode, - public scene::Cloneable +/** + * \brief Interface for a node which represents an entity. + * + * As well as providing access to the entity data with getEntity(), every + * IEntityNode can clone itself and apply a transformation matrix to its + * children (which might be brushes, patches or other entities). + */ +class IEntityNode : public IRenderEntity, + public virtual scene::INode, + public scene::Cloneable, + public IMatrixTransform { public: virtual ~IEntityNode() {} diff --git a/include/itransformnode.h b/include/itransformnode.h index 8490a96110..9b3a26b3a8 100644 --- a/include/itransformnode.h +++ b/include/itransformnode.h @@ -4,14 +4,14 @@ class Matrix4; -/// \brief A transform node. +/// A node which can transform the coordinate space of its children class ITransformNode { public: virtual ~ITransformNode() {} /// \brief Returns the transform which maps the node's local-space into the local-space of its parent node. - virtual const Matrix4& localToParent() const = 0; + virtual const Matrix4& localToParent() const = 0; }; typedef std::shared_ptr ITransformNodePtr; @@ -19,3 +19,12 @@ inline ITransformNodePtr Node_getTransformNode(const scene::INodePtr& node) { return std::dynamic_pointer_cast(node); } + +/// An ITransformNode which can provide non-const access to its transform matrix +class IMatrixTransform: public ITransformNode +{ +public: + + /// Return a modifiable reference to a contained transformation matrix + virtual Matrix4& localToParent() = 0; +}; \ No newline at end of file diff --git a/libs/transformlib.h b/libs/transformlib.h index 5b18204811..829dc8fbb2 100644 --- a/libs/transformlib.h +++ b/libs/transformlib.h @@ -17,28 +17,6 @@ class IdentityTransform : } }; -/// \brief A transform node which stores a generic transformation matrix. -class MatrixTransform : - public ITransformNode -{ - Matrix4 _localToParent; -public: - MatrixTransform() : - _localToParent(Matrix4::getIdentity()) - {} - - Matrix4& localToParent() - { - return _localToParent; - } - - /// \brief Returns the stored local->parent transform. - const Matrix4& localToParent() const - { - return _localToParent; - } -}; - namespace scene { diff --git a/radiantcore/entity/EntityNode.cpp b/radiantcore/entity/EntityNode.cpp index 5efc70ca40..5f44b050a2 100644 --- a/radiantcore/entity/EntityNode.cpp +++ b/radiantcore/entity/EntityNode.cpp @@ -32,9 +32,9 @@ EntityNode::EntityNode(const EntityNode& other) : Namespaced(other), TargetableNode(_spawnArgs, *this), Transformable(other), - MatrixTransform(other), _eclass(other._eclass), _spawnArgs(other._spawnArgs), + _localToParent(other._localToParent), _namespaceManager(_spawnArgs), _nameKey(_spawnArgs), _renderableName(_nameKey), @@ -143,8 +143,7 @@ void EntityNode::createAttachedEntities() // Set the attached entity's transform matrix according to the // required offset - MatrixTransform& mt = dynamic_cast(*attachedEnt); - mt.localToParent() = Matrix4::getTranslation(a.offset); + attachedEnt->localToParent() = Matrix4::getTranslation(a.offset); } ); } diff --git a/radiantcore/entity/EntityNode.h b/radiantcore/entity/EntityNode.h index eda54d010b..d2f846b64e 100644 --- a/radiantcore/entity/EntityNode.h +++ b/radiantcore/entity/EntityNode.h @@ -32,8 +32,7 @@ class EntityNode : public SelectionTestable, public Namespaced, public TargetableNode, - public Transformable, - public MatrixTransform // influences local2world of child nodes + public Transformable { protected: // The entity class @@ -42,6 +41,9 @@ class EntityNode : // The actual entity (which contains the key/value pairs) SpawnArgs _spawnArgs; + // Transformation applied to this node and its children + Matrix4 _localToParent = Matrix4::getIdentity(); + // The class taking care of all the namespace-relevant stuff NamespaceManager _namespaceManager; @@ -106,6 +108,10 @@ class EntityNode : virtual float getShaderParm(int parmNum) const override; virtual const Vector3& getDirection() const override; + // IMatrixTransform implementation + const Matrix4& localToParent() const override { return _localToParent; } + Matrix4& localToParent() override { return _localToParent; } + // SelectionTestable implementation virtual void testSelect(Selector& selector, SelectionTest& test) override; From fc6119c6b349162137bc5c50daf6d6128c488711 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Mon, 8 Feb 2021 20:08:38 +0000 Subject: [PATCH 40/48] Add failing test for moving attached light This test provokes similar behaviour to that seen in the application, whereby the attached light appears at the correct position initially but does not move when the main entity is dragged. --- test/Entity.cpp | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/Entity.cpp b/test/Entity.cpp index f302c378d4..61b1ebc8f4 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -622,4 +622,37 @@ TEST_F(EntityTest, AttachedLightAtCorrectPosition) EXPECT_EQ(rLight->lightAABB().origin, ORIGIN + EXPECTED_OFFSET); } +TEST_F(EntityTest, AttachedLightMovesWithEntity) +{ + const Vector3 ORIGIN(12, -0.5, 512); + const Vector3 EXPECTED_OFFSET(0, 0, 10); // attach offset in def + + // Create a torch node and set a non-zero origin + auto torch = createByClassName("atdm:torch_brazier"); + torch->getEntity().setKeyValue("origin", string::to_string(ORIGIN)); + + // First render + { + RenderFixture rf(true /* solid mode */); + rf.renderSubGraph(torch); + } + + // Move the torch + const Vector3 NEW_ORIGIN = ORIGIN + Vector3(128, 512, -54); + torch->getEntity().setKeyValue("origin", string::to_string(NEW_ORIGIN)); + + // Render again to get positions + RenderFixture rf(true /* solid mode */); + rf.renderSubGraph(torch); + + // Access the submitted light source + ASSERT_FALSE(rf.collector.lightPtrs.empty()); + const RendererLight* rLight = rf.collector.lightPtrs.front(); + ASSERT_TRUE(rLight); + + // Check the light source's position + EXPECT_EQ(rLight->getLightOrigin(), NEW_ORIGIN + EXPECTED_OFFSET); + EXPECT_EQ(rLight->lightAABB().origin, NEW_ORIGIN + EXPECTED_OFFSET); +} + } \ No newline at end of file From 070757c3d97427fe2b4d9510874d428f002578c6 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Tue, 9 Feb 2021 19:21:30 +0000 Subject: [PATCH 41/48] Add a test for a func_static's localToWorld() Confirm that the localToWorld matrix reflects changes in the "origin" key. --- test/Entity.cpp | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/Entity.cpp b/test/Entity.cpp index 61b1ebc8f4..ad5384b67e 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -402,6 +402,27 @@ TEST_F(EntityTest, LightLocalToWorldFromOrigin) EXPECT_EQ(light->localToWorld(), Matrix4::getTranslation(ORIGIN)); } +TEST_F(EntityTest, FuncStaticLocalToWorld) +{ + auto funcStatic = createByClassName("func_static"); + auto& spawnArgs = funcStatic->getEntity(); + spawnArgs.setKeyValue("origin", "0 0 0"); + + // Initial localToWorld should be an identity matrix + EXPECT_EQ(funcStatic->localToWorld(), Matrix4::getIdentity()); + + // Set a new origin and make sure the localToWorld reflects the + // corresponding translation + const Vector3 MOVED(46, -128, 4096); + spawnArgs.setKeyValue("origin", string::to_string(MOVED)); + EXPECT_EQ(funcStatic->localToWorld(), + Matrix4::getTranslation(MOVED)); + + // Clear transformation and get back to identity + spawnArgs.setKeyValue("origin", "0 0 0"); + EXPECT_EQ(funcStatic->localToWorld(), Matrix4::getIdentity()); +} + TEST_F(EntityTest, LightTransformedByParent) { // Parent a light to another entity (this isn't currently how the attachment From 42b1d0b5d6184f60191abd159474769744d2eb17 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Tue, 9 Feb 2021 19:54:58 +0000 Subject: [PATCH 42/48] Broadcast transformChanged() to attached entities Attached light now moves with the parent entity, which did not happen before because the attached entity's transformChanged() method was never called to invalidate its localToWorld when the parent entity moved. --- radiantcore/entity/EntityNode.cpp | 10 ++++++++++ radiantcore/entity/EntityNode.h | 1 + test/Entity.cpp | 4 ---- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/radiantcore/entity/EntityNode.cpp b/radiantcore/entity/EntityNode.cpp index 5f44b050a2..281a67aa44 100644 --- a/radiantcore/entity/EntityNode.cpp +++ b/radiantcore/entity/EntityNode.cpp @@ -148,6 +148,16 @@ void EntityNode::createAttachedEntities() ); } +void EntityNode::transformChanged() +{ + Node::transformChanged(); + + // Broadcast transformChanged to all attached entities so they can update + // their position + for (auto attached: _attachedEnts) + attached->transformChanged(); +} + void EntityNode::onEntityClassChanged() { // By default, we notify the KeyObservers attached to this entity diff --git a/radiantcore/entity/EntityNode.h b/radiantcore/entity/EntityNode.h index d2f846b64e..a4622790db 100644 --- a/radiantcore/entity/EntityNode.h +++ b/radiantcore/entity/EntityNode.h @@ -103,6 +103,7 @@ class EntityNode : // IEntityNode implementation Entity& getEntity() override; virtual void refreshModel() override; + void transformChanged() override; // RenderEntity implementation virtual float getShaderParm(int parmNum) const override; diff --git a/test/Entity.cpp b/test/Entity.cpp index ad5384b67e..44c1d05885 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -602,10 +602,6 @@ TEST_F(EntityTest, RenderAttachedLightEntity) RenderFixture rf(true /* solid mode */); rf.renderSubGraph(torch); - // The node visitor should have visited the entity itself and one child node (a - // static model) - EXPECT_EQ(rf.nodesVisited, 2); - // There should be 3 renderables from the torch (because the entity has a // shadowmesh and a collision mesh as well as the main model) and one from // the light (the origin diamond). From aab26c8846e8af0bdcf7c43be1d42dca93d182fc Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Tue, 9 Feb 2021 20:25:39 +0000 Subject: [PATCH 43/48] Tweaks to new light/speaker radius icons Try to improve visibility and sharpness by avoiding subtle curves at the corners, although it's still challenging to make an image readable with only 16 pixels to work with. --- install/bitmaps/view_show_lightradii.png | Bin 571 -> 490 bytes install/bitmaps/view_show_speakerradii.png | Bin 462 -> 310 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/install/bitmaps/view_show_lightradii.png b/install/bitmaps/view_show_lightradii.png index 447f039a8f9aa250272fe07eacfa9a4272a2895c..16560f7578e654c002bf8f187b0380ae23e38f69 100644 GIT binary patch delta 406 zcmV;H0crlb1nL8jUVnEcM%riP=K0Nkp63}+3&l6dA^gFD zXW|hB;|LZoSP5{7K~8Lr!nH=YyDl&X?4eAeq)<}86dwD!z<+F5sS%`Uh7p8S1a$_Q z4~sKx&&BE1CubmefB?dp&NEz7fiE=!#GL-*vD;~m1MP@(xSfd!r!RR7#4xa;0Wb#O zrV~BkS>5hLuis>>9^hL^qDIsMKz_bd7_b9i-j=Syr_GzvrSI-W1C(XJGYh35Jbetr z@6E#dDBvR4k$(VL;SAWu(?;Xv^Wr-mdnp(2XaKfkne%!hfUph_zx!yZ0P$ AVE_OC delta 487 zcmVuP0^jA)rv@56tb~yLgyOi9v7LECbrSLy1eIo zAI>?KE4qNgHuDHoI!s9e!-%6b0*ZV>XcBO@KuUNklNI^Gaq1JU`WHJmIa^96P;FD{rmHK4Fe;CBrY{Ly)e;nZ5qRW#{CQo zxHV&S!9>S3&;a_6E=8IPhy|ktU@;(ne+Xm#{t(9h4FBU97+M&9knMtk{h^B(82;HX zFfcqK%K(UL!p<`O`Den&z;J+|0mwNF8RQ)fd;L!2pF0Bs!z^rS@#X~>XjmQgi-Cb5 b9h)2g_;EWm4X$y}00000NkvXXu0mjfuFGIR delta 378 zcmV-=0fqjy0?q@FU4H>vNklzEH{}Q9Mj)wkYLnQj8s$KPPK92epH7ad425vgvzWM)~)Vv*+~weV*s-6`fHd zcU_ng)#zhgeM>OmdBO(SSkYPgaDM@s>U8@S97k9AX+dV~mVW?J-P?`jij@EcK%0TN z<5EEkkSKsw9h(hWBS}=uYRLld zZ?tD}LwNwV<2%doKsur-D<}e|w`L_f`b3FCKT{{(h;_l1c=Q(oh^KOGWb z6{P7;4kmMvy>psM*$b@wL~sp|8$9dz*c?0UzneHTM??q%H?Q@h;e5CnUKuKq0zhOp z8!I}$QW-xmGxBV;3Z%2KqEnTfYAfmRyV~ZDO9fRSYG-x)r1uu+%X*7WluHa!oi5+v Y3o5jAtW;A7O#lD@07*qoM6N<$f)019v;Y7A From b6b8c2f2d2c76cac5f29899e224af0c3717ca0a0 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Tue, 9 Feb 2021 20:32:08 +0000 Subject: [PATCH 44/48] Remove obsolete Node::setTransformChangedCallback() This callback mechanism seems to have only been used by the now-removed light list system. --- libs/scene/Node.cpp | 9 --------- libs/scene/Node.h | 3 --- 2 files changed, 12 deletions(-) diff --git a/libs/scene/Node.cpp b/libs/scene/Node.cpp index 7474bd3110..09e2919faa 100644 --- a/libs/scene/Node.cpp +++ b/libs/scene/Node.cpp @@ -440,11 +440,6 @@ void Node::transformChangedLocal() _transformMutex = false; _boundsChanged = true; _childBoundsChanged = true; - - if (_transformChangedCallback) - { - _transformChangedCallback(); - } } void Node::transformChanged() @@ -462,10 +457,6 @@ void Node::transformChanged() boundsChanged(); } -void Node::setTransformChangedCallback(const Callback& callback) { - _transformChangedCallback = callback; -} - RenderSystemPtr Node::getRenderSystem() const { return _renderSystem.lock(); diff --git a/libs/scene/Node.h b/libs/scene/Node.h index 4493e39099..e9e74fc51f 100644 --- a/libs/scene/Node.h +++ b/libs/scene/Node.h @@ -50,7 +50,6 @@ class Node : mutable bool _childBoundsMutex; mutable bool _transformChanged; mutable bool _transformMutex; - Callback _transformChangedCallback; mutable Matrix4 _local2world; @@ -152,8 +151,6 @@ class Node : void transformChanged() override; - void setTransformChangedCallback(const Callback& callback); - // greebo: This gets called as soon as a scene::Node gets inserted into // the TraversableNodeSet. This triggers an instantiation call on the child node. virtual void onChildAdded(const INodePtr& child); From 0ce5666d1cca52e785c76b8fbcc174b6436db527 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Wed, 10 Feb 2021 20:33:14 +0000 Subject: [PATCH 45/48] #5534: EntityClassChooser is no longer a static singleton Since populating the tree for this dialog is very quick, there is no benefit in keeping a singleton around. Creating a new dialog each time requires much less code and also fixes the major issue described in #5534, namely that the dialog is always empty on its second showing. There is still a problem with the search feature (at least on Linux), which results in the tree view becoming empty as soon as any text is entered in the search box. However it is at least now possible to resume normal service by closing the dialog and showing another one. --- libs/wxutil/EntityClassChooser.cpp | 48 +++++------------------------- libs/wxutil/EntityClassChooser.h | 24 +++++++-------- 2 files changed, 17 insertions(+), 55 deletions(-) diff --git a/libs/wxutil/EntityClassChooser.cpp b/libs/wxutil/EntityClassChooser.cpp index c885ba231a..c4973882ad 100644 --- a/libs/wxutil/EntityClassChooser.cpp +++ b/libs/wxutil/EntityClassChooser.cpp @@ -89,7 +89,7 @@ class EntityClassTreePopulator: // of the DISPLAY_FOLDER_KEY. addPath( eclass->getModName() + folderPath + "/" + eclass->getName(), - [&](TreeModel::Row& row, const std::string& path, + [&](TreeModel::Row& row, const std::string& path, const std::string& leafName, bool isFolder) { bool isFavourite = !isFolder && _favourites.count(leafName) > 0; @@ -144,7 +144,7 @@ class ThreadedEntityClassLoader final : }; // Main constructor -EntityClassChooser::EntityClassChooser() : +EntityClassChooser::EntityClassChooser() : DialogBase(_(ECLASS_CHOOSER_TITLE)), _treeView(nullptr), _selectedName("") @@ -203,14 +203,16 @@ EntityClassChooser::EntityClassChooser() : // Display the singleton instance std::string EntityClassChooser::chooseEntityClass(const std::string& preselectEclass) { + EntityClassChooser instance; + if (!preselectEclass.empty()) { - Instance().setSelectedEntityClass(preselectEclass); + instance.setSelectedEntityClass(preselectEclass); } - if (Instance().ShowModal() == wxID_OK) + if (instance.ShowModal() == wxID_OK) { - return Instance().getSelectedEntityClass(); + return instance.getSelectedEntityClass(); } else { @@ -218,42 +220,6 @@ std::string EntityClassChooser::chooseEntityClass(const std::string& preselectEc } } -EntityClassChooser& EntityClassChooser::Instance() -{ - EntityClassChooserPtr& instancePtr = InstancePtr(); - - if (!instancePtr) - { - // Not yet instantiated, do it now - instancePtr.reset(new EntityClassChooser); - - // Pre-destruction cleanup - GlobalMainFrame().signal_MainFrameShuttingDown().connect( - sigc::mem_fun(*instancePtr, &EntityClassChooser::onMainFrameShuttingDown) - ); - } - - return *instancePtr; -} - -EntityClassChooserPtr& EntityClassChooser::InstancePtr() -{ - static EntityClassChooserPtr _instancePtr; - return _instancePtr; -} - -void EntityClassChooser::onMainFrameShuttingDown() -{ - rMessage() << "EntityClassChooser shutting down." << std::endl; - - _modelPreview.reset(); - _defsReloaded.disconnect(); - - // Final step at shutdown, release the shared ptr - Instance().SendDestroyEvent(); - InstancePtr().reset(); -} - void EntityClassChooser::loadEntityClasses() { _treeView->Populate(std::make_shared(_columns)); diff --git a/libs/wxutil/EntityClassChooser.h b/libs/wxutil/EntityClassChooser.h index 78303adfaf..9d256bb524 100644 --- a/libs/wxutil/EntityClassChooser.h +++ b/libs/wxutil/EntityClassChooser.h @@ -61,15 +61,6 @@ class EntityClassChooser : void onOK(wxCommandEvent& ev); void onSelectionChanged(wxDataViewEvent& ev); void onDeleteEvent(wxCloseEvent& ev); - - void onMainFrameShuttingDown(); - - // This is where the static shared_ptr of the singleton instance is held. - static EntityClassChooserPtr& InstancePtr(); - -public: - // Public accessor to the singleton instance - static EntityClassChooser& Instance(); // Sets the tree selection to the given entity class void setSelectedEntityClass(const std::string& eclass); @@ -77,15 +68,20 @@ class EntityClassChooser : // Sets the tree selection to the given entity class const std::string& getSelectedEntityClass() const; + // Overridden from wxDialog int ShowModal() override; +public: + /** - * Convenience function: - * Display the dialog and block awaiting the selection of an entity class, - * which is returned to the caller. If the dialog is cancelled or no - * selection is made, and empty string will be returned. + * \brief Construct and show the dialog to choose an entity class, + * returning the result. + * + * \param preselectEclass + * Optional initial class to locate and highlight in the tree after the + * dialog is shown. */ - static std::string chooseEntityClass(const std::string& preselectEclass = std::string()); + static std::string chooseEntityClass(const std::string& preselectEclass = {}); }; } // namespace From aaf51345d4d1910ca13f5928d7ffcf2a6edddda9 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Wed, 10 Feb 2021 21:03:17 +0000 Subject: [PATCH 46/48] Add test for builder guard entity Test constructing a more complex entity with an attachment (hammer attached to a joint on the back; currently shows on floor in front of feet due to lack of parsing of joint information). --- test/Entity.cpp | 10 + test/resources/tdm/def/tdm_ai.def | 1295 +++++++++++++++++++++++++++++ 2 files changed, 1305 insertions(+) create mode 100644 test/resources/tdm/def/tdm_ai.def diff --git a/test/Entity.cpp b/test/Entity.cpp index 44c1d05885..264b0a5ebc 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -672,4 +672,14 @@ TEST_F(EntityTest, AttachedLightMovesWithEntity) EXPECT_EQ(rLight->lightAABB().origin, NEW_ORIGIN + EXPECTED_OFFSET); } +TEST_F(EntityTest, CreateAIEntity) +{ + auto guard = createByClassName("atdm:ai_builder_guard"); + ASSERT_TRUE(guard); + + // Guard should have a hammer attachment + auto attachments = getAttachments(guard); + EXPECT_EQ(attachments.size(), 1); +} + } \ No newline at end of file diff --git a/test/resources/tdm/def/tdm_ai.def b/test/resources/tdm/def/tdm_ai.def new file mode 100644 index 0000000000..608191efcb --- /dev/null +++ b/test/resources/tdm/def/tdm_ai.def @@ -0,0 +1,1295 @@ +entityDef atdm:ai_base +{ + "editor_displayFolder" "AI/Internal/Base Class" + "scriptobject" "ai_darkmod_base" + + "editor_color" "1 .5 0" + "editor_mins" "-24 -24 0" + "editor_maxs" "24 24 64" + "editor_showangle" "1" + "editor_copy" "anim" + + "editor_usage" "Don't use. Base class for all TDM AI entities." + + "editor_float melee_damage_mod" "When the AI hits with a melee weapon, this multiplies the default damage done by the weapon." + "editor_bool melee_predicts_proximity" "If true, AI takes the current velocity of itself and its enemy into account to predict whether it will hit the enemy in the future if it starts a melee attack now. All but the stupidest AI should do this." + "editor_float melee_predicted_attack_time" "The amount of time in milliseconds that the AI uses to predict whether it will hit its enemy if it starts its attack now. Should roughly correspond to the time it takes before the attack animation is threatening to an enemy in front of them. This baseline can be set on an AI for empty-hand attacks, then may be overwritten by individual weapons if their attack animations are slower/faster than this baseline." + + "editor_bool can_be_flatfooted" "If set to true, the AI becomes flat-footed in certain combat situations. This means they're unable to run at the player, but can still turn to face the player." + "editor_float flatfooted_time" "The duration in milliseconds that this AI remains flat footed after a combat action triggers this state." + "editor_float flatfoot_parry_num" "When an AI parries, only the first few parries will make them flat footed. This spawnarg defines how many parries will work before a timer kicks in." + "editor_float flatfoot_parry_time" "Determines how long it takes before the AI can be rendered flat-footed by a parry again. This is measured from the last time they succesfully parry, so the player must 'surprise' them again by waiting until they haven't parried in a while, and can't just spam attack." + + "editor_float fov" "AIs full field of view in degrees. Will be used for both horizontal and vertical fov unless vertical is explicitly specified." + "editor_float fov_vert" "AIs full vertical field of view in degrees. Leave blank or set to -1 to use the same value as the horizontal fov." + "editor_vector fov_rotation" "A vector of 3 space-delimited angles applied to rotate the AI's field of view relative to their head joint. For most heads, the angles are: yaw pitch roll." + "editor_float acuity_vis" "AI visual acuity (in percent)" + "editor_float acuity_aud" "AI aural acuity (in percent)" + "editor_float acuity_tact" "AI tactile acuity (in percent)" + "editor_float acuity_env" "AI environmental acuity. How sensitive the AI is to things that have changed in its environment. (In percent)" + "editor_float acuity_other" "AI generic acuity for scripted events. You can call this alert type on an entity with entity.Alert( , ). This will alert the entity by amount * acuity_other in your script." + + "editor_bool patrol" "If set to 1, the AI will patrol along pathnodes from the start of the map." + "editor_bool animal_patrol" "Set this to 1 to let the AI use the animal patrol task instead of the ordinary path_corner routine. It will wander aimlessly around instead of following a predefined path." + + "editor_float startpos_tolerance" "When returning to their idle position after an alert, the AI uses their bounding box by default to check whether they have reached their destination. Setting this spawnarg to values >= 0 will change the size of the bounding box check." + + "editor_bool sitting" "If true, AI will be sitting at map start." + "editor_var sitting_turn_pivot" "The offset of the pivot for turning while seated." + "editor_float sit_down_slide_dist" "Lets the AI slide back onto the seat." + "editor_float sit_down_angle" "If this is set, AI will turn to face this direction after sitting down." + + "editor_bool drunk" "If set to 1, AI will be drunk" + "editor_float drunk_acuity_factor" "If the AI is drunk, their acuities (visual, hearing, tactile) will be reduced by this factor." + + "editor_float stepvol_walk" "Modifies audible AI walking footstep volume" + "editor_float stepvol_run" "Modifies audible AI running footstep volume" + + "editor_float alert_thresh1" "The alert level threshold for reaching ObservantState (bark, but otherwise no reaction)." + "editor_float alert_time1" "The time it takes for the alert level to ramp down from the upper to the lower threshold in ObservantState (in seconds)." + "editor_float alert_time1_fuzzyness" "Applies some randomness to the alert time (in seconds)." + "editor_float alert_gracetime1" "After being alerted, the AI will ignore additional alerts during the grace period (in seconds), to avoid adding up the alert level too quickly." + "editor_float alert_gracefrac1" "An alert that is higher than the last increase times the grace fraction will terminate the grace period." + "editor_float alert_gracecount1" "If the number of additional alerts during the grace period exceeds the grace count, the grace period will be terminated." + + "editor_float alert_thresh2" "The alert level threshold for reaching SuspiciousState (bark, look, may stop and turn)." + "editor_float alert_time2" "The time it takes for the alert level to ramp down from the upper to the lower threshold in SuspiciousState (in seconds)." + "editor_float alert_time2_fuzzyness" "Applies some randomness to the alert time (in seconds)" + "editor_float alert_gracetime2" "After being alerted, the AI will ignore additional alerts during the grace period (in seconds), to avoid adding up the alert level too quickly" + "editor_float alert_gracefrac2" "An alert that is higher than the last increase times the grace fraction will terminate the grace period" + "editor_float alert_gracecount2" "If the number of additional alerts during the grace period exceeds the grace count, the grace period will be terminated" + + "editor_float alert_thresh3" "The alert level threshold for reaching SearchingState (Investigation)" + "editor_float alert_time3" "The time it takes for the alert level to ramp down from the upper to the lower threshold in SearchingState (in seconds)" + "editor_float alert_time3_fuzzyness" "Applies some randomness to the alert time (in seconds)" + "editor_float alert_gracetime3" "After being alerted, the AI will ignore additional alerts during the grace period (in seconds), to avoid adding up the alert level too quickly" + "editor_float alert_gracefrac3" "An alert that is higher than the last increase times the grace fraction will terminate the grace period" + "editor_float alert_gracecount3" "If the number of additional alerts during the grace period exceeds the grace count, the grace period will be terminated" + + "editor_float alert_thresh4" "The alert level threshold for reaching AgitatedSearchingState (Investigation, Weapon out, AI is quite sure that there is someone around)" + "editor_float alert_time4" "The time it takes for the alert level to ramp down from the upper to the lower threshold in AgitatedSearchingState (in seconds)" + "editor_float alert_time4_fuzzyness" "Applies some randomness to the alert time (in seconds)" + "editor_float alert_gracetime4" "After being alerted, the AI will ignore additional alerts during the grace period (in seconds), to avoid adding up the alert level too quickly" + "editor_float alert_gracefrac4" "An alert that is higher than the last increase times the grace fraction will terminate the grace period" + "editor_float alert_gracecount4" "If the number of additional alerts during the grace period exceeds the grace count, the grace period will be terminated" + + "editor_float alert_thresh5" "The alert level threshold for reaching CombatState" + + "editor_float alert_decrease_deadtime" "The time in msecs to pass before the AI's alert level is allowed to decrease again (after any increase)." + + "editor_float alert_time_fleedone" "Time the AI will stay after fleeing before getting back to normal (in seconds)" + "editor_float alert_time_fleedone_fuzzyness" "Applies some randomness to the fleedone time (in seconds)" + + "editor_float alert_initial" "Initial alert level at map start." + + "editor_bool alert_idle" "The AI will be in alert_idle instead of idle state at map start (no idle barks and animations, as if seen evidence of intruders before)." + "editor_bool disable_alert_idle" "If set to 1, the AI will not switch to alert idle behavior (weapon out, no idle barks and animations) when it has seen evidence of intruders before." + + "editor_float idle_bark_interval_min" "Minimum time between idle barks" + "editor_float idle_bark_interval_max" "Maximum time between idle barks" + + "editor_float alert_idle_bark_interval_min" "Minimum time between alert idle barks (played when the AI has seen evidence of intruders before)" + "editor_float alert_idle_bark_interval_max" "Maximum time between alert idle barks (played when the AI has seen evidence of intruders before)" + + "editor_float sleep_bark_interval_min" "Minimum time between barks while sleeping" + "editor_float sleep_bark_interval_max" "Maximum time between barks while sleeping" + + "editor_var alert_aud_thresh" "Threshold of audibility for the AI. This is in dB of sound pressure level (SPL). The real life value for humans is around 20 (a barely heard whisper). Changing this by even 0.1 has a large effect on sound propagation behavior, because propagated sounds below this volume are not heard at all. NOTE if this is not set, it gets the value from soundprop global settings." + + "editor_var health_critical" "When the health drops below this value, the AI will flee. Set this to 0 for AI that don't wet their pants." + "editor_bool unarmed_melee" "Set this to 1 when this type of AI doesn't need melee weapons to fight. This is usually true for spiders, undead and such. Humans are wimps and need weapons." + "editor_bool unarmed_ranged" "Set this to 1 when this type of AI doesn't need weapons for ranged combat. This is usually true for belchers." + "editor_var idle_animations" "A set of animation states to be played by the idle AI. See the AI's script object for possible Animation State functions." + "editor_var idle_animations_searching" "A set of animation states to be played by an AI when it is participating in a search as a guard or observer. See the AI's script object for possible Animation State functions." + "editor_var replace_anim" "Defines a set of animations that are to replace the default animations of the AI, depending on the state. Can be set on a bound entity, so that whenever this entity is bound to an AI, the animations change (and only then). Example: replace_anim_idle set to 'idle_torch' on a AI-carryable torch replaces the default set of idle animations with a specific torch one whenever the AI has the torch in his hand." + + "editor_var idle_animations_interval" "Defines the interval the idle animations should be played, in seconds. An uncertainty of +/- 20% is automatically applied by the code." + "editor_var idle_search_animations_interval" "Defines the interval the idle search animations should be played, in seconds. An uncertainty of +/- 20% is automatically applied by the code." // grayman #3857 + + "editor_int max_interleave_think_frames" "The AI will think only once in this number of frames if the distance to the player is larger than max_interleave_think_dist." + "editor_float min_interleave_think_dist" "The distance where the AI switches from thinking every frame to interleaved thinking." + "editor_float max_interleave_think_dist" "The distance where the AI thinks only once in max_interleave_think_frames. Thinking frequency decreases linearly in between." + + "editor_var can_unlock" "Names of doors this AI is able to unlock. Similar to 'used_by', multiple spawnargs of 'can_unlock' are allowed, suffixed with numbers, like 'can_unlock2'." + + "editor_float chanceNoticeWeapon" "Chance to notice the visual stim of suspicious weapons" + "editor_float chanceNoticeSuspiciousItem" "Chance to notice the visual stim of suspicious items" + "editor_float chanceNoticePerson" "Chance to notice the visual stim of other persons" + "editor_float chanceNoticeMonster" "Chance to notice the visual stim of a monster" + "editor_float chanceNoticeUndead" "Chance to notice the visual stim on an undead" + "editor_float chanceNoticeBlood" "Chance to notice the visual stim of blood" + "editor_float chanceNoticeLight" "Chance to notice the visual stim of a light that should be on" + "editor_float chanceNoticeMissingItem" "Chance to notice the visual stim of a missing item" + "editor_float chanceNoticeBrokenItem" "Chance to notice the visual stim of a broken item" + "editor_float chanceNoticeDoor" "Chance to notice the visual stim of an open door that should be closed" + + "editor_float chanceNoticePickedPocket" "Chance to notice that an object bound to the AI has been stolen" // grayman #3559 + "editor_float pickpocket_delay_min" "Minimum delay before reacting to a picked pocket" // grayman #3559 + "editor_float pickpocket_delay_max" "Maximum delay before reacting to a picked pocket" // grayman #3559 + "editor_float pickpocket_alert" "Incremental alert value to be added to an AI's current alert level" // grayman #3559 + + "editor_bool canLightTorches" "AI ability to switch on torches" + "editor_bool canOperateSwitchLights" "AI ability to operate light switches" + "editor_bool canOperateDoors" "AI ability to handle doors" + "editor_bool canOperateElevators" "AI ability to handle elevators" + "editor_bool canGreet" "AI ability to greet other actors" + + "editor_bool push_off_player" "If set to 1, a player that tries to stand on top of this AI will be pushed off (if the AI is alive)" + + "editor_float searchbark_delay_min" "Minimum time between barks during searching (in seconds)" + "editor_float searchbark_delay_max" "Maximum time between barks during searching (in seconds)" + + "editor_var max_area_reevaluation_interval" "The minimum time that needs to pass by before the AI re-evaluates a forbidden area (msec)" + "editor_var door_retry_time" "The time that needs to pass before locked doors are enabled for another try (seconds)" + + "editor_float headturn_delay_min" "Minimum time between random head turns (sec)" + "editor_float headturn_chance_idle" "The chance for random head turning while idle (0.0-1.0)" + "editor_float headturn_factor_alerted" "The headturning chance gets multiplied by this amount when the AI is alerted" + "editor_float headturn_yaw" "The maximum yaw angle of random head turning (deg)" + "editor_float headturn_pitch" "The maximum pitch angle of random head turning (deg)" + "editor_float headturn_duration_min" "The minimum duration of random head turning (sec)" + "editor_float headturn_duration_max" "The maximum duration of random head turning (sec)" + + "editor_float step_up_increase" "The amount in units the AI is translated upwards when stepping up to prevent gravity from dragging them down immediately." + + "editor_float reachedpos_bbox_expansion" "This expands the bounding box when the AI is checking for reached positions, default is 0. Don't change this unless you know what you're doing." + "editor_float aas_reachability_z_tolerance" "Defines the maximum vertical tolerance within wich a point above an AAS area is still considered reachable. Defaults to 75 units." + + "editor_bool shoulderable" "Sets whether the AI can be shouldered by the player when dead or unconscious (true by default, AI that can't be shouldered must be set so)" + "editor_string shouldered_name" "Name displayed under the shouldered body icon when this AI is shouldered and alive." + "editor_string shouldered_name_dead" "Name displayed under the shouldered body icon when this AI is shouldered and dead." + "editor_float shouldered_maxspeed" "Player speed is limited to this fraction of maximum sprint speed when the player is carrying this AI's body." + + "editor_var movetype" "The movetype (= move algorithm) used for this AI. See ai_base.script for possible values, default is ANIM." + "editor_bool bleed" "If set to true, this entity will bleed, e.g. spawn blood particles and decals when hurt." + + "editor_var use_aas" "The name of the AAS type. This controls how big the AI is and thus which grid to use to calculate the information for path finding and navigation." + + "editor_bool arrowsticking_forceenabled" "If set to true, arrows will always stick into this AI, regardless whether it is live or dead." + "editor_bool arrowsticking_disabled" "If set to true, arrows will never stick into this AI, regardless whether it is live or dead." + + "editor_var def_head" "For models with swappable heads (usually all humanoids), the name of the entity that is to be attached as head." + "editor_bool sleeping" "If true, the AI is sleeping." + "editor_var lay_down_left" "Determines the direction to which the character will lay down at map start when sleeping is true. 0 = right, 1 = left." + "editor_var lay_down_slide_dist" "This will move the AI further onto the bed while lying down." + + "editor_bool can_drown" "If true, the AI will drown if it gets under water, either by walking in or by being dropped in unconsciously. Set to '0' to make them breathe under water." + "editor_bool is_civilian" "If true, the AI is a civilian. Defaults to 0." + + "editor_string personGender" "For AI with 'AIUse' set to 'AIUSE_PERSON', defines the gender of the person. Can be either PERSONGENDER_MALE, PERSONGENDER_FEMALE or PERSONGERNDER_UNKNOWN." + "editor_string personType" "For AI with 'AIUse' set to 'AIUSE_PERSON', defines the type of the person. Can be one of PERSONTYPE_GENERIC, PERSONTYPE_UNKNOWN, or any of PERSONTYPE_BUILDER, PERSONTYPE_CITYWATCH, PERSONTYPE_PAGAN, PERSONTYPE_NOBLE etc." + + "editor_var ko_zone" "Name of the damage zone that must be hit for a possible knockout. Set this blank (i.e. : 'ko_zone' '') to make the AI immune to KOs from the blackjack." + "editor_bool ko_immune" "0 = not immune, 1 = immune to blackjack" + "editor_bool gas_immune" "0 = not immune, 1 = immune to gas" + "editor_float ko_alert_state" "Alert state number at which knockout behavior changes." + "editor_float ko_alert_immune_state" "Alert state number at which immunity behavior changes." + "editor_bool ko_alert_immune" "If true, the AI is immune to knockouts when alerted at or above the alert state defined in 'ko_alert_state'." + "editor_vector ko_spot_offset" "Defines the center of the head for knockout testing purposes. Vector offset from the head attachment joint to the desired head center point. The possible knockout cone extends back from this point." + "editor_float ko_angle_horiz" "Defines a horizontal cone angle extending backwards from the direction AI is looking. Anywhere within this cone and the vertical cone is a valid KO." + "editor_float ko_angle_vert" "Defines a vertical cone angle extending backwards from the direction AI is looking. Anywhere within this cone and the horizontal cone is a valid KO." + "editor_float ko_angle_alert_horiz" "Horizontal knockout cone angle when the AI is alerted at or above 'ko_alert_state'." + "editor_float ko_angle_alert_vert" "Vertical knockout cone angle when the AI is alerted at or above 'ko_alert_state'." + "editor_vector ko_rotation" "A vector of 3 space-delimited angles applied to rotate the AI's blackjack acceptance cone relative to their head joint. For most heads, the angles are: yaw pitch roll." + + "editor_float sneak_attack_alert_state" "Alert state number at which the AI can no longer be sneak attacked." + "editor_float sneak_attack_mult" "Damage multiplier applied to ALL damage when AI is damaged at an alert level below that defined in 'sneak_attack_alert_state'." + + "editor_int low_health_threshold" "When the AI's health drops below this value, the script 'low_health_script' will be invoked. Set this to -1 (which is the default) to disable this feature." + "editor_string low_health_script" "When the AI's health drops below 'low_health_threshold', this script will be called. The script must either take one entity argument (for global functions) or be a scriptobject member method without arguments." + + "editor_vector offsetHeadModel" "A vector with an offset to move the head into a better position relative to this model. (Seems currently not to work anymore.)" + + "editor_float eye_height" "Height of the eyes above the floor, in units. Used for vision and visibility checks." + "editor_var door_open_delay_on_use_anim" "Offset in msecs to wait before an AI is opening/closing a door after starting to play the 'use' anim. Defaults to 1100." + + "editor_float blind_time" "The time span in seconds the AI is staying in 'blinded' state when hit by a flashbomb or -mine. See also blind_time_fuzziness." + "editor_float blind_time_fuzziness" "This is the random portion of 'blind_time'. A value in [blind_time-fuzziness ... blind_time+fuzziness] is chosen." + + "editor_var def_vocal_set" "Defines the entity with the vocal set for this AI, defaulting to 'atdm:ai_vocal_set_base'." + "editor_var snd_rustle" "Defines the sound the AI makes in between footsteps. Should match what the AI is wearing. Defaults to 'default_rustle'." + + "editor_var state_name_0" "The name of the first (initial) AI state. Don't touch this unless you know what you're doing." + "editor_var state_name_1" "The name of the second AI state. Don't touch this unless you know what you're doing." + "editor_var state_name_2" "The name of the third AI state. Don't touch this unless you know what you're doing." + "editor_var state_name_3" "The name of the fourth AI state. Don't touch this unless you know what you're doing." + "editor_var state_name_4" "The name of the fifth AI state. Don't touch this unless you know what you're doing." + "editor_var state_name_5" "The name of the combat (topmost) AI state. Don't touch this unless you know what you're doing." + + "editor_bool set_frobable_on_death" "If true, the AI becomes frobable when dead, defaults to true." + "editor_bool set_frobable_on_knockout" "If true, the AI becomes frobable when KOd, defaults to true." + "editor_bool nonsolid_on_ragdoll" "If true (default), this AI becomes nonsolid when switching to ragdoll mode." + + "editor_bool enable_death_anim" "If true, this AI plays the 'death' animation on torso and legs channel. Defaults to 0 (disabled)." + + "editor_bool noDamage" "if true, monster won't take damage." + "editor_var enemy" "Set to name of character to attack when activated." + + "editor_var attack_path" "When activated, either by sight or by trigger, follow a path of path_corners to the end and then attack. Monster will not play sight or 'on_activate' animation. Will exit path if enemy gets too close, unless 'stay_on_attackpath' is set." + "editor_var stay_on_attackpath" "don't exit attack_path when close to enemy." + + "editor_var anim" "Animation to cycle when spawned in." + "editor_var num_cinematics" "When set, character is in cinematic mode. Remains hidden until triggered, then plays each anim in sequence, then triggers targets becomes hidden once more. Anims are specified by setting 'anim1', 'anim2', 'anim3', etc." + "editor_var cinematic_no_hide" "When set, character won't hide after playing cinematics." + "editor_var on_activate" "Animation to play when monster sees player or is triggered. Set to blank to make monster immediately start attacking the player without playing an animation." + "editor_var on_activate_blend" "# of frames to blend out of 'on_activate' animation when it's done playing. Default is 4 frames." + "editor_var trigger_anim" "Monster remains hidden until triggered, plays anim specified and immediately goes after the player." + "editor_var wake_on_attackcone" "Monster will wake up and attack when player enters it's attack cone. Monster will also wake up when triggered or shot." + "editor_var walk_on_trigger" "Monster begins following his path when triggered but doesn't get angry right away. Do not set 'attack_path' or 'trigger' with 'walk_on_trigger'." + + "editor_var trigger" "If set, monster will only become angry when triggered or damaged, and not on sight. Monster will play 'anim' until triggered, or follow his path. If 'attack_path' is set, monster waits until triggered to follow it. Do not set 'walk_on_trigger' when using 'trigger'." + "editor_var hide" "Causes monster to be hidden and nonsolid until triggered. Set to 2 for single trigger (entity wakes up when unhidden)." + "editor_var target" "set of path_* entities to visit." + "editor_var wander" "if set to 1, visit path_* targets randomly, otherwise visit them in order." + "editor_var wait" "How long to wait before following path. Monster will be unresponsive until this time has passed." + "editor_var neverdormant" "If set to 1, monsters do not sleep outside your PVS" + "editor_var ignore_obstacles" "If set, monster doesn't avoid obstacles. Generally should only be used for cinematics when you need monster to exactly follow a path." + + "editor_bool noshadows" "turns off shadows on character." + "editor_var animate_z" "disables gravity on character until he wakes up. used for on_activate anims where the bounding box needs to move up and down." + + "editor_float kick_force" "how much force to apply to blocking obstacles to get them out of the way." + + "editor_int team" "Monsters do not actively attack players or monsters with the same team #." + "editor_int rank" "Monsters only fight back when attacked by members of it's own team when its rank is greater than or equal to the attacker's rank. A rank of 0 will never fight back." + + "editor_float turn_rate" "Maximum # of degrees monster can turn per second" + + "editor_float mass" "Character's mass in kg." + "editor_var ragdoll" "Specifies the .af file to use for a ragdoll when the creature dies" + "editor_var ragdoll_slomoStart" "Start time of ragdoll slow motion relative to death time, defaults to -0.2" + "editor_var ragdoll_slomoEnd" "End time of ragdoll slow motion relative to death time, defaults to 1.0" + "editor_var ragdoll_jointFrictionDent" "Joint friction dent on ragdoll." + "editor_var ragdoll_jointFrictionStart" "Start time for joint friction dent" + "editor_var ragdoll_jointFrictionEnd" "End time for joint friction dent" + "editor_var ragdoll_contactFrictionDent" "Contact friction dent on ragdoll." + "editor_var ragdoll_contactFrictionStart" "Start time for contact friction dent" + "editor_var ragdoll_contactFrictionEnd" "End time for contact friction dent" + "editor_bool af_push_moveables" "whether the monster's articulated figure pushes obstacles while they're alive." + + "editor_float blink_min" "Minimum time to wait between eye blinks." + "editor_float blink_max" "Maximum time to wait between eye blinks." + + "editor_float fly_offset" "Preferred fly height relative to the player's view" + "editor_float fly_speed" "Speed to fly at." + "editor_float fly_bob_strength" "How far flying creatures should bob." + "editor_float fly_bob_vert" "Rate at which flying creatures bob up and down." + "editor_float fly_bob_horz" "Rate at which flying creatures bob left and right." + + "editor_var def_projectile" "name of entityDef to launch as projectile" + "editor_var num_projectiles" "how many projectiles to launch at a time." + "editor_var projectile_spread" "how much to spread out projectiles when they're fired. Works in conjunction with attack_accuracy. Good for things like shotgun blasts." + "editor_var attack_accuracy" "Maximum angular offset to randomly offset the monster's aim when firing missiles" + "editor_var attack_cone" "Monster can only throw projectile within this cone relative to his direction." + "editor_var attack_target" "Entity to fire missile at when 'fireMissileAtTarget' is called from script or 'fire_missile_at_target' as a framecommand." + + "editor_var melee_range" "do melee attack when within 3x this range" + "editor_var projectile_height_to_distance_ratio" "calculates the maximum height a projectile can be thrown. for example, a projectile thrown 100 units with a projectile_height_to_distance_ratio of 0.5 will only ever be up to 50 units above the target." + "editor_bool talks" "Whether character can be engaged in conversation." + "editor_var def_drops" "entityDefs to drop when killed. Multiple drop items are defined by adding suffixes: 'def_drops1', 'def_drops2', etc." + + "editor_var smokeParticleSystem" "particle system to attach to entity. To attach to a specific joint, add - and the name of the joint. For example 'fire-firejoint' attaches the 'fire' particle to the joint 'firejoint'." + + "editor_var pain_delay" "How long to wait between pain animations before triggering another pain animation." + "editor_var pain_threshold" "How much damage monster has to receive in one blow for it to play a pain animation." + + "editor_var damage_zone X" "List of bones that comprise damage zone 'X'. Pain anims are based on the name of the zone, for example: 'pain_chest', 'pain_rightleg_forward', etc. Directions are categorized as 'forward', 'left', 'right', or 'back'." + "editor_var damage_scale X" "How much to scale damage received to zone 'X'. For example 'damage_scale head' '10' would make head shots cause 10 times the normal amount of damage." + + "editor_var bone_leftEye" "Which bone to modify to adjust the left eye" + "editor_var bone_rightEye" "Which bone to modify to adjust the right eye" + "editor_var bone_focus" "Where the character's eyes are in his head" + + "editor_var look_min" "minimum angles for head looks" + "editor_var look_max" "maximum angles for head looks" + "editor_var look_joint jointname" "specifies the scale in 'pitch yaw roll' to modify the joint based on head looking. you can specify any number of joints to be affected by the head look. the scale of the joints that lead up to the head should always add up to 1 on yaw and pitch to ensure that the character looks directly at the player." + + "editor_var eye_turn_min" "min angular offset for eye joint before head has to turn." + "editor_var eye_turn_max" "max angular offset for eye joint before head has to turn." + "editor_var eye_verticle_offset" "verticle offset from the player's view that the character's eyes should focus on." + "editor_var eye_horizontal_offset" "horizontal offset from the player's view that the character's eyes should focus on. negative values bring eyes closer together." + "editor_var eye_focus_rate" "how quickly the eyes focus on a target" + "editor_var head_focus_rate" "how quickly the head focuses on a target" + "editor_var focus_align_time" "how long character should glance at the player before turning his head." + + //I suspect these aren't needed; turned off in 2.0 + //"editor_var mtr_flashShader" "shader to use for muzzleflash" +// "editor_var flashColor" "color of muzzleflash" +// "editor_var flashRadius" "radius of muzzleflash" +// "editor_var flashTime" "how long muzzleflash lasts" + + "editor_var use_combat_bbox" "Use a bounding box for damage and not per-poly hit detection." + "editor_var offsetModel" "Vector to offset the visual model from the movement bounding box." + "editor_var def_attach" "Name of entityDef to attach to model. Set 'joint' in the attached entity def to the joint to attach to, and use 'origin' and 'angles' to offset it." + "editor_var head" "Name of entityDef to attach as character's head. Copies animation from certain bones into the head model." + + "editor_var eye_height" "The height off the ground of the character's eyes. All sight checks are done from this point. When set, overrides determining the height from the position of the eye joint." + + "editor_var mtr_splat_flesh" "Material to use for blood splats." + "editor_var mtr_wound_flesh" "Material to use for wounds." + "editor_var smoke_wound_flesh" "Smoke effect to emit from wounds." + + "editor_var gravityDir" "Direction of gravity. Defines the character's concept of 'down'." + + "editor_float blockedRadius" "if monster did not move this distance within blockedMoveTime, then mark monster blocked." + "editor_float blockedMoveTime" "# of milliseconds since last move before monster marked blocked" + "editor_float blockedAttackTime" "# of milliseconds since last attack before monster marked blocked" + + "editor_bool canSearch" "Whether the AI can conduct a search or not" + + "editor_float hitByMoveableLookAtTime" "When hit by a moveable, stop and look at it for this long (seconds) (default 2.0)" + "editor_float hitByMoveableLookBackTime" "When hit by a moveable, look at where it came from for this long (seconds) (default 2.0)" + + // ===================== Default Values ======================= + + "spawnclass" "idAI" + + "blockedRadius" "10" + "blockedMoveTime" "750" + "blockedAttackTime" "750" + + "wait" "0" + "team" "1" + "rank" "0" + "on_activate" "sight" + "def_projectile" "" + "attack_cone" "70" + "attack_accuracy" "0" + "melee_range" "0" + "projectile_height_to_distance_ratio" "0.9" + "turn_rate" "360" + "bleed" "1" + "pain_threshold" "1" + "blink_min" "2.5" + "blink_max" "8.0" + "talks" "0" + "look_min" "-80 -75 0" + "look_max" "80 75 0" + "eye_verticle_offset" "5" + "eye_horizontal_offset" "-8" + "eye_focus_rate" "0.5" + "head_focus_rate" "0.1" + "kick_force" "60" + + // greebo: Set this to "1" when this type of AI doesn't need melee weapons to fight + // This is usually true for spiders, undead and such. Humans are wimps and need weapons. + "unarmed_melee" "1" + + // angua: Set this to "1" if this AI can perform ranged combat without needing weapons + + "unarmed_ranged" "0" + + "blind_time" "8" + "blind_time_fuzziness" "4" + + "chanceNoticeWeapon" "0" + "chanceNoticeSuspiciousItem" "0" + "chanceNoticePerson" "1" + "chanceNoticeMonster" "1" // grayman #3331 + "chanceNoticeUndead" "1" // grayman #3343 + "chanceNoticeBlood" "0" + "chanceNoticeLight" "0" + "chanceNoticeMissingItem" "0" + "chanceNoticeBrokenItem" "1" + "chanceNoticeDoor" "0" + "chanceNoticePickedPocket" "0" // grayman #3559 + + "canLightTorches" "0" + "canOperateSwitchLights" "0" + "canOperateDoors" "0" + "canOperateElevators" "0" + + // by default the player can't stand on this AI's head forever + "push_off_player" "1" + + "searchbark_delay_min" "10" + "searchbark_delay_max" "15" + + // greebo: The minimum time that needs to pass by before the AI re-evaluates a forbidden area (msec) + "max_area_reevaluation_interval" "20000" // milliseconds + + // greebo: This expands the bounding box when the AI is checking for reached positions, + "reachedpos_bbox_expansion" "0" + + // greebo: Defines the maximum vertical tolerance within wich a point above an AAS area is still considered reachable. + "aas_reachability_z_tolerance" "75" + + "step_up_increase" "0" + + // greebo: The time that needs to pass before locked doors are enabled for another try (seconds) + "door_retry_time" "120" // seconds + + // Default AI Alert Info: + + // alert_time: is the time it takes for the alert level to ramp down from the upper to the lower threshold (in seconds) + // alert_time_fuzzyness: applies some randomness to the alert times (in seconds) + // alert_gracetime: after being alerted, the AI will ignore additional alerts during this time, to avoid adding up the alert level too quickly + // alert_gracefrac: an alert that is higher than the last increase * fraction will terminate the grace period + // alert_gracecount: If the number of additional alerts during grace period exceeds the grace count, the grace period will be terminated + + "af_push_moveables" "1" + + // Observant state - bark but otherwise no reaction + "alert_thresh1" "1.5" + "alert_time1" "5" + "alert_time1_fuzzyness" "1.5" + "alert_gracetime1" "2" + "alert_gracefrac1" "1.2" + "alert_gracecount1" "5" + + // Suspicious state - barks and looks, may stop and turn + "alert_thresh2" "6" + "alert_time2" "8" + "alert_time2_fuzzyness" "2" + "alert_gracetime2" "2" + "alert_gracefrac2" "1.2" + "alert_gracecount2" "5" + + // Searching state - AI goes investigating + "alert_thresh3" "10" // grayman #3492 - was 8 + "alert_time3" "25" // grayman #3492 - was 30 + "alert_time3_fuzzyness" "8" // grayman #3492 - was 10 + "alert_gracetime3" "3" + "alert_gracefrac3" "1" + "alert_gracecount3" "4" + + // Agitated searching - investigating, weapon out + "alert_thresh4" "18" + "alert_time4" "65" + "alert_time4_fuzzyness" "20" + "alert_gracetime4" "2" + "alert_gracefrac4" "1.0" + "alert_gracecount4" "4" + + // Combat state + "alert_thresh5" "23" + + // The time in msecs to pass before the AI's alert level is allowed to decrease again (after any increase). + "alert_decrease_deadtime" "2000" + + // time the AI will stay after fleeing before getting back to normal + "alert_time_fleedone" "80" + "alert_time_fleedone_fuzzyness" "40" + + "idle_bark_interval_min" "30" + "idle_bark_interval_max" "90" + + "alert_idle_bark_interval_min" "40" + "alert_idle_bark_interval_max" "120" + + "sleep_bark_interval_min" "10" + "sleep_bark_interval_max" "30" + + // acuities, in percent + "acuity_vis" "100" + "acuity_aud" "100" + "acuity_tact" "100" + "acuity_other" "100" + + "alert_aud_thresh" "18.8" //old value "20.2" + + // not yet implemented + "acuity_env" "100" + + //set a default field of view to a realistic human value + "fov" "150" // horizontal FOV (realistic human : 180-200) + "fov_vert" "120" // vertical FOV (realistic human: 120-135, but asymmetric) + /** + * Rotation of the FOV cone relative to the head joint + * (yaw pitch roll) + * Human FOV extends farther down than up, so pitch down: + **/ + "fov_rotation" "0 -20 0" + + "can_be_flatfooted" "1" + // time AI remain stationary after becoming flat-footed + "flatfooted_time" "1200" + // settings for becoming flat footed due to parrying an attack (see usage above) + "flatfoot_parry_num" "1" + "flatfoot_parry_time" "3500" + + + // bodies stick around forever by default + "editor_int burnaway" "Time until the dead body will be removed with a special effect. Set to -1 to disable it. Disabled for TDM by default." + "burnaway" "-1" + + "editor_skin skin_dead" "Set this to a non-empty string to swap the AI's skin when it dies" + "editor_var fx_on_death" "The FX which will be played when this entity dies. Set to empty to disable." + "editor_var fx_on_ko" "The FX which will be played when this entity is knocked out. Set to empty to disable." + "editor_var death_script" "The script function (local or global) to call when this AI dies. Global functions need to take an entity as single argument, local functions no arguments at all. As argument is the AI passed that was just killed." + "editor_var ko_script" "The script function (local or global) to call when this AI is knocked out. Global functions need to take an entity as single argument, local functions no arguments at all. As argument is the AI passed that was just knocked out." + + /** + * AIs cannot be mantled by default + **/ + "is_mantleable" "0" + + /** + * Hack: For now, increase player drag force 10x when dragging AI + **/ + "drag_force_mod" "10.0" + + // AIs are not moss-able + "nomoss" "1" + + "editor_var head_bodyname" "Name of the head body on the AF. Used to detect drowning when unconscious. Not needed if the AI has a separate head model." + "head_bodyname" "head" + + "editor_var head_jointname" "Name of the head joint in the md5 mesh. This joint should either be the look joint or get rotated to match the look joint in order for KO to work properly." + "head_jointname" "Head" + + // default is shoulderable + "shoulderable" "1" + + /** + * Ishtvan: test new system, equippable + **/ + "equippable" "1" + + "editor_var shouldered_name" "Generic inventory name of the body for when it is shouldered unconscious. Individual AI can overload this to display a name when carried." + "editor_var shouldered_name_dead" "Generic inventory name of the body for when it is shouldered dead. Individual AI can overload this to display a name when carried." + "shouldered_name" "#str_02410" // Body + "shouldered_name_dead" "#str_02409" // Corpse + + "shouldered_maxspeed" "0.5" + + "editor_vector drop_angles" "The angle used to drop this body. Bodies always try to drop lying flat, head forward." + "drop_angles" "90.0 0 0" + + "editor_vector drop_point" "Offset from the player origin to the point where this AI's body is dropped. Bodies always drop straight ahead of the player's head, they don't follow where player looks up/down." + "drop_point" "30.0 0 -16.0" // changed from 35 to 30 to allow dropping in less space. + + "editor_bool drop_to_hands" "If set to true, the item drops to the hands of the player instead to the ground. Default is true." + + // Shoulderable bodies are not held on to when unshouldered + "drop_to_hands" "0" + + // sound body makes when dropped/KO'd + "snd_bounce" "body_collapse" + + "snd_bounce_carpet" "body_collapse_soft" + "snd_bounce_cloth" "body_collapse_soft" + "snd_bounce_grass" "body_collapse_soft" + "snd_bounce_snow" "body_collapse_soft" + "snd_sliding" "tdm_heavy_stone_scratching02_loop" + "snd_bounce_moss" "body_collapse_soft" + "snd_water" "water_hit_by_large_object" + + + "spr_object_hardness" "hard" + "spr_object_size" "small" //hard-small combination creates the best sound results (soft: 34, hard: 45) + + + "tdm_bounce_sound_max_vel" "50" + "tdm_bounce_sound_min_vel" "800" + // relevant to above: The collision velocities are shown when the cvar "tdm_show_moveable_collision" is on. + + /** + * Test: preliminary setup for aesthetic showing of body held on shoulder + **/ + /* + "equip_in_world" "1" + "equip_draw_on_top" "1" + "equip_nonsolid" "1" + */ + + // default starting animation is idle + "anim" "idle" + + // Name of the head animation to use for lipsync + // (Should just be a simple mouth-opening animation; fully closed at frame 1, fully open at the end. The + // lipsync code will pick out individual frames from it based on the amplitude of the lipsynced sound.) + "lipsync_anim_name" "talk1" + + // patrols path nodes by default + "patrol" "1" + "animal_patrol" "0" + "movetype" "ANIM" // greebo: see ai_base.script for possible movetypes + + "sitting" "0" + "sitting_turn_pivot" "-20 0 0" + "sit_down_slide_dist" "8" + + "lay_down_slide_dist" "16" + + "drunk_acuity_factor" "0.5" + + "stepvol_walk" "10" + "stepvol_run" "15" + + // never dormant by default + // Removed by Crispy; AI are still never dormant by default, + // but this is handled in code now. + // angua: Re-enabled to make it possible to set individual AI dormant + "neverDormant" "1" + "editor_bool neverDormant" "Set to 1 to make the AI never dormant. If set to 0, the AI can go dormant at map start under specific circumstances." + + // Knockout info + + /** + * Name of the damage zone that must be hit for possible KO + * Set this blank (i.e. : "ko_zone" "") to make the AI immune to KOs + **/ + "ko_zone" "head" + + /** + * ko_angle defines the KO cone extending backwards from the direction AI is + * looking. By default, you can hit AI anywhere on the head to KO them + * when they're not alert. + * + * Helmeted guards should use a lesser angle + **/ + "ko_angle_horiz" "360" + "ko_angle_vert" "360" + + /** + * KO cone changes when AI is alerted, this defines the new cone angle + * These settings work pretty well for not being able to hit them from the front. + **/ + "ko_angle_alert_horiz" "110" + "ko_angle_alert_vert" "180" + + /** + * Rotation of the KO cone relative to the head joint + **/ + "ko_rotation" "0 0 0" + + /** + * KO Offset in head body coordinates, relative to head joint + * Should be approximately the center of the head + * KO cone extends backwards from this point + * NOTE: Tweaked for builder guard, has the most up-to-date head setup + **/ + "ko_spot_offset" "2 -2.5 0" + + /** + * Name of the alert state when AI's KO behavior changes + **/ + "ko_alert_state" "4" //once AI draw weapon + + /** + * If set to true, AI becomes immune to KOs when alerted + **/ + "ko_alert_immune" "0" + + /** + * Sets AI alert immunity state + **/ + "ko_alert_immune_state" "5" //Level 5 for non-helmeted AI who reach combat or level 4 for helmeted AI who are agitated searching +// end Knockout info + + /** + * Name of the alert state at which AI can no longer be sneak attacked. + **/ + "sneak_attack_alert_state" "2" + + /** + * At alert states below the threshold defined above, ALL damage done to the AI + * gets this additional sneak attack multiplier: + **/ + "sneak_attack_mult" "2.0" + + "melee_predicts_proximity" "1" // very dumb AI like robots should override this + "melee_predicted_attack_time" "750" // works for our sword anims, needs overloading for animals, zombies + + // Slow down the player to this fraction * their max speed when holding this body + // Very small/light AI should override this value + "grab_encumbrance" "0.5" + + // Minimum and maximum periods to wait between throwing player-out-of-reach projectiles + "outofreach_projectile_delay_min" "7.0" + "outofreach_projectile_delay_max" "10.0" + + // Min/max periods to wait in cover before emerging again + "emerge_from_cover_delay_min" "20.0" + "emerge_from_cover_delay_max" "30.0" + + // The health threshold - health values below this cause the AI to flee + "health_critical" "15" + + // If 0, don't throw projectiles when the enemy is out of reach (by default, non-humanoid AIs will not do so) + "outofreach_projectile_enabled" "0" + + // By default, non-humanoid AIs don't take cover at all + "taking_cover_enabled" "0" + "taking_cover_max_cost" "200" // grayman #3507 (was 100) + "editor_bool taking_cover_only_from_archers" "If true, takes only cover from archers and other AI with projectile weapons." + "taking_cover_only_from_archers" "0" + + + "max_interleave_think_frames" "10" + "min_interleave_think_dist" "1000" + "max_interleave_think_dist" "3000" + + "headturn_delay_min" "3" + "headturn_chance_idle" "0.3" + "headturn_factor_alerted" "2" + "headturn_yaw" "60" + "headturn_pitch" "40" + "headturn_duration_min" "1" + "headturn_duration_max" "3" + + // greebo: Comma-separated list of anims + // Springheel: The specific animation lists were moved to tdm_ai_humanoid_newskel.def + + // Animations affecting all channels (refer to anim names in the AI's modelDef) + "idle_animations" "" + + // Animations only playing on the torso channel. Applicable anytime. + "idle_animations_torso" "" + + // Animations only playing on the torso channel while the AI is sitting. Applicable anytime. + "idle_animations_sitting" "" + + // Animations affecting all channels (refer to anim names in the AI's modelDef) + "idle_animations_searching" "" // grayman #3857 + + "idle_animations_interval" "25" // in seconds +/- 20% + "idle_search_animations_interval" "10" // in seconds +/- 20% - grayman #3857 + + // Offset in msecs to wait before opening a door after starting the "use" anim + "door_open_delay_on_use_anim" "1100" + + "canSearch" "1" // grayman #3069 - can conduct searches + + "hitByMoveableLookAtTime" "0.0" // grayman #2816 + "hitByMoveableLookBackTime" "0.0" // grayman #2816 + + "editor_int sleep_location" "0 = sleep on the floor, 1 = sleep on a bed, 2 = sleep on a chair" + "sleep_location" "1" // grayman #3396 - 0 = sleep on the floor, 1 = sleep on a bed, (grayman #3820) 2 = sleep on a chair + + "editor_bool absorb_projectile" "whether the AI should absorb a projectile, instead of allowing it to stick ('active') or break ('dud')" + "absorb_projectile" "0" + + // Response to communication Stim + "sr_class_1" "R" + "sr_type_1" "STIM_COMMUNICATION" + "sr_state_1" "1" + + // Response to visual contact + "sr_class_2" "R" + "sr_type_2" "STIM_VISUAL" + "sr_state_2" "1" + "sr_script_STIM_VISUAL" "response_visualStim" + + // Give off visual contact stim to other AIs around me + "sr_class_3" "S" + "sr_state_3" "1" + "sr_type_3" "STIM_VISUAL" + "sr_radius_3" "600" + "sr_time_interval_3" "2000" // Once per 2 seconds + + // Response to DAMAGE_STIM + "sr_class_4" "R" + "sr_state_4" "1" + "sr_type_4" "STIM_DAMAGE" + "sr_effect_4_1" "effect_damage" + "sr_effect_4_1_arg1" "_SELF" + "sr_effect_4_1_arg2" "atdm:damage_low" +} + +entityDef atdm:ai_humanoid +{ + "inherit" "atdm:ai_base" + "editor_displayFolder" "AI/Internal/Base Class" + + "editor_usage" "Don't use. Base class for all TDM humanoid AI entities." + + // This defines how AIs will interact with this entity + "AIUse" "AIUSE_PERSON" + "personType" "PERSONTYPE_GENERIC" + "personGender" "PERSONGENDER_UNKNOWN" + + "anim" "idle" // animation to play on map start + "head_joint" "Head" // humanoids will have their head attached to this joint + + "anim_rate_run" "0.8" //slows down AI so they are slightly slower than player + "anim_rate_run_torch" "0.8" + + // These defines the chances the AI will notice various visual stimuli + "chanceNoticeWeapon" "1.0" + "chanceNoticeSuspiciousItem" "1.0" // grayman #1327 + "chanceNoticeRope" "1.0" // grayman #2872 + "chanceNoticePerson" "1.0" + "chanceNoticeMonster" "1.0" // grayman #3331 + "chanceNoticeUndead" "1.0" // grayman #3343 + "chanceNoticeBlood" "1.0" + "chanceNoticeLight" "0.9" // grayman #2603 + "chanceNoticeMissingItem" "1.0" + "chanceNoticeDoor" "1.0" + + "chanceNoticePickedPocket" "0.5" // grayman #3559 + "pickpocket_alert" "0" // grayman #3559 + "pickpocket_delay_min" "5000" // grayman #3559 + "pickpocket_delay_max" "25000" // grayman #3559 + + // These define if the AI can operate various kinds of lights + "canLightTorches" "1" + "canOperateSwitchLights" "1" + "chanceLightTorches" "0.4" // grayman #2603 + "chanceOperateSwitchLights" "0.7" // grayman #2603 + + "canOperateDoors" "1" + "canOperateElevators" "1" + + "canGreet" "1" + + // Humans need weapons to fight + "unarmed_melee" "0" + + "damage_zone head" "*Neck" + "damage_zone chest" "*Spine_Dummy -*Neck" + "damage_zone torso_low" "Hips" + "damage_zone left_arm" "*LeftArm" + "damage_zone right_arm" "*RightArm" + "damage_zone legs" "*LeftHips_Dummy *RightHips_Dummy origin" + + "damage_scale head" "2" + "damage_scale chest" "1.5" + "damage_scale torso_low" "1.0" + "damage_scale left_arm" "0.3" + "damage_scale right_arm" "0.3" + "damage_scale legs" "0.4" + + //all humanoid AI will be using the same base skeleton + "ik_numLegs" "2" + "ik_minWaistAnkleDist" "16" + "ik_footSize" "4" + "ik_waist" "Hips" + "ik_hip1" "LeftUpLeg" + "ik_hip2" "RightUpLeg" + "ik_knee1" "LeftLeg" + "ik_knee2" "RightLeg" + "ik_ankle1" "LeftFoot" + "ik_ankle2" "RightFoot" + //normally a knee joint + //"ik_dir1" "LeftFoot" + //"ik_dir2" "RightFoot" + "ik_foot1" "LeftToeBase" + "ik_foot2" "RightToeBase" + + + // We now use AAS32 by default + "use_aas" "aas32" + "size" "32 32 68" + "cylinder" "10" // optional + + "eye_height" "77" + + // Rates for AI turning their head randomly--these values cause AI to move their shoulders slightly as well + + "look_min" "-80 -80 0" // "-80 -90 0" new values to address 0003754: AI seeing directly behind them + "look_max" "15 80 0" // "25 90 0" + "look_joint Spine" "0.3 0.3 0" + "look_joint Head" "0.7 0.7 0" + + // Allow vertical aiming at the player during combat + "combat_look_joint Spine" "0.1333 0.1333 0" + "combat_look_joint Spine2" "0.1333 0.1333 0" + "combat_look_joint Head" "0.6 0.6 0" + //"combat_look_joint RightArm" "0.6 0 0" // grayman #3473 + //"combat_look_joint LeftArm" "0.6 0 0" // grayman #3473 + + "head_focus_rate" "0.05" // how fast head turns to focus on target + + // the number of degrees an AI can turn in 1 second + "turn_rate" "180" + // Attachment Positions (For OLD skeleton, NEW skeleton positions are in tdm_ai_humanoid_newskel) + + // hand: orientation matches the weapon model convention + // (forward in the hand is to the right in the model) + "attach_pos_name_handr" "hand_r" + "attach_pos_joint_handr" "RightHand" + // joint orientation: down sideways forward + "attach_pos_origin_handr" "-5 2 0" + // joint orientation: roll, pitch, yaw + "attach_pos_angles_handr" "180 0 90" + + "attach_pos_name_handl" "hand_l" + "attach_pos_joint_handl" "LeftHand" + "attach_pos_origin_handl" "5 -3 0" + "attach_pos_angles_handl" "180 0 -90" + + // Position for sword sheathed at the left hip (still WIP, not quite right) + "attach_pos_name_hipsheathl" "hip_sheath_l" + "attach_pos_joint_hipsheathl" "Hips" + "attach_pos_origin_hipsheathl" "7.5 4.4 6.6" + "attach_pos_angles_hipsheathl" "-60 -90 -80" + + // Positions on the belt (calibrated for builder guard w/ purse) + + "attach_pos_name_beltb" "belt_back" + "attach_pos_joint_beltb" "Hips" + // left, up, in/out + "attach_pos_origin_beltb" "0 7 -4" + "attach_pos_angles_beltb" "-90 0 -90" + + "attach_pos_name_beltbr" "belt_back_r" + "attach_pos_joint_beltbr" "Hips" + // left, up, in/out + "attach_pos_origin_beltbr" "-5 7 -4" + "attach_pos_angles_beltbr" "-90 0 -90" + + "attach_pos_name_beltbl" "belt_back_l" + "attach_pos_joint_beltbl" "Hips" + "attach_pos_origin_beltbl" "5 7 -4" + "attach_pos_angles_beltbl" "-90 0 -90" + + "attach_pos_name_beltfr" "belt_front_r" + "attach_pos_joint_beltfr" "Hips" + // left, up, in/out + "attach_pos_origin_beltfr" "-5 7 5.5" + "attach_pos_angles_beltfr" "90 0 -90" + + "attach_pos_name_beltfl" "belt_front_l" + "attach_pos_joint_beltfl" "Hips" + "attach_pos_origin_beltfl" "5 7 5.5" + "attach_pos_angles_beltfl" "90 0 -90" + + + "editor_bool ground_when_dragged" "If set to true, don't let the player pick the ragdoll all the way up off the ground." + "ground_when_dragged" "1" + + "editor_string ground_critical_bodies" "A list of AF bodies, separated by space. These bodies checked to see if they're on the ground when 'ground_when_dragged' is true." + // Currently based on /af/guard_base.af + "ground_critical_bodies" "waist chest rupleg Lupleg ruparm Luparm" + + "editor_int ground_min_number" "At least so many AF bodies defined in 'ground_critical_bodies' must remain on the ground, or dragging upwards is not allowed." + "ground_min_number" "1" + + "editor_bool drag_af_damping" "If true, apply AF damping when dragging the ragdoll." + "drag_af_damping" "1" + + // This frob distance is intended for frobbing AI ragdolls + // Might have to revise this to have conversations via frobbing, etc. + "frob_distance" "60" + + "editor_int hold_distance_min" "Distance ragdoll is from player when dragged, default is 35. In units." + "hold_distance_min" "35" + + // melee weapon swing sound + "snd_swing" "sword_swing" + + // blade sounds + "snd_drawsword" "sword_ai_unsheath" + "snd_sheathesword" "sword_ai_sheath" + + // default footstep, + "snd_footstep" "human_stone" + + // material specific footsteps (DarkMod SFX) + + "snd_footstep_straw" "human_straw" + "snd_footstep_wood" "human_wood" + "snd_footstep_puddle" "movement_water" + "snd_footstep_wading" "movement_water" + "snd_footstep_tile" "human_tile" + + "snd_footstep_stone" "human_stone" + + "snd_footstep_snow" "human_snow" + "snd_footstep_gravel" "human_gravel" + "snd_footstep_metal" "human_metal" + "snd_footstep_grass" "human_grass" + "snd_footstep_carpet" "human_carpet" + "snd_footstep_dirt" "human_dirt" + "snd_footstep_foliage" "human_foliage" + // SteveL #3634: Adding 8 footstep sounds to match player's set + "snd_footstep_sand" "human_dirt" + "snd_footstep_glass" "human_glass" + "snd_footstep_ice" "human_ice" + "snd_footstep_cloth" "human_carpet" + "snd_footstep_armor_leath" "human_carpet" + "snd_footstep_flesh" "human_mud" + "snd_footstep_moss" "human_carpet" + "snd_footstep_liquid" "movement_water" + + // voice set (test set for now, remove before release!) + + // greebo: The base humanoid AI is using the base vocal set + "def_vocal_set" "atdm:ai_vocal_set_base" + + "health" "100" + + // Projectile, for throwing when the enemy is out of reach (make me a rock please) + "def_projectile" "atdm:projectile_rock" + + // Accuracy of projectile-throwing. 0 means perfect accuracy, larger values are worse. + "attack_accuracy" "2.3" + + //This value may not even be needed, but is included here just in case: + "attack_cone" "70" + + // ============== Melee Settings =========== + // melee difficulty set to use (default is skilled) + "def_melee_set" "atdm:ai_melee_set_skilled" + + // Range at which an AI thinks it can hit you + "melee_range" "40" + + "melee_predicts_proximity" "1" // most humanoids are smart enough to do this + "melee_predicted_attack_time" "750" // works for our sword anims, needs overloading for animals, zombies + // ============ End Melee Settings =========== + + // Set this to 0 to turn off throwing projectiles when enemies are out of reach. + // For example, you might use scripts to prevent guards from throwing things in + // the antiques room (don't want to break those vases!). + "outofreach_projectile_enabled" "1" + + // This is used to limit the distance which an AI will travel to take cover. + // 0 means no limit. As a rough guideline, this is approximately measured in Doom units + // divided by 3; however, this can vary. + "taking_cover_max_cost" "200" // grayman #3507 (was 100) + + // Set to 0 to disable taking cover. + "taking_cover_enabled" "1" + + // Set to 0 to take cover from all enemies, not just ones that can threaten us from a distance. + "taking_cover_only_from_archers" "1" + + // greebo: Humanoids should be translated upwards a bit when stepping up slopes + "step_up_increase" "10" + + /** + * ishtvan: This larger box CM for the head is swapped in + * doing collision tests with the blackjack, at all + * times the AI is alive, except when in combat state. + **/ + "blackjack_headbox_mins" "-10 -3 -9" + "blackjack_headbox_maxs" "6 4 4.5" + + "mouth_offset" "0 0 15" // grayman #1488 + "eye_offset" "0 0 19" // grayman #3525 + + "hitByMoveableLookAtTime" "2.0" // grayman #2816 + "hitByMoveableLookBackTime" "2.0" // grayman #2816 + + "canSearchCooperatively" "1" // grayman #3857 - ability to participate in a coordinated search +} + +entityDef atdm:ai_humanoid_newskel +{ + "inherit" "atdm:ai_humanoid" + "editor_displayFolder" "AI/Internal/Base Class" + + "editor_usage" "Don't use. Base class for all TDM humanoid AI entities using new skeleton." + + + "head_joint" "Spine2" // joint head gets attached to + "copy_joint Neck" "neckcontrol" // keeps neck attached to shoulders + "copy_joint Head" "headcontrol" + + "ko_spot_offset" "2 2 0" + + "bone_leftEye" "leye" // allows TDM to detect eye height automatically + "bone_rightEye" "reye" + + // tweak melee animation speeds here for gameplay + // isthvan: slowing down the overhead and left to right attacks compared to old skeleton + "anim_rate_melee_attack_lr" "1.15" + "anim_rate_melee_attack_over" "1.1" + + "anim_rate_run_charge" "0.8" + "anim_rate_run_charge_torch" "0.8" + "anim_rate_run_jog" "0.8" + + "anim_rate_use_righthand" "1.5" // speed up door opening slightly // grayman #3755 + "anim_rate_use_lefthand" "1.5" // speed up door opening slightly // grayman #3755 + "anim_rate_controller_high" "1.5" // grayman #3755 + "anim_rate_controller_med" "1.5" // grayman #3755 + "anim_rate_controller_low" "1.5" // grayman #3755 + + "anim_rate_sit_up" "1.3" + + "anim_rate_idle_search_suspicious" "1.1" + "anim_rate_idle_search_suspicious01" "1.1" + "anim_rate_idle_search_suspicious02" "1.1" + "anim_rate_idle_search_suspicious02_armed" "1.1" + "anim_rate_idle_search_suspicious03" "1.1" + "anim_rate_idle_search_suspicious03_armed" "1.1" + "anim_rate_idle_search_suspicious03_torch" "1.1" + + "anim_rate_throw02" "1.2" + + "anim_rate_sit_warmhands" "1.5" + + // greebo: Comma-separated list of anims + + // Animations affecting all channels (refer to anim names in the AI's modelDef) + "idle_animations" "idle_leg_scratch,idle_cough,idle_cough02,idle_armwipe,idle_arm_scratch,idle_sneeze,idle_adjusting_belt,idle_eat,idle_spit,idle_shifting1,idle_shifting2,idle_nose_wipe,idle_nosewipe_short,idle_stretch,idle_stretch02,idle_standontoes,idle_check_hand,idle_scuff" + + // Animations only playing on the torso channel. Applicable anytime. + "idle_animations_torso" "idle_cough,idle_nosewipe_short,idle_cough02,idle_armwipe,idle_arm_scratch,idle_sneeze,idle_adjusting_belt,idle_eat,idle_spit,idle_stretch,idle_check_hand" + + // Animations only playing on the torso channel while the AI is sitting. Applicable anytime. + "idle_animations_sitting" "idle_sit_shift1,idle_sit_cough01,idle_sit_armdrop01,idle_sit_tap01,idle_sit_neckscratch" + + // Animations affecting all channels (refer to anim names in the AI's modelDef) + "idle_animations_searching" "idle_search_suspicious,idle_search_suspicious01,idle_search_suspicious02,idle_search_suspicious03" // grayman #3857 + + // sound played while walking or moving between footsteps + + "snd_rustle" "default_rustle" + + + "ik_numLegs" "2" + "ik_minWaistAnkleDist" "0" + "ik_footSize" "0" + "ik_waist" "Pelvis2" + "ik_hip1" "LeftUpLeg" + "ik_hip2" "RightUpLeg" + "ik_knee1" "LeftLeg" + "ik_knee2" "RightLeg" + "ik_ankle1" "LeftFoot" + "ik_ankle2" "RightFoot" + //normally a knee joint + //"ik_dir1" "LeftFoot" + //"ik_dir2" "RightFoot" + "ik_foot1" "LeftToeBase" + "ik_foot2" "RightToeBase" + + + // attachment positions for the new skeleton + "attach_pos_name_hipsheathl" "hip_sheath_l" + "attach_pos_joint_hipsheathl" "Hips" + "attach_pos_origin_hipsheathl" "6.5 6.7 -6.2" //f/b, u/d, s/s + "attach_pos_angles_hipsheathl" "10 140 -10" + + "attach_pos_name_handr" "hand_r" + "attach_pos_joint_handr" "RightHand" + "attach_pos_origin_handr" "0 0 4.4" + "attach_pos_angles_handr" "260 90 0" + + "attach_pos_name_handl" "hand_l" + "attach_pos_joint_handl" "LeftHand" + "attach_pos_origin_handl" "-2 -0.8 -4.5" + "attach_pos_angles_handl" "80 -90 0" + + // greebo: needed different hand attach position due to the longbow being rotated differently + "attach_pos_name_handrbow" "hand_r_bow" + "attach_pos_joint_handrbow" "RightHand" + "attach_pos_origin_handrbow" "0 0 4.4" + "attach_pos_angles_handrbow" "90 90 0" + + // greebo: needed different hand attach position due to the longbow being rotated differently + "attach_pos_name_handlbow" "hand_l_bow" + "attach_pos_joint_handlbow" "LeftHand" + "attach_pos_origin_handlbow" "-2 -0.8 -4.5" + "attach_pos_angles_handlbow" "-80 90 0" + + + + "attach_pos_name_hipbackr" "belt_back_right" + "attach_pos_joint_hipbackr" "Hips" + "attach_pos_origin_hipbackr" "-4 8 4" // x, height, width + "attach_pos_angles_hipbackr" "90 90 0" + + // greebo: needed different back attach position since the longbow is different to hammers + "attach_pos_name_bowholster" "bow_holster_pos_rl" + "attach_pos_joint_bowholster" "Spine2" + "attach_pos_origin_bowholster" "-5 -5 0" //middle value controls distance from back + "attach_pos_angles_bowholster" "0 90 25" + +//these need double-checking + "attach_pos_name_slbackrl" "slung_across_back_rl" + "attach_pos_joint_slbackrl" "Spine2" + "attach_pos_origin_slbackrl" "14 -6 -6" //middle value controls distance from back + "attach_pos_angles_slbackrl" "0 90 25" + + "attach_pos_name_slbacklr" "slung_across_back_lr" + "attach_pos_joint_slbacklr" "Spine2" + "attach_pos_origin_slbacklr" "14 -2 -6" + "attach_pos_angles_slbacklr" "0 90 -5" + + "mouth_offset" "0 0 15" // grayman #1488 + "eye_offset" "0 0 19" // grayman #3525 +} + +model tdm_ai_builderguard { + inherit tdm_ai_proguard + mesh models/md5/chars/builders/guard/builderguardmesh.md5mesh + + channel torso ( *Spine_Dummy) + channel legs ( origin Pelvis Pelvis2 *Hips) + + //replacing animations that don't fit character + anim idle_nose_wipe models/md5/chars/guards/proguard/idle_armwipe.md5anim + anim idle_nosewipe_short models/md5/chars/guards/proguard/idle_shifting2.md5anim + anim idle_spit models/md5/chars/guards/proguard/idle_check_hand.md5anim + anim idle_eat models/md5/chars/guards/proguard/idle_shifting2.md5anim + + //the default walk2 animation does weird things to the Builder pauldrons + anim walk1 models/md5/chars/guards/proguard/walk1.md5anim + { + frame 1 sound snd_rustle + frame 3 footstep + frame 17 sound snd_rustle + frame 20 footstep + } +} + +entityDef atdm:ai_builder_guard +{ + "inherit" "atdm:ai_humanoid_newskel" + + "editor_displayFolder" "AI/Builders/Armed" + "editor_usage" "Builder Guards defend Church territory and sometimes enforce Church law." + "AIUse" "AIUSE_PERSON" + "personType" "PERSONTYPE_BUILDER" + "personGender" "PERSONGENDER_MALE" + + "ragdoll" "guard_base_newskel" + "model" "tdm_ai_builderguard" + + "def_head" "atdm:ai_head_builderguard" + "offsetHeadModel" "0 0 -3" // defaultvalue if set + "atdm:ai_head06_builder" "0 0 1" + "atdm:ai_head_builderguard" "0 0 .7" //modifier to default value + + "headturn_yaw" "40" //needs special values to avoid clipping neckguard + "headturn_pitch" "20" + + // modified look angles due to metal collar + "look_min" "-80 -40 0" + "look_max" "15 40 0" + + "team" "1" + "health" "100" + "mass" "110" // wears a heavy hammer and armor + + // skilled in melee + "def_melee_set" "atdm:ai_melee_set_skilled" + + // some idle animations don't look good on builderguard and were removed + "def_attach1" "atdm:moveable_warhammer" + "name_attach1" "melee_weapon" + "pos_attach1" "slung_across_back_rl" + "draws_weapon" "1" + "melee_range" "40" + + "def_attach2" "pauldron_builder_right" + "def_attach3" "pauldron_builder_left" + + "def_vocal_set" "atdm:ai_vocal_set_builder_03_guard" + "ko_alert_immune" "1" + + "snd_bounce" "body_armour_collapse" + "snd_bounce_carpet" "body_armour_collapse_soft" + "snd_bounce_cloth" "body_armour_collapse_soft" + "snd_bounce_grass" "body_armour_collapse_soft" + "spr_object_hardness" "hard" + "spr_object_size" "medium" + "snd_rustle" "armour_clank" + "snd_sliding" "tdm_heavy_stone_scratching02_loop" + + "rank" "2" + + "lod_1_distance" "150" + "model_lod_1" "tdm_ai_builderguard_med" + + "lod_2_distance" "250" + "model_lod_2" "tdm_ai_builderguard_low" + + "hide_distance" "-1" // never + "dist_check_period" "0.7" +} From df126d9045745251f1e6e12810b966253bdbf5a7 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Wed, 10 Feb 2021 21:18:40 +0000 Subject: [PATCH 47/48] Entity::Attachment now provides joint information Pass the already-parsed joint value from AttachmentData to the Entity::Attachment functor parameter, and confirm this in the unit test of the builder_guard AI. --- include/ientity.h | 3 +++ radiantcore/entity/AttachmentData.h | 9 ++++++++- test/Entity.cpp | 3 +++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/include/ientity.h b/include/ientity.h index a37e55159c..1199affde7 100644 --- a/include/ientity.h +++ b/include/ientity.h @@ -245,6 +245,9 @@ class Entity /// Vector offset where the attached entity should appear Vector3 offset; + + /// Optional model joint to use as origin + std::string joint; }; /// A functor which can receive Attachment objects diff --git a/radiantcore/entity/AttachmentData.h b/radiantcore/entity/AttachmentData.h index c737402a98..39e79166e0 100644 --- a/radiantcore/entity/AttachmentData.h +++ b/radiantcore/entity/AttachmentData.h @@ -107,9 +107,16 @@ class AttachmentData { for (auto i = _objects.begin(); i != _objects.end(); ++i) { + // Locate attachment position + const AttachPos& pos = _positions.at(i->second.posName); + + // Construct the functor argument Entity::Attachment a; a.eclass = i->second.className; - a.offset = _positions.at(i->second.posName).origin; + a.offset = pos.origin; + a.joint = pos.joint; + + // Invoke the functor func(a); } } diff --git a/test/Entity.cpp b/test/Entity.cpp index 264b0a5ebc..0eceabd51f 100644 --- a/test/Entity.cpp +++ b/test/Entity.cpp @@ -680,6 +680,9 @@ TEST_F(EntityTest, CreateAIEntity) // Guard should have a hammer attachment auto attachments = getAttachments(guard); EXPECT_EQ(attachments.size(), 1); + EXPECT_EQ(attachments.front().eclass, "atdm:moveable_warhammer"); + EXPECT_EQ(attachments.front().offset, Vector3(14, -6, -6)); + EXPECT_EQ(attachments.front().joint, "Spine2"); } } \ No newline at end of file From 2e35762cbab4f7cb66a7934ae2423791c758d963 Mon Sep 17 00:00:00 2001 From: Matthew Mott Date: Thu, 11 Feb 2021 19:58:50 +0000 Subject: [PATCH 48/48] Skip rendering attachments at joint positions Since we aren't yet handling the joint location correctly, attachments to joints show up in weird positions (such as weapon appearing in front of an AI's feet). For now we just skip rendering attachments altogether if they have a "joint" parameter set. --- radiantcore/entity/EntityNode.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/radiantcore/entity/EntityNode.cpp b/radiantcore/entity/EntityNode.cpp index 281a67aa44..e1e844cd9e 100644 --- a/radiantcore/entity/EntityNode.cpp +++ b/radiantcore/entity/EntityNode.cpp @@ -122,6 +122,11 @@ void EntityNode::createAttachedEntities() _spawnArgs.forEachAttachment( [this](const Entity::Attachment& a) { + // Since we can't yet handle joint positions, ignore this attachment + // if it is attached to a joint + if (!a.joint.empty()) + return; + // Check this is a valid entity class auto cls = GlobalEntityClassManager().findClass(a.eclass); if (!cls)