LCOV - code coverage report
Current view: top level - engine/render - rendermodel.cpp (source / functions) Coverage Total Hit
Test: Libprimis Test Coverage Lines: 7.5 % 639 48
Test Date: 2026-06-16 06:16:16 Functions: 19.1 % 47 9

            Line data    Source code
       1              : /* rendermodel.cpp: world static and dynamic models
       2              :  *
       3              :  * Libprimis can handle static ("mapmodel") type models which are placed in levels
       4              :  * as well as dynamic, animated models such as players or other actors. For animated
       5              :  * models, the md5 model format is supported; simpler static models can use the
       6              :  * common Wavefront (obj) model format.
       7              :  *
       8              :  */
       9              : #include "../libprimis-headers/cube.h"
      10              : #include "../../shared/geomexts.h"
      11              : #include "../../shared/glemu.h"
      12              : #include "../../shared/glexts.h"
      13              : #include "../../shared/stream.h"
      14              : 
      15              : #include <optional>
      16              : #include <memory>
      17              : #include <format>
      18              : 
      19              : #include "aa.h"
      20              : #include "csm.h"
      21              : #include "radiancehints.h"
      22              : #include "rendergl.h"
      23              : #include "renderlights.h"
      24              : #include "rendermodel.h"
      25              : #include "renderva.h"
      26              : #include "renderwindow.h"
      27              : #include "shader.h"
      28              : #include "shaderparam.h"
      29              : #include "texture.h"
      30              : 
      31              : #include "interface/console.h"
      32              : #include "interface/control.h"
      33              : #include "interface/cs.h"
      34              : 
      35              : #include "world/entities.h"
      36              : #include "world/octaedit.h"
      37              : #include "world/octaworld.h"
      38              : #include "world/bih.h"
      39              : #include "world/world.h"
      40              : 
      41              : std::vector<std::string> animnames; //set by game at runtime
      42              : 
      43              : //need the above vars inited before these headers will load properly
      44              : 
      45              : #include "model/model.h"
      46              : #include "model/ragdoll.h"
      47              : #include "model/animmodel.h"
      48              : #include "model/vertmodel.h"
      49              : #include "model/skelmodel.h"
      50              : 
      51            0 : model *loadmapmodel(int n)
      52              : {
      53            0 :     if(static_cast<int>(mapmodel::mapmodels.size()) > n)
      54              :     {
      55            0 :         model *m = mapmodel::mapmodels[n].m;
      56            0 :         return m ? m : loadmodel("", n);
      57              :     }
      58            0 :     return nullptr;
      59              : }
      60              : 
      61              : //need the above macros & fxns inited before these headers will load properly
      62              : #include "model/md5.h"
      63              : #include "model/obj.h"
      64              : #include "model/gltf.h"
      65              : 
      66              : // mapmodels
      67              : 
      68              : namespace mapmodel
      69              : {
      70              :     std::vector<MapModelInfo> mapmodels;
      71              :     static const std::string mmprefix = "mapmodel/";
      72              :     static const size_t mmprefixlen = mmprefix.size();
      73              : 
      74            1 :     void open(const char *name)
      75              :     {
      76            1 :         MapModelInfo mmi;
      77            1 :         if(name[0])
      78              :         {
      79            0 :             mmi.name = std::string().append(mmprefix).append(name);
      80              :         }
      81              :         else
      82              :         {
      83            1 :             mmi.name.clear();
      84              :         }
      85            1 :         mmi.m = mmi.collide = nullptr;
      86            1 :         mapmodels.push_back(mmi);
      87            1 :     }
      88              : 
      89            1 :     void reset(const int *n)
      90              :     {
      91            1 :         if(!(identflags&Idf_Overridden) && !allowediting)
      92              :         {
      93            1 :             return;
      94              :         }
      95            0 :         mapmodels.resize(std::clamp(*n, 0, static_cast<int>(mapmodels.size())));
      96              :     }
      97              : 
      98            0 :     const char *name(int i)
      99              :     {
     100            0 :         return (static_cast<int>(mapmodels.size()) > i) ? mapmodels[i].name.c_str() : nullptr;
     101              :     }
     102              : 
     103            1 :     void namecmd(const int *index, const int *prefix)
     104              :     {
     105            1 :         if(static_cast<int>(mapmodels.size()) > *index)
     106              :         {
     107            0 :             result(mapmodels[*index].name.empty() ? mapmodels[*index].name.c_str() + (*prefix ? 0 : mmprefixlen) : "");
     108              :         }
     109            1 :     }
     110              : 
     111            1 :     void loaded(const int *index)
     112              :     {
     113            1 :         intret(static_cast<int>(mapmodels.size()) > *index && mapmodels[*index].m ? 1 : 0);
     114            1 :     }
     115              : 
     116            1 :     void num()
     117              :     {
     118            1 :         intret(mapmodels.size());
     119            1 :     }
     120              : }
     121              : 
     122            0 : static bool modeloccluded(const vec &center, float radius)
     123              : {
     124            0 :     ivec bbmin(vec(center).sub(radius)),
     125            0 :          bbmax(vec(center).add(radius+1));
     126            0 :     return rootworld.bboccluded(bbmin, bbmax);
     127              : }
     128              : 
     129              : //ratio between model size and distance at which to cull: at 200, model must be 200 times smaller than distance to model
     130              : static VAR(maxmodelradiusdistance, 10, 200, 1000);
     131              : 
     132              : /**
     133              :  * @brief Returns whether the model should be culled.
     134              :  *
     135              :  * Attempts to cull by distance from camera1, then by view frustum from `vfc` view,
     136              :  * then by occlusion query.
     137              :  *
     138              :  * If no reason can be found to occlude the model, returns 0. Otherwise, returns
     139              :  * the Model enum flag for the cull reason.
     140              :  */
     141            0 : static int cullmodel(const vec &center, float radius, int flags, const dynent *d = nullptr)
     142              : {
     143            0 :     if(flags&Model_CullDist && (center.dist(camera1->o) / radius) > maxmodelradiusdistance)
     144              :     {
     145            0 :         return Model_CullDist;
     146              :     }
     147            0 :     if(flags&Model_CullVFC && view.isfoggedsphere(radius, center))
     148              :     {
     149            0 :         return Model_CullVFC;
     150              :     }
     151            0 :     if(flags&Model_CullOccluded && modeloccluded(center, radius))
     152              :     {
     153            0 :         return Model_CullOccluded;
     154              :     }
     155            0 :     else if(flags&Model_CullQuery && d->query && d->query->owner==d && occlusionengine.checkquery(d->query))
     156              :     {
     157            0 :         return Model_CullQuery;
     158              :     }
     159            0 :     return 0;
     160              : }
     161              : 
     162            0 : static int shadowmaskmodel(const vec &center, float radius)
     163              : {
     164            0 :     switch(shadowmapping)
     165              :     {
     166            0 :         case ShadowMap_Reflect:
     167            0 :             return calcspherersmsplits(center, radius);
     168            0 :         case ShadowMap_CubeMap:
     169              :         {
     170            0 :             vec scenter = vec(center).sub(shadoworigin);
     171            0 :             float sradius = radius + shadowradius;
     172            0 :             if(scenter.squaredlen() >= sradius*sradius)
     173              :             {
     174            0 :                 return 0;
     175              :             }
     176            0 :             return calcspheresidemask(scenter, radius, shadowbias);
     177              :         }
     178            0 :         case ShadowMap_Cascade:
     179              :         {
     180            0 :             return csm.calcspheresplits(center, radius);
     181              :         }
     182            0 :         case ShadowMap_Spot:
     183              :         {
     184            0 :             vec scenter = vec(center).sub(shadoworigin);
     185            0 :             float sradius = radius + shadowradius;
     186            0 :             return scenter.squaredlen() < sradius*sradius && sphereinsidespot(shadowdir, shadowspot, scenter, radius) ? 1 : 0;
     187              :         }
     188              :     }
     189            0 :     return 0;
     190              : }
     191              : 
     192              : namespace batching
     193              : {
     194              :     struct modelbatch final
     195              :     {
     196              :         const model *m;
     197              :         int flags, batched;
     198              :     };
     199              : 
     200              :     struct batchedmodel final
     201              :     {
     202              :         //orient = yaw, pitch, roll
     203              :         vec pos, orient, center;
     204              :         float radius, sizescale;
     205              :         vec4<float> colorscale;
     206              :         int anim, basetime, basetime2, flags, attached;
     207              :         union
     208              :         {
     209              :             int visible;
     210              :             int culled;
     211              :         };
     212              :         dynent *d;
     213              :         int next;
     214              : 
     215              :         void renderbatchedmodel(const model &m) const;
     216              :         //sets bbmin and bbmax to the min/max of itself and the batchedmodel's bb
     217              :         void applybb(vec &bbmin, vec &bbmax) const;
     218              :         bool shadowmask(bool dynshadow);
     219              : 
     220              :         int rendertransparentmodel(const modelbatch &b, bool &rendered);
     221              :     };
     222              : 
     223              :     static std::vector<batchedmodel> batchedmodels;
     224              :     static std::vector<modelbatch> batches;
     225              :     static std::vector<modelattach> modelattached;
     226              : 
     227            0 :     void resetmodelbatches()
     228              :     {
     229            0 :         batchedmodels.clear();
     230            0 :         batches.clear();
     231            0 :         modelattached.clear();
     232            0 :     }
     233              : 
     234              :     /**
     235              :      * @brief Adds or modifies a batchedmodel with the specified model
     236              :      *
     237              :      * @param m the model to potentially assign to the batched model
     238              :      * @param bm the batched model to modify
     239              :      * @param idx the index value to set, if model `m` is already the model assigned to `bm`
     240              :      */
     241            0 :     static void addbatchedmodel(model *m, batchedmodel &bm, int idx)
     242              :     {
     243            0 :         modelbatch *b = nullptr;
     244            0 :         if(batches.size() > static_cast<size_t>(m->batch))
     245              :         {
     246            0 :             b = &batches[m->batch];
     247            0 :             if(b->m == m && (b->flags & Model_Mapmodel) == (bm.flags & Model_Mapmodel))
     248              :             {
     249            0 :                 goto foundbatch; //skip setup
     250              :             }
     251              :         }
     252            0 :         m->batch = batches.size();
     253            0 :         batches.emplace_back();
     254            0 :         b = &batches.back();
     255            0 :         b->m = m;
     256            0 :         b->flags = 0;
     257            0 :         b->batched = -1;
     258              : 
     259            0 :     foundbatch:
     260            0 :         b->flags |= bm.flags;
     261            0 :         bm.next = b->batched;
     262            0 :         b->batched = idx;
     263            0 :     }
     264              : 
     265            0 :     void batchedmodel::renderbatchedmodel(const model &m) const
     266              :     {
     267            0 :         modelattach *a = nullptr;
     268            0 :         if(attached>=0)
     269              :         {
     270            0 :             a = &modelattached[attached];
     271              :         }
     272            0 :         int tempanim = anim;
     273            0 :         if(shadowmapping > ShadowMap_Reflect)
     274              :         {
     275            0 :             tempanim |= Anim_NoSkin;
     276              :         }
     277              :         else
     278              :         {
     279            0 :             if(flags&Model_FullBright)
     280              :             {
     281            0 :                 tempanim |= Anim_FullBright;
     282              :             }
     283              :         }
     284              : 
     285            0 :         m.render(tempanim, basetime, basetime2, pos, orient.x, orient.y, orient.z, d, a, sizescale, colorscale);
     286            0 :     }
     287              : 
     288            0 :     bool batchedmodel::shadowmask(bool dynshadow)
     289              :     {
     290            0 :         if(flags&(Model_Mapmodel | Model_NoShadow)) //mapmodels are not dynamic models by definition
     291              :         {
     292            0 :             return false;
     293              :         }
     294            0 :         visible = dynshadow && (colorscale.a() >= 1 || flags&(Model_OnlyShadow | Model_ForceShadow)) ? shadowmaskmodel(center, radius) : 0;
     295            0 :         return true;
     296              :     }
     297              : 
     298            0 :     void shadowmaskbatchedmodels(bool dynshadow)
     299              :     {
     300            0 :         for(batchedmodel &b : batchedmodels)
     301              :         {
     302            0 :             if(!b.shadowmask(dynshadow))
     303              :             {
     304            0 :                 break;
     305              :             }
     306              :         }
     307            0 :     }
     308              : 
     309            0 :     int batcheddynamicmodels()
     310              :     {
     311            0 :         int visible = 0;
     312            0 :         for(const batchedmodel &b : batchedmodels)
     313              :         {
     314            0 :             if(b.flags&Model_Mapmodel) //mapmodels are not dynamic models by definition
     315              :             {
     316            0 :                 break;
     317              :             }
     318            0 :             visible |= b.visible;
     319              :         }
     320            0 :         for(const modelbatch &b : batches)
     321              :         {
     322            0 :             if(!(b.flags&Model_Mapmodel) || !b.m->animated())
     323              :             {
     324            0 :                 continue;
     325              :             }
     326            0 :             for(int j = b.batched; j >= 0;)
     327              :             {
     328            0 :                 const batchedmodel &bm = batchedmodels[j];
     329            0 :                 j = bm.next;
     330            0 :                 visible |= bm.visible;
     331              :             }
     332              :         }
     333            0 :         return visible;
     334              :     }
     335              : 
     336            0 :     void batchedmodel::applybb(vec &bbmin, vec &bbmax) const
     337              :     {
     338            0 :         bbmin.min(vec(center).sub(radius));
     339            0 :         bbmax.max(vec(center).add(radius));
     340            0 :     }
     341              : 
     342            0 :     int batcheddynamicmodelbounds(int mask, vec &bbmin, vec &bbmax)
     343              :     {
     344            0 :         int vis = 0;
     345            0 :         for(const batchedmodel &b : batchedmodels)
     346              :         {
     347            0 :             if(b.flags&Model_Mapmodel) //mapmodels are not dynamic models by definition
     348              :             {
     349            0 :                 break;
     350              :             }
     351            0 :             if(b.visible&mask)
     352              :             {
     353            0 :                 b.applybb(bbmin, bbmax);
     354            0 :                 ++vis;
     355              :             }
     356              :         }
     357            0 :         for(const modelbatch &b : batches)
     358              :         {
     359            0 :             if(!(b.flags&Model_Mapmodel) || !b.m->animated())
     360              :             {
     361            0 :                 continue;
     362              :             }
     363            0 :             for(int j = b.batched; j >= 0;)
     364              :             {
     365            0 :                 const batchedmodel &bm = batchedmodels[j];
     366            0 :                 j = bm.next;
     367            0 :                 if(bm.visible&mask)
     368              :                 {
     369            0 :                     bm.applybb(bbmin, bbmax);
     370            0 :                     ++vis;
     371              :                 }
     372              :             }
     373              :         }
     374            0 :         return vis;
     375              :     }
     376              : 
     377            0 :     void rendershadowmodelbatches(bool dynmodel)
     378              :     {
     379            0 :         for(const modelbatch &b : batches)
     380              :         {
     381            0 :             if(!b.m->shadow || (!dynmodel && (!(b.flags&Model_Mapmodel) || b.m->animated())))
     382              :             {
     383            0 :                 continue;
     384              :             }
     385            0 :             bool rendered = false;
     386            0 :             for(int j = b.batched; j >= 0;)
     387              :             {
     388            0 :                 const batchedmodel &bm = batchedmodels[j];
     389            0 :                 j = bm.next;
     390            0 :                 if(!(bm.visible&(1<<shadowside)))
     391              :                 {
     392            0 :                     continue;
     393              :                 }
     394            0 :                 if(!rendered)
     395              :                 {
     396            0 :                     b.m->startrender();
     397            0 :                     rendered = true;
     398              :                 }
     399            0 :                 bm.renderbatchedmodel(*b.m);
     400              :             }
     401            0 :             if(rendered)
     402              :             {
     403            0 :                 b.m->endrender();
     404              :             }
     405              :         }
     406            0 :     }
     407              : 
     408            0 :     void rendermapmodelbatches()
     409              :     {
     410            0 :         aamask::enable();
     411            0 :         for(const modelbatch &b : batches)
     412              :         {
     413            0 :             if(!(b.flags&Model_Mapmodel))
     414              :             {
     415            0 :                 continue;
     416              :             }
     417            0 :             b.m->startrender();
     418            0 :             aamask::set(b.m->animated());
     419            0 :             for(int j = b.batched; j >= 0;)
     420              :             {
     421            0 :                 const batchedmodel &bm = batchedmodels[j];
     422            0 :                 bm.renderbatchedmodel(*b.m);
     423            0 :                 j = bm.next;
     424              :             }
     425            0 :             b.m->endrender();
     426              :         }
     427            0 :         aamask::disable();
     428            0 :     }
     429              : 
     430            0 :     int batchedmodel::rendertransparentmodel(const modelbatch &b, bool &rendered)
     431              :     {
     432            0 :         int j = next;
     433            0 :         culled = cullmodel(center, radius, flags, d);
     434            0 :         if(culled || !(colorscale.a() < 1 || flags&Model_ForceTransparent) || flags&Model_OnlyShadow)
     435              :         {
     436            0 :             return j;
     437              :         }
     438            0 :         if(!rendered)
     439              :         {
     440            0 :             b.m->startrender();
     441            0 :             rendered = true;
     442            0 :             aamask::set(true);
     443              :         }
     444            0 :         if(flags&Model_CullQuery)
     445              :         {
     446            0 :             d->query = occlusionengine.newquery(d);
     447            0 :             if(d->query)
     448              :             {
     449            0 :                 d->query->startquery();
     450            0 :                 renderbatchedmodel(*b.m);
     451            0 :                 occlusionengine.endquery();
     452            0 :                 return j;
     453              :             }
     454              :         }
     455            0 :         renderbatchedmodel(*b.m);
     456            0 :         return j;
     457              :     }
     458              : 
     459            0 :     void rendertransparentmodelbatches(int stencil)
     460              :     {
     461            0 :         aamask::enable(stencil);
     462            0 :         for(modelbatch &b : batches)
     463              :         {
     464            0 :             if(b.flags&Model_Mapmodel)
     465              :             {
     466            0 :                 continue;
     467              :             }
     468            0 :             bool rendered = false;
     469            0 :             for(int j = b.batched; j >= 0;)
     470              :             {
     471            0 :                 batchedmodel &bm = batchedmodels[j];
     472            0 :                 bm.rendertransparentmodel(b, rendered);
     473              :             }
     474            0 :             if(rendered)
     475              :             {
     476            0 :                 b.m->endrender();
     477              :             }
     478              :         }
     479            0 :         aamask::disable();
     480            0 :     }
     481              : 
     482            0 :     void clearbatchedmapmodels()
     483              :     {
     484            0 :         for(size_t i = 0; i < batches.size(); i++)
     485              :         {
     486            0 :             const modelbatch &b = batches[i];
     487            0 :             if(b.flags&Model_Mapmodel)
     488              :             {
     489            0 :                 batchedmodels.resize(b.batched);
     490            0 :                 batches.resize(i);
     491            0 :                 break;
     492              :             }
     493              :         }
     494            0 :     }
     495              : }
     496              : 
     497              : // model registry
     498              : 
     499              : std::unordered_map<std::string, model *> models;
     500              : std::vector<std::string> preloadmodels;
     501              : 
     502              : //used in iengine
     503            0 : void preloadmodel(std::string name)
     504              : {
     505            0 :     if(name.empty() || models.find(name) != models.end() || std::find(preloadmodels.begin(), preloadmodels.end(), name) != preloadmodels.end() )
     506              :     {
     507            0 :         return;
     508              :     }
     509            0 :     preloadmodels.push_back(name);
     510              : }
     511              : 
     512            0 : void flushpreloadedmodels(bool msg)
     513              : {
     514            0 :     for(size_t i = 0; i < preloadmodels.size(); i++)
     515              :     {
     516            0 :         loadprogress = static_cast<float>(i+1)/preloadmodels.size();
     517            0 :         model *m = loadmodel(preloadmodels[i].c_str(), -1, msg);
     518            0 :         if(!m)
     519              :         {
     520            0 :             if(msg)
     521              :             {
     522            0 :                 conoutf(Console_Warn, "could not load model: %s", preloadmodels[i].c_str());
     523              :             }
     524              :         }
     525              :         else
     526              :         {
     527            0 :             m->preloadmeshes();
     528            0 :             m->preloadshaders();
     529              :         }
     530              :     }
     531            0 :     preloadmodels.clear();
     532              : 
     533            0 :     loadprogress = 0;
     534            0 : }
     535              : 
     536            0 : void preloadusedmapmodels(bool msg, bool bih)
     537              : {
     538            0 :     std::vector<extentity *> &ents = entities::getents();
     539            0 :     std::vector<int> used;
     540            0 :     for(extentity *&e : ents)
     541              :     {
     542            0 :         if(e->type==EngineEnt_Mapmodel && e->attr1 >= 0 && std::find(used.begin(), used.end(), e->attr1) != used.end() )
     543              :         {
     544            0 :             used.push_back(e->attr1);
     545              :         }
     546              :     }
     547              : 
     548            0 :     std::vector<std::string> col;
     549            0 :     for(size_t i = 0; i < used.size(); i++)
     550              :     {
     551            0 :         loadprogress = static_cast<float>(i+1)/used.size();
     552            0 :         int mmindex = used[i];
     553            0 :         if(!(static_cast<int>(mapmodel::mapmodels.size()) > (mmindex)))
     554              :         {
     555            0 :             if(msg)
     556              :             {
     557            0 :                 conoutf(Console_Warn, "could not find map model: %d", mmindex);
     558              :             }
     559            0 :             continue;
     560              :         }
     561            0 :         const MapModelInfo &mmi = mapmodel::mapmodels[mmindex];
     562            0 :         if(mmi.name.empty())
     563              :         {
     564            0 :             continue;
     565              :         }
     566            0 :         model *m = loadmodel("", mmindex, msg);
     567            0 :         if(!m)
     568              :         {
     569            0 :             if(msg)
     570              :             {
     571            0 :                 conoutf(Console_Warn, "could not load map model: %s", mmi.name.c_str());
     572              :             }
     573              :         }
     574              :         else
     575              :         {
     576            0 :             if(bih)
     577              :             {
     578            0 :                 m->preloadBIH();
     579              :             }
     580            0 :             else if(m->collide == Collide_TRI && m->collidemodel.empty() && m->bih)
     581              :             {
     582            0 :                 m->setBIH();
     583              :             }
     584            0 :             m->preloadmeshes();
     585            0 :             m->preloadshaders();
     586            0 :             if(!m->collidemodel.empty() && std::find(col.begin(), col.end(), m->collidemodel) == col.end())
     587              :             {
     588            0 :                 col.push_back(m->collidemodel);
     589              :             }
     590              :         }
     591              :     }
     592              : 
     593            0 :     for(size_t i = 0; i < col.size(); i++)
     594              :     {
     595            0 :         loadprogress = static_cast<float>(i+1)/col.size();
     596            0 :         model *m = loadmodel(col[i].c_str(), -1, msg);
     597            0 :         if(!m)
     598              :         {
     599            0 :             if(msg)
     600              :             {
     601            0 :                 conoutf(Console_Warn, "could not load collide model: %s", col[i].c_str());
     602              :             }
     603              :         }
     604            0 :         else if(!m->bih)
     605              :         {
     606            0 :             m->setBIH();
     607              :         }
     608              :     }
     609              : 
     610            0 :     loadprogress = 0;
     611            0 : }
     612              : 
     613            0 : model *loadmodel(std::string_view name, int i, bool msg)
     614              : {
     615            0 :     model *(__cdecl *md5loader)(const std::string &filename) = +[] (const std::string &filename) -> model* { return new md5(filename); };
     616            0 :     model *(__cdecl *objloader)(const std::string &filename) = +[] (const std::string &filename) -> model* { return new obj(filename); };
     617            0 :     model *(__cdecl *gltfloader)(const std::string &filename) = +[] (const std::string &filename) -> model* { return new gltf(filename); };
     618              : 
     619            0 :     std::vector<model *(__cdecl *)(const std::string &)> loaders;
     620            0 :     loaders.push_back(md5loader);
     621            0 :     loaders.push_back(objloader);
     622            0 :     loaders.push_back(gltfloader);
     623            0 :     std::unordered_set<std::string> failedmodels;
     624              : 
     625            0 :     if(!name.size())
     626              :     {
     627            0 :         if(!(static_cast<int>(mapmodel::mapmodels.size()) > i))
     628              :         {
     629            0 :             return nullptr;
     630              :         }
     631            0 :         const MapModelInfo &mmi = mapmodel::mapmodels[i];
     632            0 :         if(mmi.m)
     633              :         {
     634            0 :             return mmi.m;
     635              :         }
     636            0 :         name = mmi.name.c_str();
     637              :     }
     638            0 :     std::unordered_map<std::string, model *>::const_iterator itr = models.find(std::string(name));
     639              :     model *m;
     640            0 :     if(itr != models.end())
     641              :     {
     642            0 :         m = (*itr).second;
     643              :     }
     644              :     else
     645              :     {
     646            0 :         if(!name[0] || failedmodels.find(std::string(name)) != failedmodels.end())
     647              :         {
     648            0 :             return nullptr;
     649              :         }
     650            0 :         if(msg)
     651              :         {
     652            0 :             std::string filename;
     653            0 :             filename.append(modelpath).append(name);
     654            0 :             renderprogress(loadprogress, filename.c_str());
     655            0 :         }
     656            0 :         for(model *(__cdecl *i)(const std::string &) : loaders)
     657              :         {
     658            0 :             m = i(std::string(name)); //call model ctor
     659            0 :             if(!m)
     660              :             {
     661            0 :                 continue;
     662              :             }
     663            0 :             if(m->load()) //now load the model
     664              :             {
     665            0 :                 break;
     666              :             }
     667              :             //delete model if not successful
     668            0 :             delete m;
     669            0 :             m = nullptr;
     670              :         }
     671            0 :         if(!m)
     672              :         {
     673            0 :             failedmodels.insert(std::string(name));
     674            0 :             return nullptr;
     675              :         }
     676            0 :         if(models.find(m->modelname()) == models.end())
     677              :         {
     678            0 :             models[m->modelname()] = m;
     679              :         }
     680              :     }
     681            0 :     if((mapmodel::mapmodels.size() > static_cast<size_t>(i)) && !mapmodel::mapmodels[i].m)
     682              :     {
     683            0 :         mapmodel::mapmodels[i].m = m;
     684              :     }
     685            0 :     return m;
     686            0 : }
     687              : 
     688              : //used in iengine.h
     689            0 : void clear_models()
     690              : {
     691            0 :     for(auto [k, i] : models)
     692              :     {
     693            0 :         delete i;
     694            0 :     }
     695            0 : }
     696              : 
     697            0 : void cleanupmodels()
     698              : {
     699            0 :     for(auto [k, i] : models)
     700              :     {
     701            0 :         i->cleanup();
     702            0 :     }
     703            0 : }
     704              : 
     705            1 : static void clearmodel(const char *name)
     706              : {
     707            1 :     model *m = nullptr;
     708            1 :     std::unordered_map<std::string, model *>::const_iterator it = models.find(name);
     709            1 :     if(it != models.end())
     710              :     {
     711            0 :         m = (*it).second;
     712              :     }
     713            1 :     if(!m)
     714              :     {
     715            1 :         conoutf("model %s is not loaded", name);
     716            1 :         return;
     717              :     }
     718            0 :     for(MapModelInfo &mmi : mapmodel::mapmodels)
     719              :     {
     720            0 :         if(mmi.m == m)
     721              :         {
     722            0 :             mmi.m = nullptr;
     723              :         }
     724            0 :         if(mmi.collide == m)
     725              :         {
     726            0 :             mmi.collide = nullptr;
     727              :         }
     728              :     }
     729            0 :     models.erase(name);
     730            0 :     m->cleanup();
     731            0 :     delete m;
     732            0 :     conoutf("cleared model %s", name);
     733              : }
     734              : 
     735            0 : static void rendercullmodelquery(dynent *d, const vec &center, float radius)
     736              : {
     737            0 :     if(std::fabs(camera1->o.x-center.x) < radius+1 &&
     738            0 :        std::fabs(camera1->o.y-center.y) < radius+1 &&
     739            0 :        std::fabs(camera1->o.z-center.z) < radius+1)
     740              :     {
     741            0 :         d->query = nullptr;
     742            0 :         return;
     743              :     }
     744            0 :     d->query = occlusionengine.newquery(d);
     745            0 :     if(!d->query)
     746              :     {
     747            0 :         return;
     748              :     }
     749            0 :     d->query->startquery();
     750            0 :     const int br = static_cast<int>(radius*2)+1;
     751            0 :     drawbb(ivec(static_cast<float>(center.x-radius), static_cast<float>(center.y-radius), static_cast<float>(center.z-radius)), ivec(br, br, br));
     752            0 :     occlusionengine.endquery();
     753              : }
     754              : 
     755            0 : void GBuffer::rendermodelbatches()
     756              : {
     757            0 :     tmodelinfo.mdlsx1 = tmodelinfo.mdlsy1 = 1;
     758            0 :     tmodelinfo.mdlsx2 = tmodelinfo.mdlsy2 = -1;
     759            0 :     tmodelinfo.mdltiles.fill(0);
     760              : 
     761            0 :     aamask::enable();
     762            0 :     for(const batching::modelbatch &b : batching::batches)
     763              :     {
     764            0 :         if(b.flags&Model_Mapmodel)
     765              :         {
     766            0 :             continue;
     767              :         }
     768            0 :         bool rendered = false;
     769            0 :         for(int j = b.batched; j >= 0;)
     770              :         {
     771            0 :             batching::batchedmodel &bm = batching::batchedmodels[j];
     772            0 :             j = bm.next;
     773            0 :             bm.culled = cullmodel(bm.center, bm.radius, bm.flags, bm.d);
     774            0 :             if(bm.culled || bm.flags&Model_OnlyShadow)
     775              :             {
     776            0 :                 continue;
     777              :             }
     778            0 :             if(bm.colorscale.a() < 1 || bm.flags&Model_ForceTransparent)
     779              :             {
     780              :                 float sx1, sy1, sx2, sy2;
     781            0 :                 ivec bbmin(vec(bm.center).sub(bm.radius)), bbmax(vec(bm.center).add(bm.radius+1));
     782            0 :                 if(calcbbscissor(bbmin, bbmax, sx1, sy1, sx2, sy2))
     783              :                 {
     784            0 :                     tmodelinfo.mdlsx1 = std::min(tmodelinfo.mdlsx1, sx1);
     785            0 :                     tmodelinfo.mdlsy1 = std::min(tmodelinfo.mdlsy1, sy1);
     786            0 :                     tmodelinfo.mdlsx2 = std::max(tmodelinfo.mdlsx2, sx2);
     787            0 :                     tmodelinfo.mdlsy2 = std::max(tmodelinfo.mdlsy2, sy2);
     788            0 :                     masktiles(tmodelinfo.mdltiles.data(), sx1, sy1, sx2, sy2);
     789              :                 }
     790            0 :                 continue;
     791            0 :             }
     792            0 :             if(!rendered)
     793              :             {
     794            0 :                 b.m->startrender();
     795            0 :                 rendered = true;
     796            0 :                 aamask::set(true);
     797              :             }
     798            0 :             if(bm.flags&Model_CullQuery)
     799              :             {
     800            0 :                 bm.d->query = occlusionengine.newquery(bm.d);
     801            0 :                 if(bm.d->query)
     802              :                 {
     803            0 :                     bm.d->query->startquery();
     804            0 :                     bm.renderbatchedmodel(*b.m);
     805            0 :                     occlusionengine.endquery();
     806            0 :                     continue;
     807              :                 }
     808              :             }
     809            0 :             bm.renderbatchedmodel(*b.m);
     810              :         }
     811            0 :         if(rendered)
     812              :         {
     813            0 :             b.m->endrender();
     814              :         }
     815            0 :         if(b.flags&Model_CullQuery)
     816              :         {
     817            0 :             bool queried = false;
     818            0 :             for(int j = b.batched; j >= 0;)
     819              :             {
     820            0 :                 batching::batchedmodel &bm = batching::batchedmodels[j];
     821            0 :                 j = bm.next;
     822            0 :                 if(bm.culled&(Model_CullOccluded|Model_CullQuery) && bm.flags&Model_CullQuery)
     823              :                 {
     824            0 :                     if(!queried)
     825              :                     {
     826            0 :                         if(rendered)
     827              :                         {
     828            0 :                             aamask::set(false);
     829              :                         }
     830            0 :                         startbb();
     831            0 :                         queried = true;
     832              :                     }
     833            0 :                     rendercullmodelquery(bm.d, bm.center, bm.radius);
     834              :                 }
     835              :             }
     836            0 :             if(queried)
     837              :             {
     838            0 :                 endbb();
     839              :             }
     840              :         }
     841              :     }
     842            0 :     aamask::disable();
     843            0 : }
     844              : 
     845            0 : void Occluder::setupmodelquery(occludequery *q)
     846              : {
     847            0 :     modelquery = q;
     848            0 :     modelquerybatches = batching::batches.size();
     849            0 :     modelquerymodels = batching::batchedmodels.size();
     850            0 :     modelqueryattached = batching::modelattached.size();
     851            0 : }
     852              : 
     853            0 : void Occluder::endmodelquery()
     854              : {
     855            0 :     if(static_cast<int>(batching::batchedmodels.size()) == modelquerymodels)
     856              :     {
     857            0 :         modelquery->fragments = 0;
     858            0 :         modelquery = nullptr;
     859            0 :         return;
     860              :     }
     861            0 :     aamask::enable();
     862            0 :     modelquery->startquery();
     863            0 :     for(batching::modelbatch &b : batching::batches)
     864              :     {
     865            0 :         int j = b.batched;
     866            0 :         if(j < modelquerymodels)
     867              :         {
     868            0 :             continue;
     869              :         }
     870            0 :         b.m->startrender();
     871            0 :         aamask::set(!(b.flags&Model_Mapmodel) || b.m->animated());
     872              :         do
     873              :         {
     874            0 :             const batching::batchedmodel &bm = batching::batchedmodels[j];
     875            0 :             bm.renderbatchedmodel(*b.m);
     876            0 :             j = bm.next;
     877            0 :         } while(j >= modelquerymodels);
     878            0 :         b.batched = j;
     879            0 :         b.m->endrender();
     880              :     }
     881            0 :     occlusionengine.endquery();
     882            0 :     modelquery = nullptr;
     883            0 :     batching::batches.resize(modelquerybatches);
     884            0 :     batching::batchedmodels.resize(modelquerymodels);
     885            0 :     batching::modelattached.resize(modelqueryattached);
     886            0 :     aamask::disable();
     887              : }
     888              : 
     889            0 : void rendermapmodel(int idx, int anim, const vec &o, float yaw, float pitch, float roll, int flags, int basetime, float size)
     890              : {
     891            0 :     if(!(static_cast<int>(mapmodel::mapmodels.size()) > idx))
     892              :     {
     893            0 :         return;
     894              :     }
     895            0 :     const MapModelInfo &mmi = mapmodel::mapmodels[idx];
     896            0 :     model *m = mmi.m ? mmi.m : loadmodel(mmi.name);
     897            0 :     if(!m)
     898              :     {
     899            0 :         return;
     900              :     }
     901            0 :     vec center, bbradius;
     902            0 :     m->boundbox(center, bbradius);
     903            0 :     float radius = bbradius.magnitude();
     904            0 :     center.mul(size);
     905            0 :     if(roll)
     906              :     {
     907            0 :         center.rotate_around_y(roll/RAD);
     908              :     }
     909            0 :     if(pitch && m->pitched())
     910              :     {
     911            0 :         center.rotate_around_x(pitch/RAD);
     912              :     }
     913            0 :     center.rotate_around_z(yaw/RAD);
     914            0 :     center.add(o);
     915            0 :     radius *= size;
     916              : 
     917            0 :     int visible = 0;
     918            0 :     if(shadowmapping)
     919              :     {
     920            0 :         if(!m->shadow)
     921              :         {
     922            0 :             return;
     923              :         }
     924            0 :         visible = shadowmaskmodel(center, radius);
     925            0 :         if(!visible)
     926              :         {
     927            0 :             return;
     928              :         }
     929              :     }
     930            0 :     else if(flags&(Model_CullVFC|Model_CullDist|Model_CullOccluded) && cullmodel(center, radius, flags))
     931              :     {
     932            0 :         return;
     933              :     }
     934            0 :     batching::batchedmodels.emplace_back();
     935            0 :     batching::batchedmodel &b = batching::batchedmodels.back();
     936            0 :     b.pos = o;
     937            0 :     b.center = center;
     938            0 :     b.radius = radius;
     939            0 :     b.anim = anim;
     940            0 :     b.orient = {yaw, pitch, roll};
     941            0 :     b.basetime = basetime;
     942            0 :     b.basetime2 = 0;
     943            0 :     b.sizescale = size;
     944            0 :     b.colorscale = vec4<float>(1, 1, 1, 1);
     945            0 :     b.flags = flags | Model_Mapmodel;
     946            0 :     b.visible = visible;
     947            0 :     b.d = nullptr;
     948            0 :     b.attached = -1;
     949            0 :     batching::addbatchedmodel(m, b, batching::batchedmodels.size()-1);
     950              : }
     951              : 
     952              : static VAR(oqdynent, 0, 1, 1); //occlusion query dynamic ents
     953              : 
     954            0 : void rendermodel(std::string_view mdl, int anim, const vec &o, float yaw, float pitch, float roll, int flags, dynent *d, modelattach *a, int basetime, int basetime2, float size, const vec4<float> &color)
     955              : {
     956            0 :     model *m = loadmodel(mdl);
     957            0 :     if(!m)
     958              :     {
     959            0 :         return;
     960              :     }
     961              : 
     962            0 :     vec center, bbradius;
     963            0 :     m->boundbox(center, bbradius);
     964            0 :     float radius = bbradius.magnitude();
     965            0 :     if(d)
     966              :     {
     967            0 :         if(d->ragdoll)
     968              :         {
     969            0 :             if(anim & Anim_Ragdoll && d->ragdoll->millis >= basetime)
     970              :             {
     971            0 :                 radius = std::max(radius, d->ragdoll->radius);
     972            0 :                 center = d->ragdoll->center;
     973            0 :                 goto hasboundbox; //skip roll and pitch stuff
     974              :             }
     975            0 :             if(d->ragdoll)
     976              :             {
     977            0 :                 delete d->ragdoll;
     978            0 :                 d->ragdoll = nullptr;
     979              :             }
     980              :         }
     981            0 :         if(anim & Anim_Ragdoll)
     982              :         {
     983            0 :             flags &= ~(Model_CullVFC | Model_CullOccluded | Model_CullQuery);
     984              :         }
     985              :     }
     986            0 :     center.mul(size);
     987            0 :     if(roll)
     988              :     {
     989            0 :         center.rotate_around_y(roll/RAD);
     990              :     }
     991            0 :     if(pitch && m->pitched())
     992              :     {
     993            0 :         center.rotate_around_x(pitch/RAD);
     994              :     }
     995            0 :     center.rotate_around_z(yaw/RAD);
     996            0 :     center.add(o);
     997            0 : hasboundbox:
     998            0 :     radius *= size;
     999              : 
    1000            0 :     if(flags&Model_NoRender)
    1001              :     {
    1002            0 :         anim |= Anim_NoRender;
    1003              :     }
    1004              : 
    1005            0 :     if(a)
    1006              :     {
    1007            0 :         for(int i = 0; a[i].tag; i++)
    1008              :         {
    1009            0 :             if(a[i].name)
    1010              :             {
    1011            0 :                 a[i].m = loadmodel(a[i].name);
    1012              :             }
    1013              :         }
    1014              :     }
    1015              : 
    1016            0 :     if(flags&Model_CullQuery)
    1017              :     {
    1018            0 :         if(!oqfrags || !oqdynent || !d)
    1019              :         {
    1020            0 :             flags &= ~Model_CullQuery;
    1021              :         }
    1022              :     }
    1023              : 
    1024            0 :     if(flags&Model_NoBatch)
    1025              :     {
    1026            0 :         const int culled = cullmodel(center, radius, flags, d);
    1027            0 :         if(culled)
    1028              :         {
    1029            0 :             if(culled&(Model_CullOccluded|Model_CullQuery) && flags&Model_CullQuery)
    1030              :             {
    1031            0 :                 startbb();
    1032            0 :                 rendercullmodelquery(d, center, radius);
    1033            0 :                 endbb();
    1034              :             }
    1035            0 :             return;
    1036              :         }
    1037            0 :         aamask::enable();
    1038            0 :         if(flags&Model_CullQuery)
    1039              :         {
    1040            0 :             d->query = occlusionengine.newquery(d);
    1041            0 :             if(d->query)
    1042              :             {
    1043            0 :                 d->query->startquery();
    1044              :             }
    1045              :         }
    1046            0 :         m->startrender();
    1047            0 :         aamask::set(true);
    1048            0 :         if(flags&Model_FullBright)
    1049              :         {
    1050            0 :             anim |= Anim_FullBright;
    1051              :         }
    1052            0 :         m->render(anim, basetime, basetime2, o, yaw, pitch, roll, d, a, size, color);
    1053            0 :         m->endrender();
    1054            0 :         if(flags&Model_CullQuery && d->query)
    1055              :         {
    1056            0 :             occlusionengine.endquery();
    1057              :         }
    1058            0 :         aamask::disable();
    1059            0 :         return;
    1060              :     }
    1061              : 
    1062            0 :     batching::batchedmodels.emplace_back();
    1063            0 :     batching::batchedmodel &b = batching::batchedmodels.back();
    1064            0 :     b.pos = o;
    1065            0 :     b.center = center;
    1066            0 :     b.radius = radius;
    1067            0 :     b.anim = anim;
    1068            0 :     b.orient = {yaw, pitch, roll};
    1069            0 :     b.basetime = basetime;
    1070            0 :     b.basetime2 = basetime2;
    1071            0 :     b.sizescale = size;
    1072            0 :     b.colorscale = color;
    1073            0 :     b.flags = flags;
    1074            0 :     b.visible = 0;
    1075            0 :     b.d = d;
    1076            0 :     b.attached = a ? batching::modelattached.size() : -1;
    1077            0 :     if(a)
    1078              :     {
    1079            0 :         for(int i = 0;; i++)
    1080              :         {
    1081            0 :             batching::modelattached.push_back(a[i]);
    1082            0 :             if(!a[i].tag)
    1083              :             {
    1084            0 :                 break;
    1085              :             }
    1086              :         }
    1087              :     }
    1088            0 :     batching::addbatchedmodel(m, b, batching::batchedmodels.size()-1);
    1089              : }
    1090              : 
    1091            0 : int intersectmodel(std::string_view mdl, int anim, const vec &pos, float yaw, float pitch, float roll, const vec &o, const vec &ray, dynent *d, modelattach *a, int basetime, int basetime2, float size)
    1092              : {
    1093            0 :     const model *m = loadmodel(mdl);
    1094            0 :     if(!m)
    1095              :     {
    1096            0 :         return -1;
    1097              :     }
    1098            0 :     if(d && d->ragdoll && (!(anim & Anim_Ragdoll) || d->ragdoll->millis < basetime))
    1099              :     {
    1100            0 :         if(d->ragdoll)
    1101              :         {
    1102            0 :             delete d->ragdoll;
    1103            0 :             d->ragdoll = nullptr;
    1104              :         }
    1105              :     }
    1106            0 :     if(a)
    1107              :     {
    1108            0 :         for(int i = 0; a[i].tag; i++)
    1109              :         {
    1110            0 :             if(a[i].name)
    1111              :             {
    1112            0 :                 a[i].m = loadmodel(a[i].name);
    1113              :             }
    1114              :         }
    1115              :     }
    1116            0 :     return m->intersect(anim, basetime, basetime2, pos, yaw, pitch, roll, d, a, size, o, ray);
    1117              : }
    1118              : 
    1119            0 : void abovemodel(vec &o, const char *mdl)
    1120              : {
    1121            0 :     const model *m = loadmodel(mdl);
    1122            0 :     if(!m)
    1123              :     {
    1124            0 :         return;
    1125              :     }
    1126            0 :     vec center, radius;
    1127            0 :     m->calcbb(center, radius);
    1128            0 :     o.z += center.z + radius.z;
    1129              : }
    1130              : 
    1131            3 : std::vector<size_t> findanims(std::string_view pattern)
    1132              : {
    1133            3 :     std::vector<size_t> anims;
    1134            5 :     for(size_t i = 0; i < animnames.size(); ++i)
    1135              :     {
    1136            2 :         if(!animnames.at(i).compare(pattern))
    1137              :         {
    1138            2 :             anims.push_back(i);
    1139              :         }
    1140              :     }
    1141            3 :     return anims;
    1142            0 : }
    1143              : 
    1144            1 : void findanimscmd(const char *name)
    1145              : {
    1146            1 :     std::vector<size_t> anims = findanims(name);
    1147            1 :     std::vector<char> buf;
    1148              :     string num;
    1149            1 :     for(size_t i = 0; i < anims.size(); i++)
    1150              :     {
    1151            0 :         formatstring(num, "%lu", anims[i]);
    1152            0 :         if(i > 0)
    1153              :         {
    1154            0 :             buf.push_back(' ');
    1155              :         }
    1156            0 :         for(size_t i = 0; i < std::strlen(num); ++i)
    1157              :         {
    1158            0 :             buf.push_back(num[i]);
    1159              :         }
    1160              :     }
    1161            1 :     buf.push_back('\0');
    1162            1 :     result(buf.data());
    1163            1 : }
    1164              : 
    1165            0 : void loadskin(const std::string &dir, const std::string &altdir, Texture *&skin, Texture *&masks) // model skin sharing
    1166              : {
    1167              :     //goes and attempts a textureload for png, jpg four times using the cascading if statements, first for default then for alt directory
    1168            0 :     static auto tryload = [] (Texture *tex, std::string name, const std::string &mdir, const std::string &maltdir) -> bool
    1169              :     {
    1170            0 :         if((tex = textureload(makerelpath(mdir.c_str(), name.append(".jpg").c_str(), nullptr, nullptr), 0, true, false))==notexture)
    1171              :         {
    1172            0 :             if((tex = textureload(makerelpath(mdir.c_str(), name.append(".png").c_str(), nullptr, nullptr), 0, true, false))==notexture)
    1173              :             {
    1174            0 :                 if((tex = textureload(makerelpath(maltdir.c_str(), name.append(".jpg").c_str(), nullptr, nullptr), 0, true, false))==notexture)
    1175              :                 {
    1176            0 :                     if((tex = textureload(makerelpath(maltdir.c_str(), name.append(".png").c_str(), nullptr, nullptr), 0, true, false))==notexture)
    1177              :                     {
    1178            0 :                         return true;
    1179              :                     }
    1180              :                 }
    1181              :             }
    1182              :         }
    1183            0 :         return false;
    1184              :     };
    1185              : 
    1186            0 :     std::string mdir,
    1187            0 :                 maltdir;
    1188            0 :     mdir.append(modelpath).append(dir);
    1189            0 :     mdir.append(modelpath).append(altdir);
    1190            0 :     masks = notexture;
    1191            0 :     if(tryload(skin, "skin", mdir, maltdir))
    1192              :     {
    1193            0 :         return;
    1194              :     }
    1195            0 :     if(tryload(masks, "masks", mdir, maltdir))
    1196              :     {
    1197            0 :         return;
    1198              :     }
    1199            0 : }
    1200              : 
    1201            0 : void setbbfrommodel(dynent *d, std::string_view mdl)
    1202              : {
    1203            0 :     model *m = loadmodel(mdl);
    1204            0 :     if(!m)
    1205              :     {
    1206            0 :         return;
    1207              :     }
    1208            0 :     vec center, radius;
    1209            0 :     m->collisionbox(center, radius);
    1210            0 :     if(m->collide != Collide_Ellipse)
    1211              :     {
    1212            0 :         d->collidetype = Collide_OrientedBoundingBox;
    1213              :     }
    1214            0 :     d->xradius   = radius.x + std::fabs(center.x);
    1215            0 :     d->yradius   = radius.y + std::fabs(center.y);
    1216            0 :     d->radius    = d->collidetype==Collide_OrientedBoundingBox ? sqrtf(d->xradius*d->xradius + d->yradius*d->yradius) : std::max(d->xradius, d->yradius);
    1217            0 :     d->eyeheight = (center.z-radius.z) + radius.z*2*m->eyeheight;
    1218            0 :     d->aboveeye  = radius.z*2*(1.0f-m->eyeheight);
    1219            0 :     if (d->aboveeye + d->eyeheight <= 0.5f)
    1220              :     {
    1221            0 :         float zrad = (0.5f - (d->aboveeye + d->eyeheight)) / 2;
    1222            0 :         d->aboveeye += zrad;
    1223            0 :         d->eyeheight += zrad;
    1224              :     }
    1225              : }
    1226              : 
    1227            1 : void initrendermodelcmds()
    1228              : {
    1229            1 :     addcommand("mapmodelreset", reinterpret_cast<identfun>(mapmodel::reset), "i", Id_Command);
    1230            1 :     addcommand("mapmodel", reinterpret_cast<identfun>(mapmodel::open), "s", Id_Command);
    1231            1 :     addcommand("mapmodelname", reinterpret_cast<identfun>(mapmodel::namecmd), "ii", Id_Command);
    1232            1 :     addcommand("mapmodelloaded", reinterpret_cast<identfun>(mapmodel::loaded), "i", Id_Command);
    1233            1 :     addcommand("nummapmodels", reinterpret_cast<identfun>(mapmodel::num), "", Id_Command);
    1234            1 :     addcommand("clearmodel", reinterpret_cast<identfun>(clearmodel), "s", Id_Command);
    1235            1 :     addcommand("findanims", reinterpret_cast<identfun>(findanimscmd), "s", Id_Command);
    1236            1 : }
        

Generated by: LCOV version 2.0-1