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 diff --git a/debian/changelog b/debian/changelog index ba9d3b19bc..52e777614c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +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. + * 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..cd3566b7bf 100644 --- a/debian/control +++ b/debian/control @@ -2,8 +2,8 @@ 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 -Standards-Version: 4.1.5 +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 Architecture: any diff --git a/include/ieclass.h b/include/ieclass.h index 31e0f40246..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. @@ -228,9 +229,7 @@ class IEntityClass /* ENTITY CLASS SIZE */ - /** - * Query whether this entity has a fixed size. - */ + /// Query whether this entity has a fixed size. virtual bool isFixedSize() const = 0; /** @@ -251,16 +250,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 +296,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/include/ientity.h b/include/ientity.h index 4510f29eeb..1199affde7 100644 --- a/include/ientity.h +++ b/include/ientity.h @@ -5,8 +5,12 @@ #include "imodule.h" #include "irender.h" #include "inameobserver.h" +#include "iscenegraph.h" +#include "itransformnode.h" #include +#include "string/predicate.h" + class IEntityClass; typedef std::shared_ptr IEntityClassPtr; typedef std::shared_ptr IEntityClassConstPtr; @@ -104,9 +108,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 +118,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; @@ -158,12 +173,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 @@ -173,10 +188,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 @@ -205,16 +230,54 @@ 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; + + /* 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; + + /// Optional model joint to use as origin + std::string joint; + }; + + /// 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 -class IEntityNode : - public IRenderEntity, - public virtual scene::INode +/** + * \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() {} @@ -275,13 +338,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) @@ -363,7 +426,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/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/include/irender.h b/include/irender.h index cbd96eee1a..3da395b6ba 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 { @@ -158,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 @@ -171,18 +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 @@ -195,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 @@ -210,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 @@ -229,13 +214,13 @@ 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() << ")"; } /** - * \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. @@ -467,7 +452,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; @@ -573,7 +557,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/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/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/install/bitmaps/view_show_lightradii.png b/install/bitmaps/view_show_lightradii.png index 99533d139b..16560f7578 100644 Binary files a/install/bitmaps/view_show_lightradii.png and b/install/bitmaps/view_show_lightradii.png differ diff --git a/install/bitmaps/view_show_speakerradii.png b/install/bitmaps/view_show_speakerradii.png index 1a1ff1c97d..00a7dbc415 100644 Binary files a/install/bitmaps/view_show_speakerradii.png and b/install/bitmaps/view_show_speakerradii.png differ 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); 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); 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/CMakeLists.txt b/radiantcore/CMakeLists.txt index a0b102c408..56a43c06ac 100644 --- a/radiantcore/CMakeLists.txt +++ b/radiantcore/CMakeLists.txt @@ -28,18 +28,18 @@ 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 entity/curve/CurveNURBS.cpp - entity/Doom3Entity.cpp + entity/SpawnArgs.cpp entity/doom3group/Doom3Group.cpp entity/doom3group/Doom3GroupNode.cpp entity/eclassmodel/EclassModelNode.cpp 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/eclass/Doom3EntityClass.cpp b/radiantcore/eclass/Doom3EntityClass.cpp index 6873a132eb..5ceeedc842 100644 --- a/radiantcore/eclass/Doom3EntityClass.cpp +++ b/radiantcore/eclass/Doom3EntityClass.cpp @@ -11,189 +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; - } - } - } -}; - 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); @@ -215,7 +32,6 @@ Doom3EntityClass::Doom3EntityClass(const std::string& name, const vfs::FileInfo& _inheritanceResolved(false), _modName("base"), _emptyAttribute("", "", ""), - _attachments(new Attachments(name)), _parseStamp(0) {} @@ -237,8 +53,6 @@ sigc::signal& Doom3EntityClass::changedSignal() return _changedSignal; } -/** Query whether this entity has a fixed size. - */ bool Doom3EntityClass::isFixedSize() const { if (_fixedSize) { @@ -375,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); } } } @@ -478,11 +290,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 @@ -490,8 +298,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; } @@ -511,8 +318,6 @@ void Doom3EntityClass::clear() _inheritanceResolved = false; _modName = "base"; - - _attachments->clear(); } void Doom3EntityClass::parseEditorSpawnarg(const std::string& key, @@ -586,9 +391,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()) { @@ -611,8 +413,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 0e0f0872f2..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; @@ -203,7 +199,7 @@ class Doom3EntityClass /** * Return the mod name. */ - std::string getModName() const { + std::string getModName() const override { return _modName; } diff --git a/radiantcore/entity/AttachmentData.cpp b/radiantcore/entity/AttachmentData.cpp new file mode 100644 index 0000000000..42bf7d63e5 --- /dev/null +++ b/radiantcore/entity/AttachmentData.cpp @@ -0,0 +1,105 @@ +#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() << "[AttachmentData] Entity '" << _entityName + << "' 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..39e79166e0 --- /dev/null +++ b/radiantcore/entity/AttachmentData.h @@ -0,0 +1,125 @@ +#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 _entityName; + + // 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) + : _entityName(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) + { + // 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 = pos.origin; + a.joint = pos.joint; + + // Invoke the functor + func(a); + } + } +}; + +} \ No newline at end of file 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..e1e844cd9e 100644 --- a/radiantcore/entity/EntityNode.cpp +++ b/radiantcore/entity/EntityNode.cpp @@ -12,37 +12,38 @@ 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) -{} +{ +} EntityNode::EntityNode(const EntityNode& other) : IEntityNode(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), + _localToParent(other._localToParent), + _namespaceManager(_spawnArgs), + _nameKey(_spawnArgs), _renderableName(_nameKey), _modelKey(*this), - _keyObservers(_entity), + _keyObservers(_spawnArgs), _shaderParms(_keyObservers, _colourKey), _direction(1,0,0) -{} +{ +} EntityNode::~EntityNode() { @@ -69,6 +70,9 @@ void EntityNode::construct() addKeyObserver("skin", _skinKeyObserver); _shaderParms.addKeyObservers(); + + // Construct all attached entities + createAttachedEntities(); } void EntityNode::constructClone(const EntityNode& original) @@ -113,6 +117,52 @@ void EntityNode::destruct() TargetableNode::destruct(); } +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) + { + rWarning() << "EntityNode [" << _eclass->getName() + << "]: cannot attach non-existent entity class '" + << a.eclass << "'\n"; + return; + } + + // 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 + attachedEnt->localToParent() = Matrix4::getTranslation(a.offset); + } + ); +} + +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 @@ -134,7 +184,7 @@ void EntityNode::removeKeyObserver(const std::string& key, KeyObserver& observer Entity& EntityNode::getEntity() { - return _entity; + return _spawnArgs; } void EntityNode::refreshModel() @@ -201,7 +251,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 +264,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(); } @@ -257,9 +307,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, @@ -268,8 +322,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() @@ -281,8 +341,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 { @@ -299,6 +359,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 0cd082ccf5..a4622790db 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. */ @@ -32,17 +32,17 @@ class EntityNode : public SelectionTestable, 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 Transformable { protected: // The entity class IEntityClassPtr _eclass; // The actual entity (which contains the key/value pairs) - // TODO: Rename this to "spawnargs"? - Doom3Entity _entity; + 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; @@ -62,7 +62,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 @@ -77,7 +77,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); @@ -90,11 +103,16 @@ class EntityNode : // IEntityNode implementation Entity& getEntity() override; virtual void refreshModel() override; + void transformChanged() override; // RenderEntity implementation 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; @@ -168,6 +186,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/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 66% rename from radiantcore/entity/Doom3Entity.cpp rename to radiantcore/entity/SpawnArgs.cpp index 60c8aa2229..8d918d3798 100644 --- a/radiantcore/entity/Doom3Entity.cpp +++ b/radiantcore/entity/SpawnArgs.cpp @@ -1,37 +1,55 @@ -#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()) -{} + _isContainer(!eclass->isFixedSize()), + _attachments(eclass->getName()) +{ + // Parse attachment keys + parseAttachments(); +} -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) + _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() +{ + // 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 Doom3Entity::isModel() const +bool SpawnArgs::isModel() const { std::string name = getKeyValue("name"); std::string model = getKeyValue("model"); @@ -40,12 +58,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 +82,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 +96,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 +119,7 @@ void Doom3Entity::detachObserver(Observer* observer) } } -void Doom3Entity::connectUndoSystem(IMapFileChangeTracker& changeTracker) +void SpawnArgs::connectUndoSystem(IMapFileChangeTracker& changeTracker) { _instanced = true; @@ -113,7 +131,7 @@ void Doom3Entity::connectUndoSystem(IMapFileChangeTracker& changeTracker) _undo.connectUndoSystem(changeTracker); } -void Doom3Entity::disconnectUndoSystem(IMapFileChangeTracker& changeTracker) +void SpawnArgs::disconnectUndoSystem(IMapFileChangeTracker& changeTracker) { _undo.disconnectUndoSystem(changeTracker); @@ -125,22 +143,30 @@ void Doom3Entity::disconnectUndoSystem(IMapFileChangeTracker& changeTracker) _instanced = false; } -/** 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(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 Doom3Entity::forEachEntityKeyValue(const EntityKeyValueVisitFunctor& func) +void SpawnArgs::forEachEntityKeyValue(const EntityKeyValueVisitFunctor& func) { for (const KeyValuePair& pair : _keyValues) { @@ -148,9 +174,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()) { @@ -164,9 +188,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 +205,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,47 +214,34 @@ bool Doom3Entity::isInherited(const std::string& key) const return (!definedLocally && !_eclass->getAttribute(key).getValue().empty()); } -Entity::KeyValuePairs Doom3Entity::getKeyValuePairs(const std::string& prefix) const +void SpawnArgs::forEachAttachment(AttachmentFunc func) 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; + _attachments.forEachAttachment(func); } -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 +255,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 +269,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 +282,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 +299,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 +326,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 +347,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 +359,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 +375,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 68% rename from radiantcore/entity/Doom3Entity.h rename to radiantcore/entity/SpawnArgs.h index 79046c26e9..05fe0c10de 100644 --- a/radiantcore/entity/Doom3Entity.h +++ b/radiantcore/entity/SpawnArgs.h @@ -1,27 +1,24 @@ #pragma once +#include "AttachmentData.h" + #include #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; @@ -44,42 +41,31 @@ class Doom3Entity : bool _isContainer; + // Store attachment information + AttachmentData _attachments; + 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); /* 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 forEachKeyValue(KeyValueVisitFunc func, + bool includeInherited) 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; - - // Get all KeyValues matching the given prefix. - KeyValuePairs getKeyValuePairs(const std::string& prefix) const override; + void forEachAttachment(AttachmentFunc func) const override; bool isWorldspawn() const override; bool isContainer() const override; @@ -96,6 +82,10 @@ class Doom3Entity : 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); 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/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..6ed146b285 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(_spawnArgs.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(_spawnArgs.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 = _spawnArgs.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(_spawnArgs); } -// 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(_spawnArgs); + + if (_allow3Drotations) + { + m_rotationKey.m_rotation = m_rotation; + m_rotationKey.m_rotation.writeToEntity(&_spawnArgs); + } + else + { + m_angleKey.setValue(m_angle); + m_angleKey.write(&_spawnArgs); + } +} + +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..d845f46efe 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 "../SpawnArgs.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() override; + void freezeTransform() override; + 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 diff --git a/radiantcore/entity/light/Light.cpp b/radiantcore/entity/light/Light.cpp index 6517fdbeff..67ff5cb130 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) @@ -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))); @@ -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); } } @@ -629,15 +633,21 @@ 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; + // 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 + ); } } @@ -769,8 +779,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 +806,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 +883,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 7cc60fd147..3118b09d7d 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 @@ -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(); @@ -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); @@ -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; @@ -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/radiantcore/entity/light/LightNode.cpp b/radiantcore/entity/light/LightNode.cpp index d57a297984..d0e2082630 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))), @@ -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/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; 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(); 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/radiantcore/selection/RadiantSelectionSystem.cpp b/radiantcore/selection/RadiantSelectionSystem.cpp index 87763f8341..194f205a88 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) { @@ -751,7 +751,7 @@ void RadiantSelectionSystem::selectArea(SelectionTest& test, SelectionSystem::EM } // 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; @@ -866,7 +866,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); } @@ -986,7 +986,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; @@ -1020,10 +1020,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 }); @@ -1037,7 +1037,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 @@ -1051,14 +1051,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(); @@ -1259,7 +1276,7 @@ void RadiantSelectionSystem::toggleEntityMode(const cmd::ArgumentList& args) { activateDefaultMode(); } - else + else { SetMode(eEntity); SetComponentMode(eDefault); @@ -1295,7 +1312,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/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/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/Entity.cpp b/test/Entity.cpp new file mode 100644 index 0000000000..0eceabd51f --- /dev/null +++ b/test/Entity.cpp @@ -0,0 +1,688 @@ +#include "RadiantTest.h" + +#include "ieclass.h" +#include "ientity.h" +#include "irendersystemfactory.h" +#include "iselectable.h" +#include "iselection.h" +#include "ishaders.h" + +#include "render/NopVolumeTest.h" +#include "string/convert.h" +#include "transformlib.h" + +namespace test +{ + +using EntityTest = RadiantTest; + +namespace +{ + +// Create an entity from a simple classname string +IEntityNodePtr createByClassName(const std::string& className) +{ + auto cls = GlobalEntityClassManager().findClass(className); + return GlobalEntityModule().createEntity(cls); +} + +// 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; +} + +} + +using StringMap = std::map; + +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, LightEntitiesRecognisedAsLights) +{ + // 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()); + + // 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()); +} + +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 + auto attachments = getAttachments(light); + EXPECT_EQ(attachments.size(), 0); +} + +TEST_F(EntityTest, EnumerateEntitySpawnargs) +{ + auto light = createByClassName("light"); + auto& spawnArgs = light->getEntity(); + + // Visit spawnargs by key and value string + 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, 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, 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, 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(); + Entity* clonedEnt = Node_getEntity(lightCopy); + ASSERT_TRUE(clonedEnt); + + // 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, 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/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, + const IRenderEntity* entity = nullptr) override + { + ++renderables; + } + + void addLight(const RendererLight& light) + { + ++lights; + lightPtrs.push_back(&light); + } + + bool supportsFullMaterials() const override { return true; } + void setHighlightFlag(Highlight::Flags flags, bool enabled) override + {} + }; + + // 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. 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; + + // Whether to render solid or wireframe + const bool renderSolid; + + // Keep track of nodes visited + int nodesVisited = 0; + + // Construct + 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 + { + // Count the node itself + ++nodesVisited; + + // Render the node in appropriate mode + if (renderSolid) + node->renderSolid(collector, volumeTest); + else + node->renderWireframe(collector, volumeTest); + + // Continue traversing + return true; + } + }; +} + +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, 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, 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 + // 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"); + RenderFixture renderF; + + // 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); +} + +TEST_F(EntityTest, RenderSelectedLightEntity) +{ + auto light = createByClassName("light"); + RenderFixture renderF; + + // 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_torchflame_small"); + 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->getLightOrigin(), 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, RenderEmptyFuncStatic) +{ + auto funcStatic = createByClassName("func_static"); + + // Func static without a model key is empty + RenderFixture rf; + rf.renderSubGraph(funcStatic); + EXPECT_EQ(rf.nodesVisited, 1); + 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"); + + 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); +} + +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 + auto torch = createByClassName("atdm:torch_brazier"); + ASSERT_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); + + // 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)); +} + +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"); + + // We must render in solid mode to get the light source + RenderFixture rf(true /* solid mode */); + rf.renderSubGraph(torch); + + // 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()); +} + +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); +} + +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); +} + +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); + 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 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()); 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); 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.def b/test/resources/tdm/def/lights.def index 44df7c2cac..48cf88dfea 100644 --- a/test/resources/tdm/def/lights.def +++ b/test/resources/tdm/def/lights.def @@ -9,3 +9,127 @@ 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 +} + +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 diff --git a/test/resources/tdm/def/lights_static.def b/test/resources/tdm/def/lights_static.def new file mode 100644 index 0000000000..dbe3ecf9bc --- /dev/null +++ b/test/resources/tdm/def/lights_static.def @@ -0,0 +1,47 @@ +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/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" +} + 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" +} diff --git a/test/resources/tdm/models/torch.lwo b/test/resources/tdm/models/torch.lwo new file mode 100644 index 0000000000..759d5c971e Binary files /dev/null and b/test/resources/tdm/models/torch.lwo differ