LCOV - code coverage report
Current view: top level - engine/model - skelmodel.h (source / functions) Hit Total Coverage
Test: Libprimis Test Coverage Lines: 160 312 51.3 %
Date: 2024-11-22 05:07:59 Functions: 39 45 86.7 %

          Line data    Source code
       1             : #ifndef SKELMODEL_H_
       2             : #define SKELMODEL_H_
       3             : 
       4             : enum Bonemask
       5             : {
       6             :     Bonemask_Not  = 0x8000,
       7             :     Bonemask_End  = 0xFFFF,
       8             :     Bonemask_Bone = 0x7FFF
       9             : };
      10             : 
      11             : class skelhitdata; //defined in hitzone.h
      12             : 
      13             : /* skelmodel: implementation of model object for a skeletally rigged model
      14             :  *
      15             :  * skelmodel implements most of what is required to render a skeletally rigged
      16             :  * and animated model, using animmodel's specialization of model to implement
      17             :  * animations.
      18             :  *
      19             :  * extending skelmodel for a specific file format allows a program to use these
      20             :  * formats with skeletal animation support
      21             :  *
      22             :  * arrows indicate pointer fields pointing towards an object of the pointed type
      23             :  * arrows point to base or derived type specifically (which share a box)
      24             :  *
      25             :  *              /-------------------------------\
      26             :  *              | skelmodel : animmodel : model |
      27             :  *              \--------------|---Λ------------/
      28             :  *                             |   \_______________
      29             :  *                             |                   \   ____________
      30             :  *                             |                   |  /            \
      31             :  *                             |      /------------V--V-\     /----|-------\
      32             :  *                             |      | skelpart : part-------->linkedpart |
      33             :  *                             |      \-------------|-Λ-/     \------------/
      34             :  *                              \      _____________/ \___
      35             :  *                               |    /                   \
      36             :  *            /------------------V----V---\           /---V----\
      37             :  *            | skelmeshgroup : meshgroup |           |  skin  |
      38             :  *            \-|------|--|-Λ----------Λ--/           \--|---|-/
      39             :  *              |      |  | |          |                 |   |
      40             :  *  /-----------V-\    |  | |          |                 |   |
      41             :  *  |vbocacheentry|    |  | |          |                 |   |
      42             :  *  \-|-----------/    |  | |          |                 |   |
      43             :  *    |               /   | |          |                 |   |
      44             :  *    |               |   | |          |                 |   |
      45             :  *    | /-------------V-\ | |          |                 |   \_____
      46             :  *    | |blendcacheentry| | |          \_____________    |         \
      47             :  *    | \--|------------/ |  \                       \ /-V----\     |
      48             :  *    |    |             /    \                      | |shader|     |
      49             :  *    |    |            /      \                     | \------/ /---V---\
      50             :  *    |    |            |       |                    |          |texture|
      51             :  *    |    |   /--------V-\  /--V-----\ /------------V----\     \-------/
      52             :  *    |    |   |blendcombo|  |skeleton| | skelmesh : Mesh |
      53             :  *    |    |   \----------/  \-|---|--/ \--|----|---------/
      54             :  *    \____\_________          |   |       |    |
      55             :  *                   \         |   |       |    |
      56             :  * /----------------\ | /------V-\ |    /--V-\  |
      57             :  * |dynent : physent| | |boneinfo| |    |vert|  |
      58             :  * \---|------------/ | \--------/ |    \----/  |
      59             :  *     \____________  |           /             |
      60             :  *                  \ |          /            /-V-\
      61             :  *               /--V-V------\  |             |tri|
      62             :  *               |ragdolldata|  |             \---/
      63             :  *               \-|-|-----|-/  |
      64             :  *                 | |     |    |
      65             :  *            ____/  |  /--V----V---\
      66             :  *           /       |  |ragdollskel|
      67             :  *           |       |  \-----------/
      68             :  *         /-V-\  /--V-\
      69             :  *         |tri|  |vert|
      70             :  *         \---/  \----/
      71             :  */
      72             : struct skelmodel : animmodel
      73             : {
      74             :     struct vert final
      75             :     {
      76             :         vec pos, norm;
      77             :         vec2 tc;
      78             :         quat tangent;
      79             :         int blend, interpindex;
      80             :     };
      81             : 
      82             :     struct vvertg
      83             :     {
      84             :         vec4<half> pos;
      85             :         GenericVec2<half> tc;
      86             :         squat tangent;
      87             :     };
      88             : 
      89             :     struct vvertgw final : vvertg
      90             :     {
      91             :         uchar weights[4];
      92             :         uchar bones[4];
      93             :     };
      94             : 
      95             :     struct tri final
      96             :     {
      97             :         uint vert[3];
      98             :     };
      99             : 
     100             :     /**
     101             :      * @brief An object representing a set of weights for a vertex.
     102             :      *
     103             :      * A blendcombo object stores a set of weights, which when finalized should total
     104             :      * to a total weight quantity of 1. The weights are stored in descending order,
     105             :      * and should only assume to be normalized once finalize() is called.
     106             :      */
     107             :     class blendcombo final
     108             :     {
     109             :         public:
     110             :             int uses, interpindex;
     111             : 
     112             :             struct BoneData
     113             :             {
     114             :                 float weight;
     115             :                 uchar bone;
     116             :                 uchar interpbone;
     117             :             };
     118             :             std::array<BoneData, 4> bonedata;
     119             : 
     120             :             blendcombo();
     121             : 
     122             :             /**
     123             :              * @brief Compares two blendcombos' weights and bones.
     124             :              *
     125             :              * Returns true if all bone indices and weight values in the Bone Data
     126             :              * array match. Checks no other values in the BoneData or other fields in
     127             :              * the blendcombo object
     128             :              *
     129             :              * @param c the blendcombo to compare
     130             :              *
     131             :              * @return true if the bones and weights match, false otherwise
     132             :              */
     133             :             bool operator==(const blendcombo &c) const;
     134             : 
     135             :             /**
     136             :              * @brief Returns the number of assigned weights in the blendcomb object.
     137             :              *
     138             :              * Assumes that all weights assigned are in the order created by addweight().
     139             :              *
     140             :              * @return the number of weights assigned in the bonedata
     141             :              */
     142             :             size_t size() const;
     143             : 
     144             :             /**
     145             :              * @brief Returns whether the first blendcombo has more weights than the second
     146             :              *
     147             :              * Returns true if `x` has a weight set at an index which `y` does not (reading from
     148             :              * left to right). Does not compare the actual values of the weights.
     149             :              * If both blendcombos have the same number of weights, returns false
     150             :              *
     151             :              * @param x the first blendcombo to compare
     152             :              * @param y the second blendcombo to compare
     153             :              *
     154             :              * @return true if x has more weights than y set, false if equal or less weights
     155             :              */
     156             :             static bool sortcmp(const blendcombo &x, const blendcombo &y);
     157             : 
     158             :             /**
     159             :              * @brief Attempts to assign a weight to one of the bonedata slots.
     160             :              *
     161             :              * Attempts to add the passed weight/bone combination to this blendcombo.
     162             :              * If the weight passed is less than 1e-3, the weight will not be added
     163             :              * regardless of the status of the object.
     164             :              *
     165             :              * If a weight/bone combo with a weight larger than any of the existing
     166             :              * members of the object are stored, the smaller weights are shifted
     167             :              * (and the smallest one removed) to make space for it. The stored
     168             :              * blends are assumed to be stored in descending order, and if added,
     169             :              * the inserted object will also be inserted to preserve descending
     170             :              * order.
     171             :              *
     172             :              * The inserted object will only be inserted at a depth inside the
     173             :              * weights buffer as deep as `sorted`. If this occurs, the descending
     174             :              * order of the buffer may not be maintained, and future operations
     175             :              * depending on this may not function properly.
     176             :              *
     177             :              * The returned value sorted indicates the depth into the object at
     178             :              * which the object should attempt to add an element. If an element
     179             :              * is successfully added, and if the bone data is not filled, then
     180             :              * sorted is returned incremented by one. Otherwise, the same value
     181             :              * passed as sorted will be returned.
     182             :              *
     183             :              * @param sorted the depth to add a weight in this object
     184             :              * @param weight the weight to attempt to add
     185             :              * @param bone the corresponding bone index
     186             :              *
     187             :              * @return the resulting number of allocated weights
     188             :              */
     189             :             int addweight(int sorted, float weight, int bone);
     190             : 
     191             :             /**
     192             :              * @brief Normalizes elements in the bonedata array.
     193             :              * Normalizes the elements in the weights part of the bonedata array
     194             :              * (up to `output` number of bones to normalize).
     195             :              *
     196             :              * The normalization critera for the weight data is the condition where
     197             :              * the sum of all the weights (up to the number sorted) adds to 1.
     198             :              *
     199             :              * @param sorted number of elements to normalize (must be <= 4)
     200             :              */
     201             :             void finalize(int sorted);
     202             : 
     203             :             /**
     204             :              * @brief Assigns unsigned character values to a vvertgw using the data in the blendcombo object
     205             :              *
     206             :              * If interpindex >=0:
     207             :              *  Sets the zeroth weight to 255 (1.f) and the others to zero
     208             :              *  Sets all of the bone values to 2*interpindex
     209             :              *  Note that none of the blendcombo's saved weights/bones values impact this operation
     210             :              *
     211             :              * If interpindex <0:
     212             :              *  Sets the passed vvertgw's weight values using floating point values ranging from 0..1
     213             :              *  converted to an unsigned character value ranging from 0..255
     214             :              *
     215             :              *  While the sum of the weights is greater than 1 (255), for each nonzero weight, remove
     216             :              *  1/255 from that weight, until the sum of weights is 1
     217             :              *
     218             :              *  Otherwise, while the sum of the weights is less than 1 (255), for each weight < 1, add
     219             :              *  1/255 from that weight, until the sum of the weights is 1
     220             :              *
     221             :              *  Assigns the passed object's bones to be equal to two times this objects' respective interpbones index
     222             :              *
     223             :              * @param v the vvertgw object to set weight/bone values to
     224             :              */
     225             :             void serialize(skelmodel::vvertgw &v) const;
     226             : 
     227             :             /**
     228             :              * @brief Creates a dual quaternion representation from the bone data of a blendcombo.
     229             :              *
     230             :              * Accumulates the set of dual quaternions pointed to by the bonedata object, scaled
     231             :              * by their respective weights. The resulting dual quaternion should be normalized,
     232             :              * if the blendcombo was normalized (by calling finalize()).
     233             :              *
     234             :              * @param bdata an array of dualquats, to which the BoneData.interpbones field points to
     235             :              *
     236             :              * @return a dual quaternion created from the blendcombo object's bone data
     237             :              */
     238             :             dualquat blendbones(const dualquat *bdata) const;
     239             : 
     240             :             /**
     241             :              * @brief Assigns interpbones values to the specified bonedata.
     242             :              *
     243             :              * @param val value to set to the indicated bone
     244             :              * @param i the index of the bonedata to set
     245             :              */
     246             :             void setinterpbones(int val, size_t i);
     247             : 
     248             :             /**
     249             :              * @brief Gets the bone stored at index.
     250             :              *
     251             :              * Gets the bone index associated with one of the bonedata's stored
     252             :              * bones. This index represents an index of the skeleton's boneinfo array.
     253             :              *
     254             :              * @param index the index of the bonedata to access
     255             :              */
     256             :             int getbone(size_t index);
     257             : 
     258             :     };
     259             : 
     260             :     struct animcacheentry
     261             :     {
     262             :         std::array<AnimState, maxanimparts> as;
     263             :         float pitch;
     264             :         int millis;
     265             :         const std::vector<uchar> * partmask;
     266             :         const ragdolldata *ragdoll;
     267             : 
     268             :         animcacheentry();
     269             : 
     270             :         /**
     271             :          * @brief Returns whether two animcacheentries compare equal
     272             :          *
     273             :          * Checks that all AnimStates in the animcacheentry::as field compare equal, as
     274             :          * well as the pitch, partmask, and ragdoll fields.
     275             :          *
     276             :          * If there are ragdolls in both objects, checks that neither
     277             :          * object's timestamp is less than this object's lastmove timestamp
     278             :          *
     279             :          * @param c the animcacheentry to compare
     280             :          *
     281             :          * @return true if the animcacheentries compare equal
     282             :          */
     283             :         bool operator==(const animcacheentry &c) const;
     284             : 
     285             :         /**
     286             :          * @brief Returns the opposite of animcacheentry::operator==
     287             :          *
     288             :          * @param c the animcacheentry to compare
     289             :          *
     290             :          * @return true if the animcacheentries compare unequal
     291             :          */
     292             :         bool operator!=(const animcacheentry &c) const;
     293             :     };
     294             : 
     295             :     struct vbocacheentry final : animcacheentry
     296             :     {
     297             :         GLuint vbuf; //GL_ARRAY_BUFFER (gle::bindvbo)
     298             :         int owner;
     299             : 
     300             :         bool check() const;
     301             :         vbocacheentry();
     302             :     };
     303             : 
     304             :     struct skelcacheentry : animcacheentry
     305             :     {
     306             :         dualquat *bdata; //array of size numinterpbones
     307             :         int version; //caching version
     308             : 
     309             :         skelcacheentry();
     310             :         void nextversion();
     311             :     };
     312             : 
     313             :     struct blendcacheentry final : skelcacheentry
     314             :     {
     315             :         int owner;
     316             : 
     317             :         blendcacheentry();
     318             :     };
     319             : 
     320             :     struct skelmeshgroup;
     321             : 
     322             :     class skelmesh : public Mesh
     323             :     {
     324             :         public:
     325             :             skelmesh();
     326             : 
     327             :             /**
     328             :              * @brief Constructs a skelmesh object.
     329             :              *
     330             :              * @param name name of the underlying Mesh object
     331             :              * @param verts a heap-allocated array of vertices
     332             :              * @param numverts size of verts array
     333             :              * @param tris a heap-allocated array of tris
     334             :              * @param numtris size of tris array
     335             :              */
     336             :             skelmesh(std::string_view name, vert *verts, uint numverts, tri *tris, uint numtris, meshgroup *m);
     337             : 
     338             :             virtual ~skelmesh();
     339             : 
     340             :             int addblendcombo(const blendcombo &c);
     341             : 
     342             :             void smoothnorms(float limit = 0, bool areaweight = true);
     343             :             void buildnorms(bool areaweight = true);
     344             :             void calctangents(bool areaweight = true);
     345             :             void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m) const override final;
     346             :             void genBIH(BIH::mesh &m) const override final;
     347             :             void genshadowmesh(std::vector<triangle> &out, const matrix4x3 &m) const override final;
     348             :             //assignvert() functions are used externally in test code
     349             :             static void assignvert(vvertg &vv, const vert &v);
     350             :             static void assignvert(vvertgw &vv, const vert &v, const blendcombo &c);
     351             : 
     352             :             /*
     353             :              * these two genvbo() functions are used for different cases
     354             :              * of skelmodel rendering paths:
     355             :              *
     356             :              * genvbo(const std::vector<blendcombo>&, std::vector<GLuint>&, int, std::vector<vvertgw>) is for skeleton with animation frames
     357             :              * genvbo(std::vector<GLuint>&, int, std::vector<vvertg>&, int, int) is for no animation frames
     358             :              */
     359             :             int genvbo(const std::vector<blendcombo> &bcs, std::vector<GLuint> &idxs, int offset, std::vector<vvertgw> &vverts);
     360             :             int genvbo(std::vector<GLuint> &idxs, int offset, std::vector<vvertg> &vverts, int *htdata, int htlen);
     361             : 
     362             :             void setshader(Shader *s, bool usegpuskel, int vweights, int row) override final;
     363             :             void render() const;
     364             :             /**
     365             :              * @brief Assigns indices from the remap parameter to the object's verts
     366             :              *
     367             :              * Assigns the vector of remap blend indices to the verts array. Assumes
     368             :              * that the vector passed is at least as large as the verts array.
     369             :              *
     370             :              * @param remap a vector of new indices to assign
     371             :              */
     372             :             void remapverts(const std::vector<int> remap);
     373             :             /**
     374             :              * @brief Returns the number of verts represented by the object.
     375             :              *
     376             :              * This function is used by the testing code.
     377             :              *
     378             :              * @return the number of vertices represented
     379             :              */
     380             :             int vertcount() const;
     381             : 
     382             :             /**
     383             :              * @brief Returns the number of tris represented by the object.
     384             :              *
     385             :              * This function is used by the testing code.
     386             :              *
     387             :              * @return the number of triangles represented
     388             :              */
     389             :             int tricount() const;
     390             : 
     391             :             /**
     392             :              * @brief Returns a const reference to a vert object inside this skelmesh
     393             :              *
     394             :              * This function is intended for testing and not to be used in other parts
     395             :              * of the model code.
     396             :              *
     397             :              * @param index the index of the verts array to get
     398             :              *
     399             :              * @return a reference to a skelmodel::vert corresponding to the index
     400             :              */
     401             :             const vert &getvert(size_t index) const;
     402             : 
     403             :         protected:
     404             :             tri *tris;
     405             :             int numtris;
     406             :             vert *verts;
     407             :             int numverts;
     408             : 
     409             :         private:
     410             :             int maxweights;
     411             :             int voffset, eoffset, elen;
     412             :             GLuint minvert, maxvert;
     413             :     };
     414             : 
     415             :     struct skelanimspec final
     416             :     {
     417             :         std::string name;
     418             :         int frame, range;
     419             :     };
     420             : 
     421             :     class skeleton
     422             :     {
     423             :         public:
     424             :             size_t numbones;
     425             :             int numgpubones;
     426             :             size_t numframes;
     427             :             dualquat *framebones; //array of quats, size equal to anim frames * bones in model
     428             :             std::vector<skelanimspec> skelanims;
     429             :             ragdollskel *ragdoll; //optional ragdoll object if ragdoll is in effect
     430             : 
     431             :             struct pitchtarget final
     432             :             {
     433             :                 size_t bone; //an index in skeleton::bones
     434             :                 int frame, corrects, deps;
     435             :                 float pitchmin, pitchmax, deviated;
     436             :                 dualquat pose;
     437             :             };
     438             :             std::vector<pitchtarget> pitchtargets; //vector of pitch target objects, added to models via pitchtarget command
     439             : 
     440             :             struct pitchcorrect final
     441             :             {
     442             :                 int bone, parent;
     443             :                 size_t target; //an index in skeleton::pitchtargets vector
     444             :                 float pitchmin, pitchmax, pitchscale, pitchangle, pitchtotal;
     445             : 
     446             :                 pitchcorrect(int bone, size_t target, float pitchscale, float pitchmin, float pitchmax);
     447             :                 pitchcorrect();
     448             :             };
     449             :             std::vector<pitchcorrect> pitchcorrects; //vector pitch correct objects, added to models via pitchcorrect command
     450             : 
     451             :             std::vector<skelcacheentry> skelcache;
     452             : 
     453             :             skeleton(skelmeshgroup * const group);
     454             :             ~skeleton();
     455             : 
     456             :             /**
     457             :              * @brief Finds a skelanimspec in skeleton::skelanims
     458             :              *
     459             :              * Searches for the first skelanimspec in skeleton::skelanims with a
     460             :              * nonnull name field equalling the name passed. The first such entry
     461             :              * is returned by const pointer (or nullptr if none is found)
     462             :              *
     463             :              * @param name the skelanimspec::name to query
     464             :              *
     465             :              * @return pointer to element of skeleton::skelanims
     466             :              */
     467             :             const skelanimspec *findskelanim(std::string_view name) const;
     468             : 
     469             :             /**
     470             :              * @brief Adds a skelanimspec to the end of skelanims()
     471             :              *
     472             :              * @param name the name to set in the skelanimspec
     473             :              * @param numframes the number of frames to set in the skelanimspec
     474             :              * @param amimframes the number of animation frames to set in the skelanimspec
     475             :              *
     476             :              * @return a reference to the added skelanimspec
     477             :              */
     478             :             skelanimspec &addskelanim(std::string_view name, int numframes, int animframes);
     479             : 
     480             :             /**
     481             :              * @brief Returns the first bone index in skeleton::bones with matching name field
     482             :              *
     483             :              * @param name the name to search for
     484             :              *
     485             :              * @return the index in skeleton::bones if found, nullopt if not
     486             :              */
     487             :             std::optional<size_t> findbone(std::string_view name) const;
     488             : 
     489             :             /**
     490             :              * @brief Returns the first tag index in skeleton::tags with matching name field
     491             :              *
     492             :              * @param name the name to search for
     493             :              *
     494             :              * @return the index in skeleton::tags if found, nullopt if not
     495             :              */
     496             :             std::optional<size_t> findtag(std::string_view name) const;
     497             : 
     498             :             /**
     499             :              * @brief Modifies or sets a tag in the `skeleton::tags`
     500             :              *
     501             :              * If there is a tag in `skeleton::tags` with a matching name to `name`,
     502             :              * sets the value of `bone` and `matrix` in that object.
     503             :              *
     504             :              * If there is no such tag, creates a new one at the end of `skeleton:tags`
     505             :              * with `name`, `bone` and `matrix` set
     506             :              *
     507             :              * @param name name string to search for
     508             :              * @param bone bone value to set
     509             :              * @param matrix matrix value to set
     510             :              */
     511             :             bool addtag(std::string_view name, int bone, const matrix4x3 &matrix);
     512             : 
     513             :             /**
     514             :              * @brief Returns the first pitchcorrect index in skeleton::pitchcorrects with matching bone
     515             :              *
     516             :              * @param bone the bone to search for
     517             :              *
     518             :              * @return the index in skeleton::pitchcorrects if found, nullopt if not
     519             :              */
     520             :             std::optional<size_t> findpitchcorrect(int bone) const;
     521             :             void optimize();
     522             : 
     523             :             /**
     524             :              * @brief Applies bone mask values to the given partmask.
     525             :              *
     526             :              * No effect if mask is empty or starts with end enum value (Bonemask_End).
     527             :              * The partmask will not be changed in size, and is implied to be of size
     528             :              * `numbones`.
     529             :              * Sets `partindex` value to elements in `partmask` according to `expandbonemask()`,
     530             :              * applied to a copy of `mask`.
     531             :              * Partindex will be downcast from int -> unsigned char.
     532             :              *
     533             :              * @param mask vector of mask values to determine setting with, size numbones
     534             :              * @param partmask vector of values to conditionally set
     535             :              * @param partindex value to conditionally set to partmask
     536             :              */
     537             :             void applybonemask(const std::vector<uint> &mask, std::vector<uchar> &partmask, int partindex) const;
     538             : 
     539             :             /**
     540             :              * @brief Links this skeleton's children (boneinfo elements)
     541             :              *
     542             :              * Invalidates each element's child index and then resets it, if another boneinfo
     543             :              * read later (later implies position lower on tree) indicates it as a parent. The
     544             :              * value previously pointed to as the child in that parent object is set to the child's
     545             :              * `next` field.
     546             :              *
     547             :              * Invalidates the `next` element in the boneinfo's linkedlist if there is no valid parent
     548             :              * for a bone (is the top of the tree).
     549             :              */
     550             :             void linkchildren();
     551             : 
     552             :             /**
     553             :              * @brief Returns the number of bones available to be accelerated
     554             :              *
     555             :              * Returns the lesser of the `maxvsuniforms` (max vertex shader uniforms)
     556             :              * and `maxskelanimdata` (divided by two).
     557             :              *
     558             :              * @return number of bones that can be accelerated
     559             :              */
     560             :             static int availgpubones();
     561             : 
     562             :             /**
     563             :              * @brief Sets up the ragdolldata passed using the metadata of this skeleton.
     564             :              *
     565             :              * The ragdolldata, which is an object that acts as an instatiation of the ragdollskel
     566             :              * contained in skeleton->ragdoll, has its joints, animjoints, verts, and reljoints
     567             :              * set up by the ragdollskel associated with this model's skeleton.
     568             :              *
     569             :              * These values are modified by the array of dualquat transformations stored in the
     570             :              * skelcacheentry pointed to by `sc`.
     571             :              *
     572             :              * @param d the ragdolldata to set up
     573             :              * @param sc the location of the dualquat transformations to apply
     574             :              * @param scale scale factor for the vertex coordinates
     575             :              */
     576             :             void initragdoll(ragdolldata &d, const skelcacheentry &sc, float scale) const;
     577             : 
     578             :             /**
     579             :              * @brief Sets n to the product of m, the i'th bone's base matrix, and the i'th tag's matrix
     580             :              *
     581             :              * The contents of the matrix n have no effect on the resulting value stored in n.
     582             :              *
     583             :              * @param i the tag index in skeleton::tags to use
     584             :              * @param m the matrix to start the transform with
     585             :              * @param n the matrix to set
     586             :              */
     587             :             void concattagtransform(int i, const matrix4x3 &m, matrix4x3 &n) const;
     588             :             void calctags(part *p, const skelcacheentry *sc = nullptr) const;
     589             :             void cleanup(bool full = true);
     590             : 
     591             :             /**
     592             :              * @brief Gets a skelcacheentry from skeleton::skelcache
     593             :              *
     594             :              * Returns the first skelcacheentry matching the specified pitch, partmask (from `as` parent part),
     595             :              * ragdolldata. If no such element exists, creates one and adds it to the back of the skelcache,
     596             :              * modifying the ragdollbones and calling interpbones() to update the model's bones
     597             :              *
     598             :              * @param pos position to set in interpbones() if entry added
     599             :              * @param scale scale to add to new skelcache entry if added
     600             :              * @param as array of animstates, size numanimparts, from which metadata is queried
     601             :              * @param pitch pitch value to check against and conditionally set
     602             :              * @param axis value to pass to interpbones() if new entry added
     603             :              * @param forward value to pass to interpbones() if new entry added
     604             :              * @param rdata ragdoll data to check against and conditionally set
     605             :              *
     606             :              * @return the skelcache entry which was either found or added
     607             :              */
     608             :             const skelcacheentry &checkskelcache(const vec &pos, float scale, const AnimState *as, float pitch, const vec &axis, const vec &forward, const ragdolldata * const rdata);
     609             :             void setgpubones(const skelcacheentry &sc, const blendcacheentry *bc, int count);
     610             :             bool shouldcleanup() const;
     611             : 
     612             :             /**
     613             :              * @brief Sets the pitch information for the index'th bone in the skeleton's bones
     614             :              *
     615             :              * If no bone exists at `index` returns false; otherwise returns true
     616             :              * If no bone exists, only effect is to return false.
     617             :              *
     618             :              * @param index the index in `skeleton::bones`
     619             :              * @param scale scale factor to set
     620             :              * @param offset pitch offset value to set
     621             :              * @param min pitch minimum value to set
     622             :              * @param max pitch maximum value to set
     623             :              *
     624             :              * @return true if index within bounds, false if outside
     625             :              */
     626             :             bool setbonepitch(size_t index, float scale, float offset, float min, float max);
     627             : 
     628             :             /**
     629             :              * @brief Returns the dualquaternion base transform from the specified bone
     630             :              *
     631             :              * Returns the dualquat base field from skeleton::bones; if index is out
     632             :              * of bounds, returns nullopt.
     633             :              *
     634             :              * @param index index in skeleton::bones
     635             :              *
     636             :              * @return value of bone's dualquat base
     637             :              */
     638             :             std::optional<dualquat> getbonebase(size_t index) const;
     639             : 
     640             :             /**
     641             :              * @brief Assigns the vector of dual quaternion bases to skeleton:bones
     642             :              *
     643             :              * Assigns the vector of dual quaternion bases to the skeleton::bones
     644             :              * field. If skeleton::bones and bases are not the same length, returns
     645             :              * false and performs no operation; returns true otherwise.
     646             :              *
     647             :              * @param bases the vector of bases to assign
     648             :              */
     649             :             bool setbonebases(const std::vector<dualquat> &bases);
     650             : 
     651             :             /**
     652             :              * @brief Sets a boneinfo's name in skeleton::bones
     653             :              *
     654             :              * Only sets name if no name is present (null string). Does not apply
     655             :              * any effect if the index is out of bounds.
     656             :              *
     657             :              * @param index the element of skeleton::bones to modify
     658             :              * @param name the new name to set
     659             :              *
     660             :              * @return true if the name was set, false if a name already existed or invalid index
     661             :              */
     662             :             bool setbonename(size_t index, std::string_view name);
     663             : 
     664             :             /**
     665             :              * @brief Sets a boneinfo's parent in skeleton::bones
     666             :              *
     667             :              * Does not apply any effect if the index or parent is out of bounds
     668             :              * (if either value is larger than numbones)
     669             :              *
     670             :              * @param index the element of skeleton::bones to modify
     671             :              * @param name the new name to set
     672             :              *
     673             :              * @return true if the name was set, false if either value was an invalid index
     674             :              */
     675             :             bool setboneparent(size_t index, size_t parent);
     676             : 
     677             :             /**
     678             :              * @brief Creates a boneinfo array and assigns it to skeleton::bones
     679             :              *
     680             :              * Also sets the value of numbones to the size passed.
     681             :              * Will cause a memory leak if skeleton::bones is already heap-allocated.
     682             :              *
     683             :              * @param num the number of array elements in the new array
     684             :              */
     685             :             void createbones(size_t num);
     686             : 
     687             :             /**
     688             :              * @brief Creates a ragdoll if none is defined; returns the skeleton's ragdoll
     689             :              *
     690             :              * @return a pointer to this model's ragdoll
     691             :              */
     692             :             ragdollskel *trycreateragdoll();
     693             : 
     694             :         private:
     695             :             skelmeshgroup * const owner;
     696             :             size_t numinterpbones;
     697             : 
     698             :             struct boneinfo final
     699             :             {
     700             :                 std::string name;
     701             :                 int parent, //parent node in boneinfo
     702             :                     children, //first index of child bone list in boneinfo
     703             :                     next, //next adjacent sibling bone in boneinfo, last bone in sibling list has next = 0
     704             :                     group,
     705             :                     scheduled,
     706             :                     interpindex,
     707             :                     interpparent,
     708             :                     ragdollindex,
     709             :                     correctindex;
     710             :                 float pitchscale, pitchoffset, pitchmin, pitchmax;
     711             :                 dualquat base;
     712             : 
     713             :                 boneinfo();
     714             :             };
     715             :             /**
     716             :              * nodes in boneinfo tree, node relations in the tree are indicated
     717             :              * by boneinfo's fields
     718             :              *
     719             :              * n-leaf tree (nodes can have 0...INT_MAX children)
     720             :              *
     721             :              * size equal to numbones
     722             :              */
     723             :             boneinfo *bones;
     724             : 
     725             :             struct pitchdep
     726             :             {
     727             :                 int bone, parent;
     728             :                 dualquat pose;
     729             :             };
     730             :             std::vector<pitchdep> pitchdeps;
     731             : 
     732             :             struct antipode
     733             :             {
     734             :                 int parent, child;
     735             : 
     736           0 :                 antipode(int parent, int child) : parent(parent), child(child) {}
     737             :             };
     738             :             std::vector<antipode> antipodes;
     739             : 
     740             :             struct tag
     741             :             {
     742             :                 std::string name;
     743             :                 int bone;
     744             :                 matrix4x3 matrix;
     745             : 
     746           2 :                 tag(std::string_view name, int bone, matrix4x3 matrix) : name(name), bone(bone), matrix(matrix) {}
     747             :             };
     748             :             std::vector<tag> tags;
     749             : 
     750             :             /**
     751             :              * @brief Cache used by skeleton::getblendoffset() to cache glGetUniformLocation queries
     752             :              */
     753             :             std::unordered_map<GLuint, GLint> blendoffsets;
     754             : 
     755             :             /**
     756             :              * @brief Creates a new antipode array
     757             :              *
     758             :              * Clears the existing skeleton::antipode vector and adds new bones
     759             :              * corresponding to boneinfo objects from the skeleton::bones array.
     760             :              *
     761             :              * The number of antipodes in the created array is no larger than the
     762             :              * number of values in the skeleton::bones array (skeleton::numbones)
     763             :              * multiplied by the number of bones with their group field set to
     764             :              * a value larger than numbones.
     765             :              */
     766             :             void calcantipodes();
     767             :             void remapbones();
     768             : 
     769             :             struct framedata
     770             :             {
     771             :                 const dualquat *fr1, *fr2, //frame data
     772             :                                *pfr1, *pfr2; //part frame data
     773             :             };
     774             : 
     775             :             /**
     776             :              * @brief Gets the location of the uniform specified
     777             :              *
     778             :              * Helper function for setglslbones().
     779             :              *
     780             :              * Gets the uniform location of the uniform in `u`, at index
     781             :              * 2*`skeleton::numgpubones`. Adds the shader program in `shader::lastshader`
     782             :              * to `blendoffsets` if it is not already there.
     783             :              *
     784             :              * Once a shader program has been added to the `skeleton::blendoffsets`
     785             :              * map, further calls of this function while that shader program is
     786             :              * assigned to `shader::lastshader` will return the first uniformloc
     787             :              * location query value associated with that program, and the parameter
     788             :              * will be ignored.
     789             :              *
     790             :              * Returns the array element at 2*skeleton::numgpubones, skipping
     791             :              * the values used in `setglslbones()` to set the `sc` skelcacheentry values.
     792             :              *
     793             :              * @param u the uniformloc to query
     794             :              *
     795             :              * @return a GLint location of the uniform array at a position skipping the "sc" elements
     796             :              */
     797             :             GLint getblendoffset(const UniformLoc &u);
     798             : 
     799             :             /**
     800             :              * @brief Sets uniform values from skelcacheentries to the uniform at the specified UniformLoc
     801             :              *
     802             :              * Uses glUniform4fv to copy 4 dimensional quaternion values from the specified
     803             :              * skelcacheentries into the GL uniform array pointed to by the UniformLoc u.
     804             :              * The number of values copied from sc will be skeleton::numgpubones*2, and
     805             :              * the number of values copied from bc will be `count`. Only the real component
     806             :              * of the dual quaternions are copied.
     807             :              *
     808             :              * Sets the version and data values of the UniformLoc to that of the bc parameter,
     809             :              * to cache this operation only to occur when there is a mismatch between those
     810             :              * two objects.
     811             :              *
     812             :              * @param u the uniform location object to modify corresponding uniforms of
     813             :              * @param sc the skelcacheentry from which to set
     814             :              * @param bc the skelcacheentry from which to set the trailing values from
     815             :              * @param count the number of entries from bc to place in
     816             :              */
     817             :             void setglslbones(UniformLoc &u, const skelcacheentry &sc, const skelcacheentry &bc, int count);
     818             :             dualquat interpbone(int bone, const std::array<framedata, maxanimparts> &partframes, const AnimState *as, const uchar *partmask) const;
     819             :             void addpitchdep(int bone, int frame);
     820             :             static float calcdeviation(const vec &axis, const vec &forward, const dualquat &pose1, const dualquat &pose2);
     821             : 
     822             :             /**
     823             :              * @brief Searches for a pitchdep in the pitchdeps field
     824             :              *
     825             :              * Searches the pitchdeps vector field in ascending order. For each pitchdep
     826             :              * in the pitchdeps vector, if the bone stored in that pitchdep is at least as
     827             :              * high of an index (lower on the tree) then searching is stopped and the
     828             :              * function will either return that index (if the pitchdep's bone field exactly
     829             :              * matches) or nullopt if the bone at that pitchdep is lower on the tree.
     830             :              *
     831             :              * If the bone passed is larger than any bone data in any pitchep in the
     832             :              * pitchdeps vector, returns nullopt.
     833             :              *
     834             :              * @param bone the bone to search for
     835             :              *
     836             :              * @return nullopt if no such valid pitchdep exists
     837             :              * @return index of pitchdep if found
     838             :              */
     839             :             std::optional<size_t> findpitchdep(int bone) const;
     840             :             void initpitchdeps();
     841             : 
     842             :             /**
     843             :              * @brief Recursively applies the specified mask value to the bone mask array passed.
     844             :              *
     845             :              * The expansion array should be equal to the number of bones (which may be greater
     846             :              * than the bone parameter).
     847             :              *
     848             :              * Assigns the value val to the bone'th element in the expansion array, then calls
     849             :              * this function recursively for all children pointed to by the bone's index in
     850             :              * the boneinfo array `bones`.
     851             :              *
     852             :              * Applies the value val to children in boneinfo depth-first. All nodes traversed
     853             :              * will have the same value assigned (in the expansion array).
     854             :              *
     855             :              * @param expansion mask array to assign values to
     856             :              * @param bone the root bone to assign values to
     857             :              * @param val the value to set
     858             :              */
     859             :             void expandbonemask(uchar *expansion, int bone, int val) const;
     860             :             void calcpitchcorrects(float pitch, const vec &axis, const vec &forward);
     861             :             void interpbones(const AnimState *as, float pitch, const vec &axis, const vec &forward, int numanimparts, const uchar *partmask, skelcacheentry &sc);
     862             : 
     863             :             /**
     864             :              * @brief Sets up a skelcacheentry's bone transformations.
     865             :              *
     866             :              * Uses the model data at `d` and the translation/positon information passed
     867             :              * to set up the dual quaternion transformation array in the passed skelcacheentry.
     868             :              * Creates a new array of size `skeleton::numinterpbones` if no array exists, allocated
     869             :              * on the heap.
     870             :              *
     871             :              * @param d the ragdolldata to use vertex/tri information from
     872             :              * @param sc the skelcacheentry to set up
     873             :              * @param translate the position of the model
     874             :              * @param scale the scale factor of the model's vertices
     875             :              */
     876             :             void genragdollbones(const ragdolldata &d, skelcacheentry &sc, const vec &translate, float scale) const;
     877             : 
     878             :     };
     879             : 
     880             :     class skelmeshgroup : public meshgroup
     881             :     {
     882             :         public:
     883             :             skeleton *skel;
     884             : 
     885             :             std::vector<blendcombo> blendcombos;
     886             : 
     887             :             GLuint *edata;
     888             : 
     889           4 :             skelmeshgroup() : skel(nullptr), edata(nullptr), ebuf(0), vweights(0), vlen(0), vertsize(0), vblends(0), vdata(nullptr)
     890             :             {
     891           4 :                 numblends.fill(0);
     892           4 :             }
     893             : 
     894             :             virtual ~skelmeshgroup();
     895             : 
     896             :             std::optional<size_t> findtag(std::string_view) override final;
     897             : 
     898             :             /**
     899             :              * @brief Returns the skelmodel::skeleton object this skelmeshgroup points to.
     900             :              *
     901             :              * Returns the pointer to the skeleton object associated with this object.
     902             :              */
     903             :             void *animkey() override final;
     904             :             int totalframes() const override final;
     905             :             void concattagtransform(int i, const matrix4x3 &m, matrix4x3 &n) const override final;
     906             :             void preload() override final;
     907             :             void render(const AnimState *as, float pitch, const vec &axis, const vec &forward, dynent *d, part *p) override final;
     908             : 
     909             :             //for vvert, vvertg and vvertgw (also for vvertgw see below function),
     910             :             //disable bones if active
     911             :             //must have same const-qualification to properly interact with bindbones() below
     912             :             template<class T>
     913           0 :             void bindbones(const T *)
     914             :             {
     915           0 :                 if(enablebones)
     916             :                 {
     917           0 :                     disablebones();
     918             :                 }
     919           0 :             }
     920             : 
     921             :             /* this function is only called if `bindbones(vvertgw *)` is used to call it;
     922             :              * if you call bindbones<vvertgw>(), that will call the above template
     923             :              * (this function can be called if no <> specifier is provided, because
     924             :              * of partial ordering rules -- see C++20 N4849 13.10.2.4)
     925             :              */
     926             :             void bindbones(const vvertgw *vverts);
     927             : 
     928             :             template<class T>
     929           0 :             void bindvbo(const AnimState *as, const part *p, const vbocacheentry &vc)
     930             :             {
     931           0 :                 T *vverts = nullptr;
     932           0 :                 bindpos(ebuf, vc.vbuf, &vverts->pos, vertsize);
     933           0 :                 if(as->cur.anim & Anim_NoSkin)
     934             :                 {
     935           0 :                     if(enabletangents)
     936             :                     {
     937           0 :                         disabletangents();
     938             :                     }
     939           0 :                     if(p->alphatested())
     940             :                     {
     941           0 :                         bindtc(&vverts->tc, vertsize);
     942             :                     }
     943           0 :                     else if(enabletc)
     944             :                     {
     945           0 :                         disabletc();
     946             :                     }
     947             :                 }
     948             :                 else
     949             :                 {
     950           0 :                     bindtangents(&vverts->tangent, vertsize);
     951             : 
     952           0 :                     bindtc(&vverts->tc, vertsize);
     953             :                 }
     954           0 :                 bindbones(vverts);
     955           0 :             }
     956             : 
     957             :             void makeskeleton();
     958             :             /*
     959             :              * generates a vertex buffer object for an associated vbocache entry
     960             :              * the vbocacheentry passed will have its vbuf assigned to a GL buffer,
     961             :              * and if there is no ebuf (element array buffer) the following will
     962             :              * occur (summarized):
     963             :              *
     964             :              * - vweights will be set depending animation presence and gpuskel
     965             :              * - vlen will be set to the sum of all the encapsulated meshes' vertices
     966             :              * - vdata will be deleted and re-allocated as an array of size vlen*sizeof(vert object)
     967             :              * - vdata will be filled with values using fillverts() (which gets data from skelmesh::verts array)
     968             :              * - ebuf will be filled with data from skelmesh::genvbo, with ebuf size being equal to all of the respective meshes' tri counts summed
     969             :              */
     970             :             void genvbo(vbocacheentry &vc);
     971             :             void bindvbo(const AnimState *as, const part *p, const vbocacheentry &vc, const skelcacheentry *sc = nullptr);
     972             :             int addblendcombo(const blendcombo &c);
     973             :             //sorts the blendcombos by its comparison function, then applies this new order to associated skelmesh verts
     974             :             void sortblendcombos();
     975             :             void blendbones(const skelcacheentry &sc, blendcacheentry &bc);
     976             :             void cleanup() override final;
     977             : 
     978             :             virtual bool load(std::string_view meshfile, float smooth, part &p) = 0;
     979             :             virtual const skelanimspec *loadanim(const std::string &filename) = 0;
     980             :         private:
     981             :             std::array<int, 4> numblends;
     982             : 
     983             :             static constexpr size_t maxblendcache = 16; //number of entries in the blendcache entry array
     984             :             static constexpr size_t maxvbocache = 16;   //number of entries in the vertex buffer object array
     985             : 
     986             :             std::array<blendcacheentry, maxblendcache> blendcache;
     987             :             std::array<vbocacheentry, maxvbocache> vbocache;
     988             :             /*
     989             :              * ebuf, vbo variables are all initialized by genvbo(vbocacheentry), if render() has an ebuf
     990             :              * present then vbo variables will not be modified in render()
     991             :              */
     992             :             GLuint ebuf; //GL_ELEMENT_ARRAY_BUFFER gluint handle
     993             :             int vweights, //number of vbo weights, values 0...4
     994             :                 vlen, //sum of this skelmeshgroup's renderable meshes' vertex counts
     995             :                 vertsize, //sizeof vvert, if skeleton has animation frames & gpuskel; sizeof vvertgw if animation frames and no gpuskel, sizeof vvertg if neither
     996             :                 vblends; //number of blendcombos (= number of verts in e.g. md5)
     997             :             uchar *vdata; //vertex data drawn in the render() stage. It is filled by genvbo() and then used as a GL_ARRAY_BUFFER in the render() stage.
     998             : 
     999             :             blendcacheentry &checkblendcache(const skelcacheentry &sc, int owner);
    1000             :     };
    1001             : 
    1002             :     class skelpart : public part
    1003             :     {
    1004             :         public:
    1005             :             std::vector<uchar> partmask;
    1006             : 
    1007             :             skelpart(animmodel *model, int index = 0);
    1008             :             virtual ~skelpart();
    1009             : 
    1010             :             void initanimparts();
    1011             :             bool addanimpart(const std::vector<uint> &bonemask);
    1012             :             void loaded() override final;
    1013             :         private:
    1014             :             std::vector<uchar> buildingpartmask;
    1015             : 
    1016             :             std::vector<uchar> &sharepartmask(std::vector<uchar> &o);
    1017             :             std::vector<uchar> newpartmask();
    1018             :             void endanimparts();
    1019             :     };
    1020             : 
    1021             :     //ordinary methods
    1022             :     skelmodel(std::string name);
    1023             :     skelpart &addpart();
    1024             :     meshgroup *loadmeshes(const std::string &name, float smooth = 2);
    1025             :     meshgroup *sharemeshes(const std::string &name, float smooth = 2);
    1026             :     //virtual methods
    1027             :     virtual skelmeshgroup *newmeshes() = 0;
    1028             :     //override methods
    1029             : 
    1030             :     /* Returns the link type of an animmodel relative to a part
    1031             :      *
    1032             :      * If `this` model's zeroth part's mesh's skel is the same as the passed part's
    1033             :      * mesh's skel, returns Link_Reuse
    1034             :      * If the passed model parameter is not linkable, or does not meet the criteria above,
    1035             :      * returns Link_Tag.
    1036             :      *
    1037             :      * m *must* point to a valid animmodel object
    1038             :      * p *must* point to a valid part object which points to a valid skeleton.
    1039             :      */
    1040             :     int linktype(const animmodel *m, const part *p) const override final;
    1041             :     bool skeletal() const override final;
    1042             : 
    1043             : };
    1044             : 
    1045             : class skeladjustment final
    1046             : {
    1047             :     public:
    1048           0 :         skeladjustment(float yaw, float pitch, float roll, const vec &translate) : yaw(yaw), pitch(pitch), roll(roll), translate(translate) {}
    1049             :         void adjust(dualquat &dq) const;
    1050             : 
    1051             :     private:
    1052             :         float yaw, pitch, roll;
    1053             :         vec translate;
    1054             : };
    1055             : 
    1056             : template<class MDL>
    1057             : struct skelloader : modelloader<MDL, skelmodel>
    1058             : {
    1059             :     static std::vector<skeladjustment> adjustments;
    1060             :     static std::vector<uchar> hitzones;
    1061             : 
    1062          19 :     skelloader(std::string name) : modelloader<MDL, skelmodel>(name) {}
    1063             : };
    1064             : 
    1065             : template<class MDL>
    1066             : std::vector<skeladjustment> skelloader<MDL>::adjustments;
    1067             : 
    1068             : template<class MDL>
    1069             : std::vector<uchar> skelloader<MDL>::hitzones;
    1070             : 
    1071             : /*
    1072             :  * this template structure defines a series of commands for a model object (or
    1073             :  * child of the model object) which can be used to set its dynamically modifiable
    1074             :  * properties
    1075             :  *
    1076             :  */
    1077             : template<class MDL>
    1078             : struct skelcommands : modelcommands<MDL>
    1079             : {
    1080             :     typedef modelcommands<MDL> commands;
    1081             :     typedef class  MDL::skeleton skeleton;
    1082             :     typedef struct MDL::skelmeshgroup meshgroup;
    1083             :     typedef class  MDL::skelpart part;
    1084             :     typedef struct MDL::skelanimspec animspec;
    1085             :     typedef struct MDL::skeleton::pitchtarget pitchtarget;
    1086             :     typedef struct MDL::skeleton::pitchcorrect pitchcorrect;
    1087             : 
    1088             :     //unused second param
    1089          13 :     static void loadpart(const char *meshfile, const char *, const float *smooth)
    1090             :     {
    1091          13 :         if(!MDL::loading)
    1092             :         {
    1093           2 :             conoutf("not loading an %s", MDL::formatname());
    1094           2 :             return;
    1095             :         }
    1096          11 :         std::string filename;
    1097          11 :         filename.append(MDL::dir).append("/").append(meshfile);
    1098          11 :         part &mdl = MDL::loading->addpart();
    1099          11 :         mdl.meshes = MDL::loading->sharemeshes(path(filename), *smooth > 0 ? std::cos(std::clamp(*smooth, 0.0f, 180.0f)/RAD) : 2);
    1100          11 :         if(!mdl.meshes)
    1101             :         {
    1102           0 :             conoutf("could not load %s", filename.c_str());
    1103             :         }
    1104             :         else
    1105             :         {
    1106          11 :             if(mdl.meshes && static_cast<meshgroup *>(mdl.meshes)->skel->numbones > 0)
    1107             :             {
    1108          11 :                 mdl.disablepitch();
    1109             :             }
    1110          11 :             mdl.initanimparts();
    1111          11 :             mdl.initskins();
    1112             :         }
    1113          11 :     }
    1114             : 
    1115             :     /**
    1116             :      * @brief Adds a tag corresponding to a bone
    1117             :      *
    1118             :      * @param name the name of the bone in the skeletal model
    1119             :      * @param name the new name to assign the associated tag
    1120             :      *
    1121             :      * @param tx/ty/tz translation parameters
    1122             :      * @param rx/ry/rz rotation parameters
    1123             :      */
    1124           4 :     static void settag(const char *name, const char *tagname,
    1125             :                        const float *tx, const float *ty, const float *tz,
    1126             :                        const float *rx, const float *ry, const float *rz)
    1127             :     {
    1128           4 :         if(!MDL::loading || MDL::loading->parts.empty())
    1129             :         {
    1130           2 :             conoutf("not loading an %s", MDL::formatname());
    1131           4 :             return;
    1132             :         }
    1133           2 :         part &mdl = *static_cast<part *>(MDL::loading->parts.back());
    1134           2 :         std::optional<size_t> i = mdl.meshes ? static_cast<meshgroup *>(mdl.meshes)->skel->findbone(name) : std::nullopt;
    1135           2 :         if(i)
    1136             :         {
    1137           2 :             float cx = *rx ? std::cos(*rx/(2*RAD)) : 1, sx = *rx ? std::sin(*rx/(2*RAD)) : 0,
    1138           2 :                   cy = *ry ? std::cos(*ry/(2*RAD)) : 1, sy = *ry ? std::sin(*ry/(2*RAD)) : 0,
    1139           2 :                   cz = *rz ? std::cos(*rz/(2*RAD)) : 1, sz = *rz ? std::sin(*rz/(2*RAD)) : 0;
    1140           2 :             matrix4x3 m(matrix3(quat(sx*cy*cz - cx*sy*sz, cx*sy*cz + sx*cy*sz, cx*cy*sz - sx*sy*cz, cx*cy*cz + sx*sy*sz)),
    1141           2 :                         vec(*tx, *ty, *tz));
    1142           2 :             static_cast<meshgroup *>(mdl.meshes)->skel->addtag(tagname, *i, m);
    1143           2 :             return;
    1144             :         }
    1145           0 :         conoutf("could not find bone %s for tag %s", name, tagname);
    1146             :     }
    1147             : 
    1148             :     //attempts to set the pitch of a named bone within a MDL object, within the bounds set
    1149             :     //prints to console failure messages if no model or no bone with name passed
    1150           2 :     static void setpitch(const char *name, const float *pitchscale,
    1151             :                          const float *pitchoffset, const float *pitchmin, const float *pitchmax)
    1152             :     {
    1153           2 :         if(!MDL::loading || MDL::loading->parts.empty())
    1154             :         {
    1155           2 :             conoutf("not loading an %s", MDL::formatname());
    1156           2 :             return;
    1157             :         }
    1158           0 :         part &mdl = *static_cast<part *>(MDL::loading->parts.back());
    1159             : 
    1160           0 :         if(name[0])
    1161             :         {
    1162           0 :             std::optional<size_t> i = mdl.meshes ? static_cast<meshgroup *>(mdl.meshes)->skel->findbone(name) : std::nullopt;
    1163           0 :             if(i)
    1164             :             {
    1165           0 :                 float newpitchmin = 0.f,
    1166           0 :                       newpitchmax = 0.f;
    1167           0 :                 if(*pitchmin || *pitchmax)
    1168             :                 {
    1169           0 :                     newpitchmin = *pitchmin;
    1170           0 :                     newpitchmax = *pitchmax;
    1171             :                 }
    1172             :                 else
    1173             :                 {
    1174           0 :                     newpitchmin = -360*std::fabs(*pitchscale) + *pitchoffset;
    1175           0 :                     newpitchmax = 360*std::fabs(*pitchscale) + *pitchoffset;
    1176             :                 }
    1177           0 :                 static_cast<meshgroup *>(mdl.meshes)->skel->setbonepitch(*i, *pitchscale, *pitchoffset, newpitchmin, newpitchmax);
    1178           0 :                 return;
    1179             :             }
    1180           0 :             conoutf("could not find bone %s to pitch", name);
    1181           0 :             return;
    1182             :         }
    1183             : 
    1184           0 :         mdl.pitchscale = *pitchscale;
    1185           0 :         mdl.pitchoffset = *pitchoffset;
    1186           0 :         if(*pitchmin || *pitchmax)
    1187             :         {
    1188           0 :             mdl.pitchmin = *pitchmin;
    1189           0 :             mdl.pitchmax = *pitchmax;
    1190             :         }
    1191             :         else
    1192             :         {
    1193           0 :             mdl.pitchmin = -360*std::fabs(mdl.pitchscale) + mdl.pitchoffset;
    1194           0 :             mdl.pitchmax = 360*std::fabs(mdl.pitchscale) + mdl.pitchoffset;
    1195             :         }
    1196             :     }
    1197             : 
    1198           3 :     static void setpitchtarget(const char *name, const char *animfile, const int *frameoffset,
    1199             :                                const float *pitchmin, const float *pitchmax)
    1200             :     {
    1201           3 :         if(!MDL::loading || MDL::loading->parts.empty())
    1202             :         {
    1203           2 :             conoutf("not loading an %s", MDL::formatname());
    1204           2 :             return;
    1205             :         }
    1206           1 :         part &mdl = *static_cast<part *>(MDL::loading->parts.back());
    1207           1 :         if(!mdl.meshes)
    1208             :         {
    1209           0 :             return;
    1210             :         }
    1211           1 :         DEF_FORMAT_STRING(filename, "%s/%s", MDL::dir.c_str(), animfile);
    1212           1 :         const animspec *sa = static_cast<meshgroup *>(mdl.meshes)->loadanim(path(filename));
    1213           1 :         if(!sa)
    1214             :         {
    1215           0 :             conoutf("could not load %s anim file %s", MDL::formatname(), filename);
    1216           0 :             return;
    1217             :         }
    1218           1 :         skeleton *skel = static_cast<meshgroup *>(mdl.meshes)->skel;
    1219           1 :         std::optional<size_t> bone = skel ? skel->findbone(name) : std::nullopt;
    1220           1 :         if(!bone)
    1221             :         {
    1222           0 :             conoutf("could not find bone %s to pitch target", name);
    1223           0 :             return;
    1224             :         }
    1225           1 :         for(const pitchtarget &i : skel->pitchtargets)
    1226             :         {
    1227           0 :             if(i.bone == *bone)
    1228             :             {
    1229           0 :                 return;
    1230             :             }
    1231             :         }
    1232           1 :         pitchtarget t;
    1233           1 :         t.bone = *bone;
    1234           1 :         t.frame = sa->frame + std::clamp(*frameoffset, 0, sa->range-1);
    1235           1 :         t.pitchmin = *pitchmin;
    1236           1 :         t.pitchmax = *pitchmax;
    1237           1 :         skel->pitchtargets.push_back(t);
    1238             :     }
    1239             : 
    1240             :     /**
    1241             :      * @brief Adds a pitch correction to this model's skeleton
    1242             :      *
    1243             :      * @param name the name of the bone to pitch correct
    1244             :      * @param targetname the name of the bone to target
    1245             :      * @param scale the scale to apply to the pitchcorrect
    1246             :      * @param pitchmin the minimum pitch to apply
    1247             :      * @param pitchmax the maximum pitch to apply
    1248             :      */
    1249           2 :     static void setpitchcorrect(const char *name, const char *targetname,
    1250             :                                 const float *scale, const float *pitchmin, const float *pitchmax)
    1251             :     {
    1252           2 :         if(!MDL::loading || MDL::loading->parts.empty())
    1253             :         {
    1254           2 :             conoutf("not loading an %s", MDL::formatname());
    1255           2 :             return;
    1256             :         }
    1257           0 :         part &mdl = *static_cast<part *>(MDL::loading->parts.back());
    1258           0 :         if(!mdl.meshes)
    1259             :         {
    1260           0 :             return;
    1261             :         }
    1262           0 :         skeleton *skel = static_cast<meshgroup *>(mdl.meshes)->skel;
    1263           0 :         std::optional<int> bone = skel ? skel->findbone(name) : std::nullopt;
    1264           0 :         if(!bone)
    1265             :         {
    1266           0 :             conoutf("could not find bone %s to pitch correct", name);
    1267           0 :             return;
    1268             :         }
    1269           0 :         if(skel->findpitchcorrect(*bone) >= 0)
    1270             :         {
    1271           0 :             return;
    1272             :         }
    1273           0 :         std::optional<size_t> targetbone = skel->findbone(targetname),
    1274           0 :                               target = std::nullopt;
    1275           0 :         if(targetbone)
    1276             :         {
    1277           0 :             for(size_t i = 0; i < skel->pitchtargets.size(); i++)
    1278             :             {
    1279           0 :                 if(skel->pitchtargets[i].bone == *targetbone)
    1280             :                 {
    1281           0 :                     target = i;
    1282           0 :                     break;
    1283             :                 }
    1284             :             }
    1285             :         }
    1286           0 :         if(!target)
    1287             :         {
    1288           0 :             conoutf("could not find pitch target %s to pitch correct %s", targetname, name);
    1289           0 :             return;
    1290             :         }
    1291           0 :         pitchcorrect c(*bone, *target, *pitchmin, *pitchmax, *scale);
    1292           0 :         size_t pos = skel->pitchcorrects.size();
    1293           0 :         for(size_t i = 0; i < skel->pitchcorrects.size(); i++)
    1294             :         {
    1295           0 :             if(bone <= skel->pitchcorrects[i].bone)
    1296             :             {
    1297           0 :                 pos = i;
    1298           0 :                 break;
    1299             :             }
    1300             :         }
    1301           0 :         skel->pitchcorrects.insert(skel->pitchcorrects.begin() + pos, c);
    1302             :     }
    1303             : 
    1304             :     /**
    1305             :      * @param Assigns an animation to the currently loaded model.
    1306             :      *
    1307             :      * Attempts to give a model object an animation by the name of anim parameter
    1308             :      * loaded from animfile with speed/priority/offsets to determine how fast
    1309             :      * and what frames play.
    1310             :      *
    1311             :      * The name of the anim being loaded (anim param) must be in the global animnames vector.
    1312             :      *
    1313             :      * The MDL::loading static field must be set by calling startload() for the
    1314             :      * relevant model object.
    1315             :      *
    1316             :      * The animation will be applied to the most recent loaded part (with loadpart()).
    1317             :      */
    1318           4 :     static void setanim(const char *anim, const char *animfile, const float *speed,
    1319             :                         const int *priority, const int *startoffset, const int *endoffset)
    1320             :     {
    1321           4 :         if(!MDL::loading || MDL::loading->parts.empty())
    1322             :         {
    1323           2 :             conoutf("not loading an %s", MDL::formatname());
    1324           2 :             return;
    1325             :         }
    1326           2 :         std::vector<size_t> anims = findanims(anim);
    1327           2 :         if(anims.empty())
    1328             :         {
    1329           0 :             conoutf("could not find animation %s", anim);
    1330             :         }
    1331             :         else
    1332             :         {
    1333           2 :             part *p = static_cast<part *>(MDL::loading->parts.back());
    1334           2 :             if(!p->meshes)
    1335             :             {
    1336           0 :                 return;
    1337             :             }
    1338           2 :             DEF_FORMAT_STRING(filename, "%s/%s", MDL::dir.c_str(), animfile);
    1339           2 :             const animspec *sa = static_cast<meshgroup *>(p->meshes)->loadanim(path(filename));
    1340           2 :             if(!sa)
    1341             :             {
    1342           0 :                 conoutf("could not load %s anim file %s", MDL::formatname(), filename);
    1343             :             }
    1344             :             else
    1345             :             {
    1346           4 :                 for(size_t i = 0; i < anims.size(); i++)
    1347             :                 {
    1348           2 :                     int start = sa->frame,
    1349           2 :                         end = sa->range;
    1350           2 :                     if(*startoffset > 0)
    1351             :                     {
    1352           0 :                         start += std::min(*startoffset, end-1);
    1353             :                     }
    1354           2 :                     else if(*startoffset < 0)
    1355             :                     {
    1356           0 :                         start += std::max(end + *startoffset, 0);
    1357             :                     }
    1358           2 :                     end -= start - sa->frame;
    1359           2 :                     if(*endoffset > 0)
    1360             :                     {
    1361           0 :                         end = std::min(end, *endoffset);
    1362             :                     }
    1363           2 :                     else if(*endoffset < 0)
    1364             :                     {
    1365           0 :                         end = std::max(end + *endoffset, 1);
    1366             :                     }
    1367           2 :                     MDL::loading->parts.back()->setanim(p->numanimparts-1, anims[i], start, end, *speed, *priority);
    1368             :                 }
    1369             :             }
    1370             :         }
    1371           2 :     }
    1372             : 
    1373             :     /**
    1374             :      * @brief Assigns a subtree of bones to a bone mask.
    1375             :      *
    1376             :      * This bone mask is used to separate a part into two (and no more than two)
    1377             :      * distinct groups of subtrees which can be animated independently.
    1378             :      *
    1379             :      * These bones which make up subtree(s) under the specified bones are saved
    1380             :      * in the partmask vector of the relevant skelpart.
    1381             :      *
    1382             :      */
    1383           3 :     static void setanimpart(const char *maskstr)
    1384             :     {
    1385           3 :         if(!MDL::loading || MDL::loading->parts.empty())
    1386             :         {
    1387           2 :             conoutf("not loading an %s", MDL::formatname());
    1388           2 :             return;
    1389             :         }
    1390           1 :         part *p = static_cast<part *>(MDL::loading->parts.back());
    1391             : 
    1392           1 :         std::vector<std::string> bonestrs;
    1393           1 :         explodelist(maskstr, bonestrs);
    1394           1 :         std::vector<uint> bonemask;
    1395           2 :         for(uint i = 0; i < bonestrs.size(); i++)
    1396             :         {
    1397           1 :             const std::string &bonestr = bonestrs[i];
    1398           1 :             std::optional<int> bone = p->meshes ? static_cast<meshgroup *>(p->meshes)->skel->findbone(bonestr[0]=='!' ? bonestr.substr(1) : bonestr) : std::nullopt;
    1399           1 :             if(!bone)
    1400             :             {
    1401           0 :                 conoutf("could not find bone %s for anim part mask [%s]", bonestr.c_str(), maskstr);
    1402           0 :                 return;
    1403             :             }
    1404           1 :             bonemask.push_back(*bone | (bonestr[0]=='!' ? Bonemask_Not : 0));
    1405             :         }
    1406           1 :         std::sort(bonemask.begin(), bonemask.end());
    1407           1 :         if(bonemask.size())
    1408             :         {
    1409           1 :             bonemask.push_back(Bonemask_End);
    1410             :         }
    1411           1 :         if(!p->addanimpart(bonemask))
    1412             :         {
    1413           0 :             conoutf("too many animation parts");
    1414             :         }
    1415           1 :     }
    1416             : 
    1417           2 :     static void setadjust(const char *name, const float *yaw, const float *pitch, const float *roll,
    1418             :                                             const float *tx,  const float *ty,    const float *tz)
    1419             :     {
    1420           2 :         if(!MDL::loading || MDL::loading->parts.empty())
    1421             :         {
    1422           2 :             conoutf("not loading an %s", MDL::formatname());
    1423           2 :             return;
    1424             :         }
    1425           0 :         part &mdl = *static_cast<part *>(MDL::loading->parts.back());
    1426           0 :         if(!name[0])
    1427             :         {
    1428           0 :             return;
    1429             :         }
    1430           0 :         std::optional<int> i = mdl.meshes ? static_cast<meshgroup *>(mdl.meshes)->skel->findbone(name) : std::nullopt;
    1431           0 :         if(!i)
    1432             :         {
    1433           0 :             conoutf("could not find bone %s to adjust", name);
    1434           0 :             return;
    1435             :         }
    1436           0 :         while(!(static_cast<int>(MDL::adjustments.size()) > *i))
    1437             :         {
    1438           0 :             MDL::adjustments.push_back(skeladjustment(0, 0, 0, vec(0, 0, 0)));
    1439             :         }
    1440           0 :         MDL::adjustments[*i] = skeladjustment(*yaw, *pitch, *roll, vec(*tx/4, *ty/4, *tz/4));
    1441             :     }
    1442             : 
    1443           2 :     static void sethitzone(const int *id, const char *maskstr)
    1444             :     {
    1445           2 :         if(!MDL::loading || MDL::loading->parts.empty())
    1446             :         {
    1447           2 :             conoutf("not loading an %s", MDL::formatname());
    1448           2 :             return;
    1449             :         }
    1450           0 :         if(*id >= 0x80)
    1451             :         {
    1452           0 :             conoutf("invalid hit zone id %d", *id);
    1453           0 :             return;
    1454             :         }
    1455           0 :         part *p = static_cast<part *>(MDL::loading->parts.back());
    1456           0 :         meshgroup *m = static_cast<meshgroup *>(p->meshes);
    1457           0 :         if(!m)
    1458             :         {
    1459           0 :             return;
    1460             :         }
    1461           0 :         std::vector<std::string> bonestrs;
    1462           0 :         explodelist(maskstr, bonestrs);
    1463           0 :         std::vector<uint> bonemask;
    1464           0 :         for(uint i = 0; i < bonestrs.size(); i++)
    1465             :         {
    1466           0 :             const std::string &bonestr = bonestrs[i];
    1467           0 :             std::optional<int> bone = p->meshes ? static_cast<meshgroup *>(p->meshes)->skel->findbone(bonestr[0]=='!' ? bonestr.substr(1) : bonestr) : std::nullopt;
    1468           0 :             if(!bone)
    1469             :             {
    1470           0 :                 conoutf("could not find bone %s for hit zone mask [%s]", bonestr.c_str(), maskstr);
    1471           0 :                 return;
    1472             :             }
    1473           0 :             bonemask.push_back(*bone | (bonestr[0]=='!' ? Bonemask_Not : 0));
    1474             :         }
    1475           0 :         if(bonemask.empty())
    1476             :         {
    1477           0 :             return;
    1478             :         }
    1479           0 :         std::sort(bonemask.begin(), bonemask.end());
    1480           0 :         bonemask.push_back(Bonemask_End);
    1481             : 
    1482           0 :         while(MDL::hitzones.size() < m->skel->numbones)
    1483             :         {
    1484           0 :             MDL::hitzones.emplace_back(0xFF);
    1485             :         }
    1486           0 :         m->skel->applybonemask(bonemask, MDL::hitzones, *id < 0 ? 0xFF : *id);
    1487           0 :     }
    1488             : 
    1489             :     // if a skeletal model is being loaded, and meets the criteria for a ragdoll,
    1490             :     // returns the pointer to that ragdollskel (new one made if necessary), returns nullptr otherwise
    1491          14 :     static ragdollskel *checkragdoll()
    1492             :     {
    1493          14 :         if(!MDL::loading)
    1494             :         {
    1495          14 :             conoutf(Console_Error, "not loading a model");
    1496          14 :             return nullptr;
    1497             :         }
    1498           0 :         if(!MDL::loading->skeletal())
    1499             :         {
    1500           0 :             conoutf(Console_Error, "not loading a skeletal model");
    1501           0 :             return nullptr;
    1502             :         }
    1503           0 :         skelmodel *m = static_cast<skelmodel *>(MDL::loading);
    1504           0 :         if(m->parts.empty())
    1505             :         {
    1506           0 :             return nullptr;
    1507             :         }
    1508           0 :         skelmodel::skelmeshgroup *meshes = static_cast<skelmodel::skelmeshgroup *>(m->parts.back()->meshes);
    1509           0 :         if(!meshes)
    1510             :         {
    1511           0 :             return nullptr;
    1512             :         }
    1513           0 :         skelmodel::skeleton *skel = meshes->skel;
    1514           0 :         ragdollskel *ragdoll = skel->trycreateragdoll();
    1515           0 :         if(ragdoll->loaded)
    1516             :         {
    1517           0 :             return nullptr;
    1518             :         }
    1519           0 :         return ragdoll;
    1520             :     }
    1521             : 
    1522             :     /**
    1523             :      * @brief Adds a vertex to the working ragdoll vert list
    1524             :      *
    1525             :      * @param x the x position of the new vert
    1526             :      * @param y the y position of the new vert
    1527             :      * @param z the z position of the new vert
    1528             :      * @param radius the effect radius of the ragdoll vert
    1529             :      */
    1530           2 :     static void rdvert(const float *x, const float *y, const float *z, const float *radius)
    1531             :     {
    1532           2 :         ragdollskel *ragdoll = checkragdoll();
    1533           2 :         if(!ragdoll)
    1534             :         {
    1535           2 :             return;
    1536             :         }
    1537           0 :         ragdoll->verts.push_back({vec(*x, *y, *z), *radius > 0 ? *radius : 1});
    1538             :     }
    1539             : 
    1540             :     /**
    1541             :      *  @brief sets the ragdoll eye level
    1542             :      *
    1543             :      * Sets the ragdoll's eye point to the level passed
    1544             :      * implicitly modifies the ragdoll selected by CHECK_RAGDOLL
    1545             :      *
    1546             :      * @param v the level to set the eye at
    1547             :      */
    1548           2 :     static void rdeye(const int *v)
    1549             :     {
    1550           2 :         ragdollskel *ragdoll = checkragdoll();
    1551           2 :         if(!ragdoll)
    1552             :         {
    1553           2 :             return;
    1554             :         }
    1555           0 :         ragdoll->eye = *v;
    1556             :     }
    1557             : 
    1558           2 :     static void rdtri(const int *v1, const int *v2, const int *v3)
    1559             :     {
    1560           2 :         ragdollskel *ragdoll = checkragdoll();
    1561           2 :         if(!ragdoll)
    1562             :         {
    1563           2 :             return;
    1564             :         }
    1565           0 :         ragdoll->tris.push_back({*v1, *v2, *v3});
    1566             :     }
    1567             : 
    1568           2 :     static void rdjoint(const int *n, const int *t, const int *v1, const int *v2, const int *v3)
    1569             :     {
    1570           2 :         ragdollskel *ragdoll = checkragdoll();
    1571           2 :         if(!ragdoll)
    1572             :         {
    1573           2 :             return;
    1574             :         }
    1575           0 :         const skelmodel *m = static_cast<skelmodel *>(MDL::loading);
    1576           0 :         const skelmodel::skelmeshgroup *meshes = static_cast<const skelmodel::skelmeshgroup *>(m->parts.back()->meshes);
    1577           0 :         const skelmodel::skeleton *skel = meshes->skel;
    1578           0 :         if(*n < 0 || *n >= static_cast<int>(skel->numbones))
    1579             :         {
    1580           0 :             return;
    1581             :         }
    1582           0 :         ragdoll->joints.push_back({*n, *t, {*v1, *v2, *v3}});
    1583             :     }
    1584             : 
    1585           2 :     static void rdlimitdist(const int *v1, const int *v2, const float *mindist, const float *maxdist)
    1586             :     {
    1587           2 :         ragdollskel *ragdoll = checkragdoll();
    1588           2 :         if(!ragdoll)
    1589             :         {
    1590           2 :             return;
    1591             :         }
    1592           0 :         ragdoll->distlimits.push_back({*v1, *v2, *mindist, std::max(*maxdist, *mindist)});
    1593             :     }
    1594             : 
    1595           2 :     static void rdlimitrot(const int *t1, const int *t2, const float *maxangle, const float *qx, const float *qy, const float *qz, const float *qw)
    1596             :     {
    1597           2 :         ragdollskel *ragdoll = checkragdoll();
    1598           2 :         if(!ragdoll)
    1599             :         {
    1600           2 :             return;
    1601             :         }
    1602           0 :         float rmaxangle = *maxangle / RAD;
    1603           0 :         ragdoll->rotlimits.push_back({*t1,
    1604             :                                       *t2,
    1605             :                                       rmaxangle,
    1606           0 :                                       1 + 2*std::cos(rmaxangle),
    1607           0 :                                       matrix3(quat(*qx, *qy, *qz, *qw))});
    1608             :     }
    1609             : 
    1610           2 :     static void rdanimjoints(const int *on)
    1611             :     {
    1612           2 :         ragdollskel *ragdoll = checkragdoll();
    1613           2 :         if(!ragdoll)
    1614             :         {
    1615           2 :             return;
    1616             :         }
    1617           0 :         ragdoll->animjoints = *on!=0;
    1618             :     }
    1619             : 
    1620           2 :     skelcommands()
    1621           2 :     {
    1622           2 :         if(MDL::multiparted())
    1623             :         {
    1624           2 :             this->modelcommand(loadpart, "load", "ssf"); //<fmt>load [mesh] [skel] [smooth]
    1625             :         }
    1626           2 :         this->modelcommand(settag, "tag", "ssffffff"); //<fmt>tag [name] [tag] [tx] [ty] [tz] [rx] [ry] [rz]
    1627           2 :         this->modelcommand(setpitch, "pitchbone", "sffff"); //<fmt>pitchbone [name] [target] [scale] [min] [max]
    1628           2 :         this->modelcommand(setpitchtarget, "pitchtarget", "ssiff"); //<fmt>pitchtarget [name] [anim] [offset] [min] [max]
    1629           2 :         this->modelcommand(setpitchcorrect, "pitchcorrect", "ssfff"); //<fmt>pitchcorrect [name] [target] [scale] [min] [max]
    1630           2 :         this->modelcommand(sethitzone, "hitzone", "is"); //<fmt>hitzone [id] [mask]
    1631           2 :         if(MDL::cananimate())
    1632             :         {
    1633           2 :             this->modelcommand(setanim, "anim", "ssfiii"); //<fmt>anim [anim] [animfile] [speed] [priority] [startoffset] [endoffset]
    1634           2 :             this->modelcommand(setanimpart, "animpart", "s"); //<fmt>animpart [maskstr]
    1635           2 :             this->modelcommand(setadjust, "adjust", "sffffff"); //<fmt>adjust [name] [yaw] [pitch] [tx] [ty] [tz]
    1636             :         }
    1637             : 
    1638           2 :         this->modelcommand(rdvert, "rdvert", "ffff");
    1639           2 :         this->modelcommand(rdeye, "rdeye", "i");
    1640           2 :         this->modelcommand(rdtri, "rdtri", "iii");
    1641           2 :         this->modelcommand(rdjoint, "rdjoint", "iibbb");
    1642           2 :         this->modelcommand(rdlimitdist, "rdlimitdist", "iiff");
    1643           2 :         this->modelcommand(rdlimitrot, "rdlimitrot", "iifffff");
    1644           2 :         this->modelcommand(rdanimjoints, "rdanimjoints", "i");
    1645           2 :     }
    1646             : };
    1647             : 
    1648             : #endif

Generated by: LCOV version 1.14