VARP(gpuskel, 0, 1, 1);

VAR(maxskelanimdata, 1, 192, 0);

#define BONEMASK_NOT  0x8000
#define BONEMASK_END  0xFFFF
#define BONEMASK_BONE 0x7FFF

struct skelhitdata;

struct skelmodel : animmodel
{
    struct vert { vec pos, norm; vec2 tc; quat tangent; int blend, interpindex; };
    struct vvert { vec pos; hvec2 tc; squat tangent; };
    struct vvertg { hvec4 pos; hvec2 tc; squat tangent; };
    struct vvertgw : vvertg { uchar weights[4]; uchar bones[4]; };
    struct tri { ushort vert[3]; };

    struct blendcombo
    {
        int uses, interpindex;
        float weights[4];
        uchar bones[4], interpbones[4];

        blendcombo() : uses(1)
        {
        }

        bool operator==(const blendcombo &c) const
        {
            loopk(4) if(bones[k] != c.bones[k]) return false;
            loopk(4) if(weights[k] != c.weights[k]) return false;
            return true;
        }

        int size() const
        {
            int i = 1;
            while(i < 4 && weights[i]) i++;
            return i;
        }

        static bool sortcmp(const blendcombo &x, const blendcombo &y)
        {
            loopi(4)
            {
                if(x.weights[i])
                {
                    if(!y.weights[i]) return true;
                }
                else if(y.weights[i]) return false;
                else break;
            }
            return false;
        }

        int addweight(int sorted, float weight, int bone)
        {
            if(weight <= 1e-3f) return sorted;
            loopk(sorted) if(weight > weights[k])
            {
                for(int l = min(sorted-1, 2); l >= k; l--)
                {
                    weights[l+1] = weights[l];
                    bones[l+1] = bones[l];
                }
                weights[k] = weight;
                bones[k] = bone;
                return sorted<4 ? sorted+1 : sorted;
            }
            if(sorted>=4) return sorted;
            weights[sorted] = weight;
            bones[sorted] = bone;
            return sorted+1;
        }

        void finalize(int sorted)
        {
            loopj(4-sorted) { weights[sorted+j] = 0; bones[sorted+j] = 0; }
            if(sorted <= 0) return;
            float total = 0;
            loopj(sorted) total += weights[j];
            total = 1.0f/total;
            loopj(sorted) weights[j] *= total;
        }

        template<class T>
        void serialize(T &v)
        {
            if(interpindex >= 0)
            {
                v.weights[0] = 255;
                loopk(3) v.weights[k+1] = 0;
                v.bones[0] = 2*interpindex;
                loopk(3) v.bones[k+1] = v.bones[0];
            }
            else
            {
                int total = 0;
                loopk(4) total += (v.weights[k] = uchar(0.5f + weights[k]*255));
                while(total > 255)
                {
                    loopk(4) if(v.weights[k] > 0 && total > 255) { v.weights[k]--; total--; }
                }
                while(total < 255)
                {
                    loopk(4) if(v.weights[k] < 255 && total < 255) { v.weights[k]++; total++; }
                }
                loopk(4) v.bones[k] = 2*interpbones[k];
            }
        }
    };


    struct animcacheentry
    {
        animstate as[MAXANIMPARTS];
        float pitch;
        int millis;
        uchar *partmask;
        ragdolldata *ragdoll;

        animcacheentry() : ragdoll(NULL)
        {
            loopk(MAXANIMPARTS) as[k].cur.fr1 = as[k].prev.fr1 = -1;
        }

        bool operator==(const animcacheentry &c) const
        {
            loopi(MAXANIMPARTS) if(as[i]!=c.as[i]) return false;
            return pitch==c.pitch && partmask==c.partmask && ragdoll==c.ragdoll && (!ragdoll || min(millis, c.millis) >= ragdoll->lastmove);
        }

        bool operator!=(const animcacheentry &c) const
        {
            return !operator==(c);
        }
    };

    struct vbocacheentry : animcacheentry
    {
        GLuint vbuf;
        int owner;

        vbocacheentry() : vbuf(0), owner(-1) {}
    };

    struct skelcacheentry : animcacheentry
    {
        dualquat *bdata;
        int version;

        skelcacheentry() : bdata(NULL), version(-1) {}

        void nextversion()
        {
            version = Shader::uniformlocversion();
        }
    };

    struct blendcacheentry : skelcacheentry
    {
        int owner;

        blendcacheentry() : owner(-1) {}
    };

    struct skelmeshgroup;

    struct skelmesh : mesh
    {
        vert *verts;
        tri *tris;
        int numverts, numtris, maxweights;

        int voffset, eoffset, elen;
        ushort minvert, maxvert;

        skelmesh() : verts(NULL), tris(NULL), numverts(0), numtris(0), maxweights(0)
        {
        }

        virtual ~skelmesh()
        {
            DELETEA(verts);
            DELETEA(tris);
        }

        int addblendcombo(const blendcombo &c)
        {
            maxweights = max(maxweights, c.size());
            return ((skelmeshgroup *)group)->addblendcombo(c);
        }

        void smoothnorms(float limit = 0, bool areaweight = true)
        {
            mesh::smoothnorms(verts, numverts, tris, numtris, limit, areaweight);
        }

        void buildnorms(bool areaweight = true)
        {
            mesh::buildnorms(verts, numverts, tris, numtris, areaweight);
        }

        void calctangents(bool areaweight = true)
        {
            mesh::calctangents(verts, verts, numverts, tris, numtris, areaweight);
        }

        void calcbb(vec &bbmin, vec &bbmax, const matrix4x3 &m)
        {
            loopj(numverts)
            {
                vec v = m.transform(verts[j].pos);
                bbmin.min(v);
                bbmax.max(v);
            }
        }

        void genBIH(BIH::mesh &m)
        {
            m.tris = (const BIH::tri *)tris;
            m.numtris = numtris;
            m.pos = (const uchar *)&verts->pos;
            m.posstride = sizeof(vert);
            m.tc = (const uchar *)&verts->tc;
            m.tcstride = sizeof(vert);
        }

        void genshadowmesh(vector<triangle> &out, const matrix4x3 &m)
        {
            loopj(numtris)
            {
                triangle &t = out.add();
                t.a = m.transform(verts[tris[j].vert[0]].pos);
                t.b = m.transform(verts[tris[j].vert[1]].pos);
                t.c = m.transform(verts[tris[j].vert[2]].pos);
            }
        }

        static inline void assignvert(vvertg &vv, int j, vert &v, blendcombo &c)
        {
            vv.pos = hvec4(v.pos, 1);
            vv.tc = v.tc;
            vv.tangent = v.tangent;
        }

        static inline void assignvert(vvertgw &vv, int j, vert &v, blendcombo &c)
        {
            vv.pos = hvec4(v.pos, 1);
            vv.tc = v.tc;
            vv.tangent = v.tangent;
            c.serialize(vv);
        }

        template<class T>
        int genvbo(vector<ushort> &idxs, int offset, vector<T> &vverts)
        {
            voffset = offset;
            eoffset = idxs.length();
            loopi(numverts)
            {
                vert &v = verts[i];
                assignvert(vverts.add(), i, v, ((skelmeshgroup *)group)->blendcombos[v.blend]);
            }
            loopi(numtris) loopj(3) idxs.add(voffset + tris[i].vert[j]);
            elen = idxs.length()-eoffset;
            minvert = voffset;
            maxvert = voffset + numverts-1;
            return numverts;
        }

        template<class T>
        int genvbo(vector<ushort> &idxs, int offset, vector<T> &vverts, int *htdata, int htlen)
        {
            voffset = offset;
            eoffset = idxs.length();
            minvert = 0xFFFF;
            loopi(numtris)
            {
                tri &t = tris[i];
                loopj(3)
                {
                    int index = t.vert[j];
                    vert &v = verts[index];
                    T vv;
                    assignvert(vv, index, v, ((skelmeshgroup *)group)->blendcombos[v.blend]);
                    int htidx = hthash(v.pos)&(htlen-1);
                    loopk(htlen)
                    {
                        int &vidx = htdata[(htidx+k)&(htlen-1)];
                        if(vidx < 0) { vidx = idxs.add(ushort(vverts.length())); vverts.add(vv); break; }
                        else if(!memcmp(&vverts[vidx], &vv, sizeof(vv))) { minvert = min(minvert, idxs.add(ushort(vidx))); break; }
                    }
                }
            }
            elen = idxs.length()-eoffset;
            minvert = min(minvert, ushort(voffset));
            maxvert = max(minvert, ushort(vverts.length()-1));
            return vverts.length()-voffset;
        }

        int genvbo(vector<ushort> &idxs, int offset)
        {
            loopi(numverts) verts[i].interpindex = ((skelmeshgroup *)group)->remapblend(verts[i].blend);

            voffset = offset;
            eoffset = idxs.length();
            loopi(numtris)
            {
                tri &t = tris[i];
                loopj(3) idxs.add(voffset+t.vert[j]);
            }
            minvert = voffset;
            maxvert = voffset + numverts-1;
            elen = idxs.length()-eoffset;
            return numverts;
        }

        template<class T>
        static inline void fillvert(T &vv, int j, vert &v)
        {
            vv.tc = v.tc;
        }

        template<class T>
        void fillverts(T *vdata)
        {
            vdata += voffset;
            loopi(numverts) fillvert(vdata[i], i, verts[i]);
        }

        template<class T>
        void interpverts(const dualquat * RESTRICT bdata1, const dualquat * RESTRICT bdata2, T * RESTRICT vdata, skin &s)
        {
            const int blendoffset = ((skelmeshgroup *)group)->skel->numgpubones;
            bdata2 -= blendoffset;
            vdata += voffset;
            loopi(numverts)
            {
                const vert &src = verts[i];
                T &dst = vdata[i];
                const dualquat &b = (src.interpindex < blendoffset ? bdata1 : bdata2)[src.interpindex];
                dst.pos = b.transform(src.pos);
                quat q = b.transform(src.tangent);
                fixqtangent(q, src.tangent.w);
                dst.tangent = q;
            }
        }

        void setshader(Shader *s, int row)
        {
            skelmeshgroup *g = (skelmeshgroup *)group;
            if(row) s->setvariant(g->skel->usegpuskel ? min(maxweights, g->vweights) : 0, row);
            else if(g->skel->usegpuskel) s->setvariant(min(maxweights, g->vweights)-1, 0);
            else s->set();
        }

        void render(const animstate *as, skin &s, vbocacheentry &vc)
        {
            if(!Shader::lastshader) return;
            glDrawRangeElements_(GL_TRIANGLES, minvert, maxvert, elen, GL_UNSIGNED_SHORT, &((skelmeshgroup *)group)->edata[eoffset]);
            glde++;
            xtravertsva += numverts;
        }
    };

    struct tag
    {
        char *name;
        int bone;
        matrix4x3 matrix;

        tag() : name(NULL) {}
        ~tag() { DELETEA(name); }
    };

    struct skelanimspec
    {
        char *name;
        int frame, range;

        skelanimspec() : name(NULL), frame(0), range(0) {}
        ~skelanimspec()
        {
            DELETEA(name);
        }
    };

    struct boneinfo
    {
        const char *name;
        int parent, children, next, group, scheduled, interpindex, interpparent, ragdollindex, correctindex;
        float pitchscale, pitchoffset, pitchmin, pitchmax;
        dualquat base, invbase;

        boneinfo() : name(NULL), parent(-1), children(-1), next(-1), group(INT_MAX), scheduled(-1), interpindex(-1), interpparent(-1), ragdollindex(-1), correctindex(-1), pitchscale(0), pitchoffset(0), pitchmin(0), pitchmax(0) {}
        ~boneinfo()
        {
            DELETEA(name);
        }
    };

    struct antipode
    {
        int parent, child;

        antipode(int parent, int child) : parent(parent), child(child) {}
    };

    struct pitchdep
    {
        int bone, parent;
        dualquat pose;
    };

    struct pitchtarget
    {
        int bone, frame, corrects, deps;
        float pitchmin, pitchmax, deviated;
        dualquat pose;
    };

    struct pitchcorrect
    {
        int bone, target, parent;
        float pitchmin, pitchmax, pitchscale, pitchangle, pitchtotal;

        pitchcorrect() : parent(-1), pitchangle(0), pitchtotal(0) {}
    };

    struct skeleton
    {
        char *name;
        int shared;
        vector<skelmeshgroup *> users;
        boneinfo *bones;
        int numbones, numinterpbones, numgpubones, numframes;
        dualquat *framebones;
        vector<skelanimspec> skelanims;
        vector<tag> tags;
        vector<antipode> antipodes;
        ragdollskel *ragdoll;
        vector<pitchdep> pitchdeps;
        vector<pitchtarget> pitchtargets;
        vector<pitchcorrect> pitchcorrects;

        bool usegpuskel;
        vector<skelcacheentry> skelcache;
        hashtable<GLuint, int> blendoffsets;

        skeleton() : name(NULL), shared(0), bones(NULL), numbones(0), numinterpbones(0), numgpubones(0), numframes(0), framebones(NULL), ragdoll(NULL), usegpuskel(false), blendoffsets(32)
        {
        }

        ~skeleton()
        {
            DELETEA(name);
            DELETEA(bones);
            DELETEA(framebones);
            DELETEP(ragdoll);
            loopv(skelcache)
            {
                DELETEA(skelcache[i].bdata);
            }
        }

        skelanimspec *findskelanim(const char *name, char sep = '\0')
        {
            int len = sep ? strlen(name) : 0;
            loopv(skelanims)
            {
                if(skelanims[i].name)
                {
                    if(sep)
                    {
                        const char *end = strchr(skelanims[i].name, ':');
                        if(end && end - skelanims[i].name == len && !memcmp(name, skelanims[i].name, len)) return &skelanims[i];
                    }
                    if(!strcmp(name, skelanims[i].name)) return &skelanims[i];
                }
            }
            return NULL;
        }

        skelanimspec &addskelanim(const char *name)
        {
            skelanimspec &sa = skelanims.add();
            sa.name = name ? newstring(name) : NULL;
            return sa;
        }

        int findbone(const char *name)
        {
            loopi(numbones) if(bones[i].name && !strcmp(bones[i].name, name)) return i;
            return -1;
        }

        int findtag(const char *name)
        {
            loopv(tags) if(!strcmp(tags[i].name, name)) return i;
            return -1;
        }

        bool addtag(const char *name, int bone, const matrix4x3 &matrix)
        {
            int idx = findtag(name);
            if(idx >= 0)
            {
                if(!testtags) return false;
                tag &t = tags[idx];
                t.bone = bone;
                t.matrix = matrix;
            }
            else
            {
                tag &t = tags.add();
                t.name = newstring(name);
                t.bone = bone;
                t.matrix = matrix;
            }
            return true;
        }

        void calcantipodes()
        {
            antipodes.shrink(0);
            vector<int> schedule;
            loopi(numbones)
            {
                if(bones[i].group >= numbones)
                {
                    bones[i].scheduled = schedule.length();
                    schedule.add(i);
                }
                else bones[i].scheduled = -1;
            }
            loopv(schedule)
            {
                int bone = schedule[i];
                const boneinfo &info = bones[bone];
                loopj(numbones) if(abs(bones[j].group) == bone && bones[j].scheduled < 0)
                {
                    antipodes.add(antipode(info.interpindex, bones[j].interpindex));
                    bones[j].scheduled = schedule.length();
                    schedule.add(j);
                }
                if(i + 1 == schedule.length())
                {
                    int conflict = INT_MAX;
                    loopj(numbones) if(bones[j].group < numbones && bones[j].scheduled < 0) conflict = min(conflict, abs(bones[j].group));
                    if(conflict < numbones)
                    {
                        bones[conflict].scheduled = schedule.length();
                        schedule.add(conflict);
                    }
                }
            }
        }

        void remapbones()
        {
            loopi(numbones)
            {
                boneinfo &info = bones[i];
                info.interpindex = -1;
                info.ragdollindex = -1;
            }
            numgpubones = 0;
            loopv(users)
            {
                skelmeshgroup *group = users[i];
                loopvj(group->blendcombos)
                {
                    blendcombo &c = group->blendcombos[j];
                    loopk(4)
                    {
                        if(!c.weights[k]) { c.interpbones[k] = k > 0 ? c.interpbones[k-1] : 0; continue; }
                        boneinfo &info = bones[c.bones[k]];
                        if(info.interpindex < 0) info.interpindex = numgpubones++;
                        c.interpbones[k] = info.interpindex;
                        if(info.group < 0) continue;
                        loopl(4)
                        {
                            if(!c.weights[l]) break;
                            if(l == k) continue;
                            int parent = c.bones[l];
                            if(info.parent == parent || (info.parent >= 0 && info.parent == bones[parent].parent)) { info.group = -info.parent; break; }
                            if(info.group <= parent) continue;
                            int child = c.bones[k];
                            while(parent > child) parent = bones[parent].parent;
                            if(parent != child) info.group = c.bones[l];
                        }
                    }
                }
            }
            numinterpbones = numgpubones;
            loopv(tags)
            {
                boneinfo &info = bones[tags[i].bone];
                if(info.interpindex < 0) info.interpindex = numinterpbones++;
            }
            if(ragdoll)
            {
                loopv(ragdoll->joints)
                {
                    boneinfo &info = bones[ragdoll->joints[i].bone];
                    if(info.interpindex < 0) info.interpindex = numinterpbones++;
                    info.ragdollindex = i;
                }
            }
            loopi(numbones)
            {
                boneinfo &info = bones[i];
                if(info.interpindex < 0) continue;
                for(int parent = info.parent; parent >= 0 && bones[parent].interpindex < 0; parent = bones[parent].parent)
                    bones[parent].interpindex = numinterpbones++;
            }
            loopi(numbones)
            {
                boneinfo &info = bones[i];
                if(info.interpindex < 0) continue;
                info.interpparent = info.parent >= 0 ? bones[info.parent].interpindex : -1;
            }
            if(ragdoll)
            {
                loopi(numbones)
                {
                    boneinfo &info = bones[i];
                    if(info.interpindex < 0 || info.ragdollindex >= 0) continue;
                    for(int parent = info.parent; parent >= 0; parent = bones[parent].parent)
                    {
                        if(bones[parent].ragdollindex >= 0) { ragdoll->addreljoint(i, bones[parent].ragdollindex); break; }
                    }
                }
            }
            calcantipodes();
        }


        void addpitchdep(int bone, int frame)
        {
            for(; bone >= 0; bone = bones[bone].parent)
            {
                int pos = pitchdeps.length();
                loopvj(pitchdeps) if(bone <= pitchdeps[j].bone)
                {
                    if(bone == pitchdeps[j].bone) goto nextbone;
                    pos = j;
                    break;
                }
                {
                    pitchdep d;
                    d.bone = bone;
                    d.parent = -1;
                    d.pose = framebones[frame*numbones + bone];
                    pitchdeps.insert(pos, d);
                }
            nextbone:;
            }
        }

        int findpitchdep(int bone)
        {
            loopv(pitchdeps) if(bone <= pitchdeps[i].bone) return bone == pitchdeps[i].bone ? i : -1;
            return -1;
        }

        int findpitchcorrect(int bone)
        {
            loopv(pitchcorrects) if(bone <= pitchcorrects[i].bone) return bone == pitchcorrects[i].bone ? i : -1;
            return -1;
        }

        void initpitchdeps()
        {
            pitchdeps.setsize(0);
            if(pitchtargets.empty()) return;
            loopv(pitchtargets)
            {
                pitchtarget &t = pitchtargets[i];
                t.deps = -1;
                addpitchdep(t.bone, t.frame);
            }
            loopv(pitchdeps)
            {
                pitchdep &d = pitchdeps[i];
                int parent = bones[d.bone].parent;
                if(parent >= 0)
                {
                    int j = findpitchdep(parent);
                    if(j >= 0)
                    {
                        d.parent = j;
                        d.pose.mul(pitchdeps[j].pose, dualquat(d.pose));
                    }
                }
            }
            loopv(pitchtargets)
            {
                pitchtarget &t = pitchtargets[i];
                int j = findpitchdep(t.bone);
                if(j >= 0)
                {
                    t.deps = j;
                    t.pose = pitchdeps[j].pose;
                }
                t.corrects = -1;
                for(int parent = t.bone; parent >= 0; parent = bones[parent].parent)
                {
                    t.corrects = findpitchcorrect(parent);
                    if(t.corrects >= 0) break;
                }
            }
            loopv(pitchcorrects)
            {
                pitchcorrect &c = pitchcorrects[i];
                bones[c.bone].correctindex = i;
                c.parent = -1;
                for(int parent = c.bone;;)
                {
                    parent = bones[parent].parent;
                    if(parent < 0) break;
                    c.parent = findpitchcorrect(parent);
                    if(c.parent >= 0) break;
                }
            }
        }

        void optimize()
        {
            cleanup();
            if(ragdoll) ragdoll->setup();
            remapbones();
            initpitchdeps();
        }

        void expandbonemask(uchar *expansion, int bone, int val)
        {
            expansion[bone] = val;
            bone = bones[bone].children;
            while(bone>=0) { expandbonemask(expansion, bone, val); bone = bones[bone].next; }
        }

        void applybonemask(ushort *mask, uchar *partmask, int partindex)
        {
            if(!mask || *mask==BONEMASK_END) return;
            uchar *expansion = new uchar[numbones];
            memset(expansion, *mask&BONEMASK_NOT ? 1 : 0, numbones);
            while(*mask!=BONEMASK_END)
            {
                expandbonemask(expansion, *mask&BONEMASK_BONE, *mask&BONEMASK_NOT ? 0 : 1);
                mask++;
            }
            loopi(numbones) if(expansion[i]) partmask[i] = partindex;
            delete[] expansion;
        }

        void linkchildren()
        {
            loopi(numbones)
            {
                boneinfo &b = bones[i];
                b.children = -1;
                if(b.parent<0) b.next = -1;
                else
                {
                    b.next = bones[b.parent].children;
                    bones[b.parent].children = i;
                }
            }
        }

        int availgpubones() const { return min(maxvsuniforms, maxskelanimdata) / 2; }
        bool gpuaccelerate() const { return numframes && gpuskel && numgpubones<=availgpubones(); }

        float calcdeviation(const vec &axis, const vec &forward, const dualquat &pose1, const dualquat &pose2)
        {
            vec forward1 = pose1.transformnormal(forward).project(axis).normalize(),
                forward2 = pose2.transformnormal(forward).project(axis).normalize(),
                daxis = vec().cross(forward1, forward2);
            float dx = clamp(forward1.dot(forward2), -1.0f, 1.0f), dy = clamp(daxis.magnitude(), -1.0f, 1.0f);
            if(daxis.dot(axis) < 0) dy = -dy;
            return atan2f(dy, dx)/RAD;
        }

        void calcpitchcorrects(float pitch, const vec &axis, const vec &forward)
        {
            loopv(pitchtargets)
            {
                pitchtarget &t = pitchtargets[i];
                t.deviated = calcdeviation(axis, forward, t.pose, pitchdeps[t.deps].pose);
            }
            loopv(pitchcorrects)
            {
                pitchcorrect &c = pitchcorrects[i];
                c.pitchangle = c.pitchtotal = 0;
            }
            loopvj(pitchtargets)
            {
                pitchtarget &t = pitchtargets[j];
                float tpitch = pitch - t.deviated;
                for(int parent = t.corrects; parent >= 0; parent = pitchcorrects[parent].parent)
                    tpitch -= pitchcorrects[parent].pitchangle;
                if(t.pitchmin || t.pitchmax) tpitch = clamp(tpitch, t.pitchmin, t.pitchmax);
                loopv(pitchcorrects)
                {
                    pitchcorrect &c = pitchcorrects[i];
                    if(c.target != j) continue;
                    float total = c.parent >= 0 ? pitchcorrects[c.parent].pitchtotal : 0,
                          avail = tpitch - total,
                          used = tpitch*c.pitchscale;
                    if(c.pitchmin || c.pitchmax)
                    {
                        if(used < 0) used = clamp(c.pitchmin, used, 0.0f);
                        else used = clamp(c.pitchmax, 0.0f, used);
                    }
                    if(used < 0) used = clamp(avail, used, 0.0f);
                    else used = clamp(avail, 0.0f, used);
                    c.pitchangle = used;
                    c.pitchtotal = used + total;
                }
            }
        }

        #define INTERPBONE(bone) \
            const animstate &s = as[partmask[bone]]; \
            const framedata &f = partframes[partmask[bone]]; \
            dualquat d; \
            (d = f.fr1[bone]).mul((1-s.cur.t)*s.interp); \
            d.accumulate(f.fr2[bone], s.cur.t*s.interp); \
            if(s.interp<1) \
            { \
                d.accumulate(f.pfr1[bone], (1-s.prev.t)*(1-s.interp)); \
                d.accumulate(f.pfr2[bone], s.prev.t*(1-s.interp)); \
            }

        void interpbones(const animstate *as, float pitch, const vec &axis, const vec &forward, int numanimparts, const uchar *partmask, skelcacheentry &sc)
        {
            if(!sc.bdata) sc.bdata = new dualquat[numinterpbones];
            sc.nextversion();
            struct framedata
            {
                const dualquat *fr1, *fr2, *pfr1, *pfr2;
            } partframes[MAXANIMPARTS];
            loopi(numanimparts)
            {
                partframes[i].fr1 = &framebones[as[i].cur.fr1*numbones];
                partframes[i].fr2 = &framebones[as[i].cur.fr2*numbones];
                if(as[i].interp<1)
                {
                    partframes[i].pfr1 = &framebones[as[i].prev.fr1*numbones];
                    partframes[i].pfr2 = &framebones[as[i].prev.fr2*numbones];
                }
            }
            loopv(pitchdeps)
            {
                pitchdep &p = pitchdeps[i];
                INTERPBONE(p.bone);
                d.normalize();
                if(p.parent >= 0) p.pose.mul(pitchdeps[p.parent].pose, d);
                else p.pose = d;
            }
            calcpitchcorrects(pitch, axis, forward);
            loopi(numbones) if(bones[i].interpindex>=0)
            {
                INTERPBONE(i);
                d.normalize();
                const boneinfo &b = bones[i];
                if(b.interpparent<0) sc.bdata[b.interpindex] = d;
                else sc.bdata[b.interpindex].mul(sc.bdata[b.interpparent], d);

                float angle;
                if(b.pitchscale) { angle = b.pitchscale*pitch + b.pitchoffset; if(b.pitchmin || b.pitchmax) angle = clamp(angle, b.pitchmin, b.pitchmax); }
                else if(b.correctindex >= 0) angle = pitchcorrects[b.correctindex].pitchangle;
                else continue;
                if(as->cur.anim&ANIM_NOPITCH || (as->interp < 1 && as->prev.anim&ANIM_NOPITCH))
                    angle *= (as->cur.anim&ANIM_NOPITCH ? 0 : as->interp) + (as->interp < 1 && as->prev.anim&ANIM_NOPITCH ? 0 : 1-as->interp);
                sc.bdata[b.interpindex].mulorient(quat(axis, angle*RAD), b.base);
            }
            loopv(antipodes) sc.bdata[antipodes[i].child].fixantipodal(sc.bdata[antipodes[i].parent]);
        }

        void initragdoll(ragdolldata &d, skelcacheentry &sc, part *p)
        {
            const dualquat *bdata = sc.bdata;
            loopv(ragdoll->joints)
            {
                const ragdollskel::joint &j = ragdoll->joints[i];
                const boneinfo &b = bones[j.bone];
                const dualquat &q = bdata[b.interpindex];
                loopk(3) if(j.vert[k] >= 0)
                {
                    ragdollskel::vert &v = ragdoll->verts[j.vert[k]];
                    ragdolldata::vert &dv = d.verts[j.vert[k]];
                    dv.pos.add(q.transform(v.pos).mul(v.weight));
                }
            }
            if(ragdoll->animjoints) loopv(ragdoll->joints)
            {
                const ragdollskel::joint &j = ragdoll->joints[i];
                const boneinfo &b = bones[j.bone];
                const dualquat &q = bdata[b.interpindex];
                d.calcanimjoint(i, matrix4x3(q));
            }
            loopv(ragdoll->verts)
            {
                ragdolldata::vert &dv = d.verts[i];
                matrixstack[matrixpos].transform(vec(dv.pos).mul(p->model->scale), dv.pos);
            }
            loopv(ragdoll->reljoints)
            {
                const ragdollskel::reljoint &r = ragdoll->reljoints[i];
                const ragdollskel::joint &j = ragdoll->joints[r.parent];
                const boneinfo &br = bones[r.bone], &bj = bones[j.bone];
                d.reljoints[i].mul(dualquat(bdata[bj.interpindex]).invert(), bdata[br.interpindex]);
            }
        }

        void genragdollbones(ragdolldata &d, skelcacheentry &sc, part *p)
        {
            if(!sc.bdata) sc.bdata = new dualquat[numinterpbones];
            sc.nextversion();
            vec trans = vec(d.center).div(p->model->scale).add(p->model->translate);
            loopv(ragdoll->joints)
            {
                const ragdollskel::joint &j = ragdoll->joints[i];
                const boneinfo &b = bones[j.bone];
                vec pos(0, 0, 0);
                loopk(3) if(j.vert[k]>=0) pos.add(d.verts[j.vert[k]].pos);
                pos.mul(j.weight/p->model->scale).sub(trans);
                matrix4x3 m;
                m.mul(d.tris[j.tri], pos, d.animjoints ? d.animjoints[i] : j.orient);
                sc.bdata[b.interpindex] = dualquat(m);
            }
            loopv(ragdoll->reljoints)
            {
                const ragdollskel::reljoint &r = ragdoll->reljoints[i];
                const ragdollskel::joint &j = ragdoll->joints[r.parent];
                const boneinfo &br = bones[r.bone], &bj = bones[j.bone];
                sc.bdata[br.interpindex].mul(sc.bdata[bj.interpindex], d.reljoints[i]);
            }
            loopv(antipodes) sc.bdata[antipodes[i].child].fixantipodal(sc.bdata[antipodes[i].parent]);
        }

        void concattagtransform(part *p, int i, const matrix4x3 &m, matrix4x3 &n)
        {
            matrix4x3 t;
            t.mul(bones[tags[i].bone].base, tags[i].matrix);
            n.mul(m, t);
        }

        void calctags(part *p, skelcacheentry *sc = NULL)
        {
            loopv(p->links)
            {
                linkedpart &l = p->links[i];
                tag &t = tags[l.tag];
                dualquat q;
                if(sc) q.mul(sc->bdata[bones[t.bone].interpindex], bones[t.bone].base);
                else q = bones[t.bone].base;
                matrix4x3 m;
                m.mul(q, t.matrix);
                m.d.mul(p->model->scale * sizescale);
                l.matrix = m;
            }
        }

        void cleanup(bool full = true)
        {
            loopv(skelcache)
            {
                skelcacheentry &sc = skelcache[i];
                loopj(MAXANIMPARTS) sc.as[j].cur.fr1 = -1;
                DELETEA(sc.bdata);
            }
            skelcache.setsize(0);
            blendoffsets.clear();
            if(full) loopv(users) users[i]->cleanup();
        }

        bool canpreload() { return !numframes || gpuaccelerate(); }

        void preload()
        {
            if(!numframes) return;
            if(skelcache.empty())
            {
                usegpuskel = gpuaccelerate();
            }
        }

        skelcacheentry &checkskelcache(part *p, const animstate *as, float pitch, const vec &axis, const vec &forward, ragdolldata *rdata)
        {
            if(skelcache.empty())
            {
                usegpuskel = gpuaccelerate();
            }

            int numanimparts = ((skelpart *)as->owner)->numanimparts;
            uchar *partmask = ((skelpart *)as->owner)->partmask;
            skelcacheentry *sc = NULL;
            bool match = false;
            loopv(skelcache)
            {
                skelcacheentry &c = skelcache[i];
                loopj(numanimparts) if(c.as[j]!=as[j]) goto mismatch;
                if(c.pitch != pitch || c.partmask != partmask || c.ragdoll != rdata || (rdata && c.millis < rdata->lastmove)) goto mismatch;
                match = true;
                sc = &c;
                break;
            mismatch:
                if(c.millis < lastmillis) { sc = &c; break; }
            }
            if(!sc) sc = &skelcache.add();
            if(!match)
            {
                loopi(numanimparts) sc->as[i] = as[i];
                sc->pitch = pitch;
                sc->partmask = partmask;
                sc->ragdoll = rdata;
                if(rdata) genragdollbones(*rdata, *sc, p);
                else interpbones(as, pitch, axis, forward, numanimparts, partmask, *sc);
            }
            sc->millis = lastmillis;
            return *sc;
        }

        int getblendoffset(UniformLoc &u)
        {
            int &offset = blendoffsets.access(Shader::lastshader->program, -1);
            if(offset < 0)
            {
                defformatstring(offsetname, "%s[%d]", u.name, 2*numgpubones);
                offset = glGetUniformLocation_(Shader::lastshader->program, offsetname);
            }
            return offset;
        }

        void setglslbones(UniformLoc &u, skelcacheentry &sc, skelcacheentry &bc, int count)
        {
            if(u.version == bc.version && u.data == bc.bdata) return;
            glUniform4fv_(u.loc, 2*numgpubones, sc.bdata[0].real.v);
            if(count > 0)
            {
                int offset = getblendoffset(u);
                if(offset >= 0) glUniform4fv_(offset, 2*count, bc.bdata[0].real.v);
            }
            u.version = bc.version;
            u.data = bc.bdata;
        }

        void setgpubones(skelcacheentry &sc, blendcacheentry *bc, int count)
        {
            if(!Shader::lastshader) return;
            if(Shader::lastshader->uniformlocs.length() < 1) return;
            UniformLoc &u = Shader::lastshader->uniformlocs[0];
            setglslbones(u, sc, bc ? *bc : sc, count);
        }

        bool shouldcleanup() const
        {
            return numframes && (skelcache.empty() || gpuaccelerate()!=usegpuskel);
        }
    };

    static hashnameset<skeleton *> skeletons;

    struct skelmeshgroup : meshgroup
    {
        skeleton *skel;

        vector<blendcombo> blendcombos;
        int numblends[4];

        static const int MAXBLENDCACHE = 16;
        blendcacheentry blendcache[MAXBLENDCACHE];

        static const int MAXVBOCACHE = 16;
        vbocacheentry vbocache[MAXVBOCACHE];

        ushort *edata;
        GLuint ebuf;
        int vlen, vertsize, vblends, vweights;
        uchar *vdata;

        skelhitdata *hitdata;

        skelmeshgroup() : skel(NULL), edata(NULL), ebuf(0), vlen(0), vertsize(0), vblends(0), vweights(0), vdata(NULL), hitdata(NULL)
        {
            memset(numblends, 0, sizeof(numblends));
        }

        virtual ~skelmeshgroup()
        {
            if(skel)
            {
                if(skel->shared) skel->users.removeobj(this);
                else DELETEP(skel);
            }
            if(ebuf) glDeleteBuffers_(1, &ebuf);
            loopi(MAXBLENDCACHE)
            {
                DELETEA(blendcache[i].bdata);
            }
            loopi(MAXVBOCACHE)
            {
                if(vbocache[i].vbuf) glDeleteBuffers_(1, &vbocache[i].vbuf);
            }
            DELETEA(vdata);
            deletehitdata();
        }

        void shareskeleton(const char *name)
        {
            if(!name)
            {
                skel = new skeleton;
                skel->users.add(this);
                return;
            }

            if(skeletons.access(name)) skel = skeletons[name];
            else
            {
                skel = new skeleton;
                skel->name = newstring(name);
                skeletons.add(skel);
            }
            skel->users.add(this);
            skel->shared++;
        }

        int findtag(const char *name)
        {
            return skel->findtag(name);
        }

        void *animkey() { return skel; }
        int totalframes() const { return max(skel->numframes, 1); }

        virtual skelanimspec *loadanim(const char *filename) { return NULL; }

        void genvbo(vbocacheentry &vc)
        {
            if(!vc.vbuf) glGenBuffers_(1, &vc.vbuf);
            if(ebuf) return;

            vector<ushort> idxs;

            vlen = 0;
            vblends = 0;
            if(skel->numframes && !skel->usegpuskel)
            {
                vweights = 1;
                loopv(blendcombos)
                {
                    blendcombo &c = blendcombos[i];
                    c.interpindex = c.weights[1] ? skel->numgpubones + vblends++ : -1;
                }

                vertsize = sizeof(vvert);
                looprendermeshes(skelmesh, m, vlen += m.genvbo(idxs, vlen));
                DELETEA(vdata);
                vdata = new uchar[vlen*vertsize];
                looprendermeshes(skelmesh, m,
                {
                    m.fillverts((vvert *)vdata);
                });
            }
            else
            {
                if(skel->numframes)
                {
                    vweights = 4;
                    int availbones = skel->availgpubones() - skel->numgpubones;
                    while(vweights > 1 && availbones >= numblends[vweights-1]) availbones -= numblends[--vweights];
                    loopv(blendcombos)
                    {
                        blendcombo &c = blendcombos[i];
                        c.interpindex = c.size() > vweights ? skel->numgpubones + vblends++ : -1;
                    }
                }
                else
                {
                    vweights = 0;
                    loopv(blendcombos) blendcombos[i].interpindex = -1;
                }

                gle::bindvbo(vc.vbuf);
                #define GENVBO(type, args) \
                    do \
                    { \
                        vertsize = sizeof(type); \
                        vector<type> vverts; \
                        looprendermeshes(skelmesh, m, vlen += m.genvbo args); \
                        glBufferData_(GL_ARRAY_BUFFER, vverts.length()*sizeof(type), vverts.getbuf(), GL_STATIC_DRAW); \
                    } while(0)
                #define GENVBOANIM(type) GENVBO(type, (idxs, vlen, vverts))
                #define GENVBOSTAT(type) GENVBO(type, (idxs, vlen, vverts, htdata, htlen))
                if(skel->numframes) GENVBOANIM(vvertgw);
                else
                {
                    int numverts = 0, htlen = 128;
                    looprendermeshes(skelmesh, m, numverts += m.numverts);
                    while(htlen < numverts) htlen *= 2;
                    if(numverts*4 > htlen*3) htlen *= 2;
                    int *htdata = new int[htlen];
                    memset(htdata, -1, htlen*sizeof(int));
                    GENVBOSTAT(vvertg);
                    delete[] htdata;
                }
                #undef GENVBO
                #undef GENVBOANIM
                #undef GENVBOSTAT
                gle::clearvbo();
            }

            glGenBuffers_(1, &ebuf);
            gle::bindebo(ebuf);
            glBufferData_(GL_ELEMENT_ARRAY_BUFFER, idxs.length()*sizeof(ushort), idxs.getbuf(), GL_STATIC_DRAW);
            gle::clearebo();
        }

        template<class T>
        void bindbones(T *vverts) { if(enablebones) disablebones(); }
        void bindbones(vvertgw *vverts) { meshgroup::bindbones(vverts->weights, vverts->bones, vertsize); }

        template<class T>
        void bindvbo(const animstate *as, part *p, vbocacheentry &vc)
        {
            T *vverts = 0;
            bindpos(ebuf, vc.vbuf, &vverts->pos, vertsize);
            if(as->cur.anim&ANIM_NOSKIN)
            {
                if(enabletangents) disabletangents();

                if(p->alphatested()) bindtc(&vverts->tc, vertsize);
                else if(enabletc) disabletc();
            }
            else
            {
                bindtangents(&vverts->tangent, vertsize);

                bindtc(&vverts->tc, vertsize);
            }
            bindbones(vverts);
        }

        void bindvbo(const animstate *as, part *p, vbocacheentry &vc, skelcacheentry *sc = NULL, blendcacheentry *bc = NULL)
        {
            if(!skel->numframes) bindvbo<vvertg>(as, p, vc);
            else if(skel->usegpuskel) bindvbo<vvertgw>(as, p, vc);
            else bindvbo<vvert>(as, p, vc);
        }

        void concattagtransform(part *p, int i, const matrix4x3 &m, matrix4x3 &n)
        {
            skel->concattagtransform(p, i, m, n);
        }

        int addblendcombo(const blendcombo &c)
        {
            loopv(blendcombos) if(blendcombos[i]==c)
            {
                blendcombos[i].uses += c.uses;
                return i;
            }
            numblends[c.size()-1]++;
            blendcombo &a = blendcombos.add(c);
            return a.interpindex = blendcombos.length()-1;
        }

        void sortblendcombos()
        {
            blendcombos.sort(blendcombo::sortcmp);
            int *remap = new int[blendcombos.length()];
            loopv(blendcombos) remap[blendcombos[i].interpindex] = i;
            looprendermeshes(skelmesh, m,
            {
                loopj(m.numverts)
                {
                    vert &v = m.verts[j];
                    v.blend = remap[v.blend];
                }
            });
            delete[] remap;
        }

        int remapblend(int blend)
        {
            const blendcombo &c = blendcombos[blend];
            return c.weights[1] ? c.interpindex : c.interpbones[0];
        }

        static inline void blendbones(dualquat &d, const dualquat *bdata, const blendcombo &c)
        {
            d = bdata[c.interpbones[0]];
            d.mul(c.weights[0]);
            d.accumulate(bdata[c.interpbones[1]], c.weights[1]);
            if(c.weights[2])
            {
                d.accumulate(bdata[c.interpbones[2]], c.weights[2]);
                if(c.weights[3]) d.accumulate(bdata[c.interpbones[3]], c.weights[3]);
            }
        }

        void blendbones(const skelcacheentry &sc, blendcacheentry &bc)
        {
            bc.nextversion();
            if(!bc.bdata) bc.bdata = new dualquat[vblends];
            dualquat *dst = bc.bdata - skel->numgpubones;
            bool normalize = !skel->usegpuskel || vweights<=1;
            loopv(blendcombos)
            {
                const blendcombo &c = blendcombos[i];
                if(c.interpindex<0) break;
                dualquat &d = dst[c.interpindex];
                blendbones(d, sc.bdata, c);
                if(normalize) d.normalize();
            }
        }

        static inline void blendbones(const dualquat *bdata, dualquat *dst, const blendcombo *c, int numblends)
        {
            loopi(numblends)
            {
                dualquat &d = dst[i];
                blendbones(d, bdata, c[i]);
                d.normalize();
            }
        }

        void cleanup()
        {
            loopi(MAXBLENDCACHE)
            {
                blendcacheentry &c = blendcache[i];
                DELETEA(c.bdata);
                c.owner = -1;
            }
            loopi(MAXVBOCACHE)
            {
                vbocacheentry &c = vbocache[i];
                if(c.vbuf) { glDeleteBuffers_(1, &c.vbuf); c.vbuf = 0; }
                c.owner = -1;
            }
            if(ebuf) { glDeleteBuffers_(1, &ebuf); ebuf = 0; }
            if(skel) skel->cleanup(false);
            cleanuphitdata();
        }

        #define SEARCHCACHE(cachesize, cacheentry, cache, reusecheck) \
            loopi(cachesize) \
            { \
                cacheentry &c = cache[i]; \
                if(c.owner==owner) \
                { \
                     if(c==sc) return c; \
                     else c.owner = -1; \
                     break; \
                } \
            } \
            loopi(cachesize-1) \
            { \
                cacheentry &c = cache[i]; \
                if(reusecheck c.owner < 0 || c.millis < lastmillis) \
                    return c; \
            } \
            return cache[cachesize-1];

        vbocacheentry &checkvbocache(skelcacheentry &sc, int owner)
        {
            SEARCHCACHE(MAXVBOCACHE, vbocacheentry, vbocache, !c.vbuf || );
        }

        blendcacheentry &checkblendcache(skelcacheentry &sc, int owner)
        {
            SEARCHCACHE(MAXBLENDCACHE, blendcacheentry, blendcache, )
        }

        void cleanuphitdata();
        void deletehitdata();
        void buildhitdata(const uchar *hitzones);
        void intersect(skelhitdata *z, part *p, const skelmodel::skelcacheentry &sc, const vec &o, const vec &ray);

        void intersect(const animstate *as, float pitch, const vec &axis, const vec &forward, dynent *d, part *p, const vec &o, const vec &ray)
        {
            if(!hitdata) return;

            if(skel->shouldcleanup()) skel->cleanup();

            skelcacheentry &sc = skel->checkskelcache(p, as, pitch, axis, forward, !d || !d->ragdoll || d->ragdoll->skel != skel->ragdoll || d->ragdoll->millis == lastmillis ? NULL : d->ragdoll);

            intersect(hitdata, p, sc, o, ray);

            skel->calctags(p, &sc);
        }

        void preload(part *p)
        {
            if(!skel->canpreload()) return;
            if(skel->shouldcleanup()) skel->cleanup();
            skel->preload();
            if(!vbocache->vbuf) genvbo(*vbocache);
        }

        void render(const animstate *as, float pitch, const vec &axis, const vec &forward, dynent *d, part *p)
        {
            if(skel->shouldcleanup()) { skel->cleanup(); disablevbo(); }

            if(!skel->numframes)
            {
                if(!(as->cur.anim&ANIM_NORENDER))
                {
                    if(!vbocache->vbuf) genvbo(*vbocache);
                    bindvbo(as, p, *vbocache);
                    looprendermeshes(skelmesh, m,
                    {
                        p->skins[i].bind(m, as);
                        m.render(as, p->skins[i], *vbocache);
                    });
                }
                skel->calctags(p);
                return;
            }

            skelcacheentry &sc = skel->checkskelcache(p, as, pitch, axis, forward, !d || !d->ragdoll || d->ragdoll->skel != skel->ragdoll || d->ragdoll->millis == lastmillis ? NULL : d->ragdoll);
            if(!(as->cur.anim&ANIM_NORENDER))
            {
                int owner = &sc-&skel->skelcache[0];
                vbocacheentry &vc = skel->usegpuskel ? *vbocache : checkvbocache(sc, owner);
                vc.millis = lastmillis;
                if(!vc.vbuf) genvbo(vc);
                blendcacheentry *bc = NULL;
                if(vblends)
                {
                    bc = &checkblendcache(sc, owner);
                    bc->millis = lastmillis;
                    if(bc->owner!=owner)
                    {
                        bc->owner = owner;
                        *(animcacheentry *)bc = sc;
                        blendbones(sc, *bc);
                    }
                }
                if(!skel->usegpuskel && vc.owner != owner)
                {
                    vc.owner = owner;
                    (animcacheentry &)vc = sc;
                    looprendermeshes(skelmesh, m,
                    {
                        m.interpverts(sc.bdata, bc ? bc->bdata : NULL, (vvert *)vdata, p->skins[i]);
                    });
                    gle::bindvbo(vc.vbuf);
                    glBufferData_(GL_ARRAY_BUFFER, vlen*vertsize, vdata, GL_STREAM_DRAW);
                }

                bindvbo(as, p, vc, &sc, bc);

                looprendermeshes(skelmesh, m,
                {
                    p->skins[i].bind(m, as);
                    if(skel->usegpuskel) skel->setgpubones(sc, bc, vblends);
                    m.render(as, p->skins[i], vc);
                });
            }

            skel->calctags(p, &sc);

            if(as->cur.anim&ANIM_RAGDOLL && skel->ragdoll && !d->ragdoll)
            {
                d->ragdoll = new ragdolldata(skel->ragdoll, p->model->scale);
                skel->initragdoll(*d->ragdoll, sc, p);
                d->ragdoll->init(d);
            }
        }

        virtual bool load(const char *name, float smooth) = 0;
    };

    virtual skelmeshgroup *newmeshes() = 0;

    meshgroup *loadmeshes(const char *name, const char *skelname = NULL, float smooth = 2)
    {
        skelmeshgroup *group = newmeshes();
        group->shareskeleton(skelname);
        if(!group->load(name, smooth)) { delete group; return NULL; }
        return group;
    }

    meshgroup *sharemeshes(const char *name, const char *skelname = NULL, float smooth = 2)
    {
        if(!meshgroups.access(name))
        {
            meshgroup *group = loadmeshes(name, skelname, smooth);
            if(!group) return NULL;
            meshgroups.add(group);
        }
        return meshgroups[name];
    }

    struct animpartmask
    {
        animpartmask *next;
        int numbones;
        uchar bones[1];
    };

    struct skelpart : part
    {
        animpartmask *buildingpartmask;

        uchar *partmask;

        skelpart(animmodel *model, int index = 0) : part(model, index), buildingpartmask(NULL), partmask(NULL)
        {
        }

        virtual ~skelpart()
        {
            DELETEA(buildingpartmask);
        }

        uchar *sharepartmask(animpartmask *o)
        {
            static animpartmask *partmasks = NULL;
            animpartmask *p = partmasks;
            for(; p; p = p->next) if(p->numbones==o->numbones && !memcmp(p->bones, o->bones, p->numbones))
            {
                delete[] (uchar *)o;
                return p->bones;
            }

            o->next = p;
            partmasks = o;
            return o->bones;
        }

        animpartmask *newpartmask()
        {
            animpartmask *p = (animpartmask *)new uchar[sizeof(animpartmask) + ((skelmeshgroup *)meshes)->skel->numbones-1];
            p->numbones = ((skelmeshgroup *)meshes)->skel->numbones;
            memset(p->bones, 0, p->numbones);
            return p;
        }

        void initanimparts()
        {
            DELETEA(buildingpartmask);
            buildingpartmask = newpartmask();
        }

        bool addanimpart(ushort *bonemask)
        {
            if(!buildingpartmask || numanimparts>=MAXANIMPARTS) return false;
            ((skelmeshgroup *)meshes)->skel->applybonemask(bonemask, buildingpartmask->bones, numanimparts);
            numanimparts++;
            return true;
        }

        void endanimparts()
        {
            if(buildingpartmask)
            {
                partmask = sharepartmask(buildingpartmask);
                buildingpartmask = NULL;
            }

            ((skelmeshgroup *)meshes)->skel->optimize();
        }

        void loaded()
        {
            endanimparts();
            part::loaded();
        }
    };

    skelmodel(const char *name) : animmodel(name)
    {
    }

    int linktype(animmodel *m, part *p) const
    {
        return type()==m->type() &&
            ((skelmeshgroup *)parts[0]->meshes)->skel == ((skelmeshgroup *)p->meshes)->skel ?
                LINK_REUSE :
                LINK_TAG;
    }

    bool skeletal() const { return true; }

    skelpart &addpart()
    {
        flushpart();
        skelpart *p = new skelpart(this, parts.length());
        parts.add(p);
        return *p;
    }
};

hashnameset<skelmodel::skeleton *> skelmodel::skeletons;

struct skeladjustment
{
    float yaw, pitch, roll;
    vec translate;

    skeladjustment(float yaw, float pitch, float roll, const vec &translate) : yaw(yaw), pitch(pitch), roll(roll), translate(translate) {}

    void adjust(dualquat &dq)
    {
        if(yaw) dq.mulorient(quat(vec(0, 0, 1), yaw*RAD));
        if(pitch) dq.mulorient(quat(vec(0, -1, 0), pitch*RAD));
        if(roll) dq.mulorient(quat(vec(-1, 0, 0), roll*RAD));
        if(!translate.iszero()) dq.translate(translate);
    }
};

template<class MDL> struct skelloader : modelloader<MDL, skelmodel>
{
    static vector<skeladjustment> adjustments;
    static vector<uchar> hitzones;

    skelloader(const char *name) : modelloader<MDL, skelmodel>(name) {}

    void flushpart()
    {
        if(hitzones.length() && skelmodel::parts.length())
        {
            skelmodel::skelpart *p = (skelmodel::skelpart *)skelmodel::parts.last();
            skelmodel::skelmeshgroup *m = (skelmodel::skelmeshgroup *)p->meshes;
            if(m) m->buildhitdata(hitzones.getbuf());
        }

        adjustments.setsize(0);
        hitzones.setsize(0);
    }
};

template<class MDL> vector<skeladjustment> skelloader<MDL>::adjustments;
template<class MDL> vector<uchar> skelloader<MDL>::hitzones;

template<class MDL> struct skelcommands : modelcommands<MDL, struct MDL::skelmesh>
{
    typedef modelcommands<MDL, struct MDL::skelmesh> commands;
    typedef struct MDL::skeleton skeleton;
    typedef struct MDL::skelmeshgroup meshgroup;
    typedef struct MDL::skelpart part;
    typedef struct MDL::skin skin;
    typedef struct MDL::boneinfo boneinfo;
    typedef struct MDL::skelanimspec animspec;
    typedef struct MDL::pitchdep pitchdep;
    typedef struct MDL::pitchtarget pitchtarget;
    typedef struct MDL::pitchcorrect pitchcorrect;

    static void loadpart(char *meshfile, char *skelname, float *smooth)
    {
        if(!MDL::loading) { conoutf("not loading an %s", MDL::formatname()); return; }
        defformatstring(filename, "%s/%s", MDL::dir, meshfile);
        part &mdl = MDL::loading->addpart();
        mdl.meshes = MDL::loading->sharemeshes(path(filename), skelname[0] ? skelname : NULL, *smooth > 0 ? cosf(clamp(*smooth, 0.0f, 180.0f)*RAD) : 2);
        if(!mdl.meshes) conoutf("could not load %s", filename);
        else
        {
            if(mdl.meshes && ((meshgroup *)mdl.meshes)->skel->numbones > 0) mdl.disablepitch();  
            mdl.initanimparts();
            mdl.initskins();
        }
    }

    static void settag(char *name, char *tagname, float *tx, float *ty, float *tz, float *rx, float *ry, float *rz)
    {
        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf("not loading an %s", MDL::formatname()); return; }
        part &mdl = *(part *)MDL::loading->parts.last();
        int i = mdl.meshes ? ((meshgroup *)mdl.meshes)->skel->findbone(name) : -1;
        if(i >= 0)
        {
            float cx = *rx ? cosf(*rx/2*RAD) : 1, sx = *rx ? sinf(*rx/2*RAD) : 0,
                  cy = *ry ? cosf(*ry/2*RAD) : 1, sy = *ry ? sinf(*ry/2*RAD) : 0,
                  cz = *rz ? cosf(*rz/2*RAD) : 1, sz = *rz ? sinf(*rz/2*RAD) : 0;
            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)),
                        vec(*tx, *ty, *tz));
            ((meshgroup *)mdl.meshes)->skel->addtag(tagname, i, m);
            return;
        }
        conoutf("could not find bone %s for tag %s", name, tagname);
    }

    static void setpitch(char *name, float *pitchscale, float *pitchoffset, float *pitchmin, float *pitchmax)
    {
        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf("not loading an %s", MDL::formatname()); return; }
        part &mdl = *(part *)MDL::loading->parts.last();

        if(name[0])
        {
            int i = mdl.meshes ? ((meshgroup *)mdl.meshes)->skel->findbone(name) : -1;
            if(i>=0)
            {
                boneinfo &b = ((meshgroup *)mdl.meshes)->skel->bones[i];
                b.pitchscale = *pitchscale;
                b.pitchoffset = *pitchoffset;
                if(*pitchmin || *pitchmax)
                {
                    b.pitchmin = *pitchmin;
                    b.pitchmax = *pitchmax;
                }
                else
                {
                    b.pitchmin = -360*fabs(b.pitchscale) + b.pitchoffset;
                    b.pitchmax = 360*fabs(b.pitchscale) + b.pitchoffset;
                }
                return;
            }
            conoutf("could not find bone %s to pitch", name);
            return;
        }

        mdl.pitchscale = *pitchscale;
        mdl.pitchoffset = *pitchoffset;
        if(*pitchmin || *pitchmax)
        {
            mdl.pitchmin = *pitchmin;
            mdl.pitchmax = *pitchmax;
        }
        else
        {
            mdl.pitchmin = -360*fabs(mdl.pitchscale) + mdl.pitchoffset;
            mdl.pitchmax = 360*fabs(mdl.pitchscale) + mdl.pitchoffset;
        }
    }

    static void setpitchtarget(char *name, char *animfile, int *frameoffset, float *pitchmin, float *pitchmax)
    {
        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf("\frnot loading an %s", MDL::formatname()); return; }
        part &mdl = *(part *)MDL::loading->parts.last();
        if(!mdl.meshes) return;
        defformatstring(filename, "%s/%s", MDL::dir, animfile);
        animspec *sa = ((meshgroup *)mdl.meshes)->loadanim(path(filename));
        if(!sa) { conoutf("\frcould not load %s anim file %s", MDL::formatname(), filename); return; }
        skeleton *skel = ((meshgroup *)mdl.meshes)->skel;
        int bone = skel ? skel->findbone(name) : -1;
        if(bone < 0)
        {
            conoutf("\frcould not find bone %s to pitch target", name);
            return;
        }
        loopv(skel->pitchtargets) if(skel->pitchtargets[i].bone == bone) return;
        pitchtarget &t = skel->pitchtargets.add();
        t.bone = bone;
        t.frame = sa->frame + clamp(*frameoffset, 0, sa->range-1);
        t.pitchmin = *pitchmin;
        t.pitchmax = *pitchmax;
    }

    static void setpitchcorrect(char *name, char *targetname, float *scale, float *pitchmin, float *pitchmax)
    {
        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf("\frnot loading an %s", MDL::formatname()); return; }
        part &mdl = *(part *)MDL::loading->parts.last();
        if(!mdl.meshes) return;
        skeleton *skel = ((meshgroup *)mdl.meshes)->skel;
        int bone = skel ? skel->findbone(name) : -1;
        if(bone < 0)
        {
            conoutf("\frcould not find bone %s to pitch correct", name);
            return;
        }
        if(skel->findpitchcorrect(bone) >= 0) return;
        int targetbone = skel->findbone(targetname), target = -1;
        if(targetbone >= 0) loopv(skel->pitchtargets) if(skel->pitchtargets[i].bone == targetbone) { target = i; break; }
        if(target < 0)
        {
            conoutf("\frcould not find pitch target %s to pitch correct %s", targetname, name);
            return;
        }
        pitchcorrect c;
        c.bone = bone;
        c.target = target;
        c.pitchmin = *pitchmin;
        c.pitchmax = *pitchmax;
        c.pitchscale = *scale;
        int pos = skel->pitchcorrects.length();
        loopv(skel->pitchcorrects) if(bone <= skel->pitchcorrects[i].bone) { pos = i; break; }
        skel->pitchcorrects.insert(pos, c);
    }

    static void setanim(char *anim, char *animfile, float *speed, int *priority, int *startoffset, int *endoffset)
    {
        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf("not loading an %s", MDL::formatname()); return; }

        vector<int> anims;
        game::findanims(anim, anims);
        if(anims.empty()) conoutf("could not find animation %s", anim);
        else
        {
            part *p = (part *)MDL::loading->parts.last();
            if(!p->meshes) return;
            defformatstring(filename, "%s/%s", MDL::dir, animfile);
            animspec *sa = ((meshgroup *)p->meshes)->loadanim(path(filename));
            if(!sa) conoutf("could not load %s anim file %s", MDL::formatname(), filename);
            else loopv(anims)
            {
                int start = sa->frame, end = sa->range;
                if(*startoffset > 0) start += min(*startoffset, end-1);
                else if(*startoffset < 0) start += max(end + *startoffset, 0);
                end -= start - sa->frame;
                if(*endoffset > 0) end = min(end, *endoffset);
                else if(*endoffset < 0) end = max(end + *endoffset, 1);
                MDL::loading->parts.last()->setanim(p->numanimparts-1, anims[i], start, end, *speed, *priority);
            }
        }
    }

    static void setanimpart(char *maskstr)
    {
        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf("not loading an %s", MDL::formatname()); return; }

        part *p = (part *)MDL::loading->parts.last();

        vector<char *> bonestrs;
        explodelist(maskstr, bonestrs);
        vector<ushort> bonemask;
        loopv(bonestrs)
        {
            char *bonestr = bonestrs[i];
            int bone = p->meshes ? ((meshgroup *)p->meshes)->skel->findbone(bonestr[0]=='!' ? bonestr+1 : bonestr) : -1;
            if(bone<0) { conoutf("could not find bone %s for anim part mask [%s]", bonestr, maskstr); bonestrs.deletearrays(); return; }
            bonemask.add(bone | (bonestr[0]=='!' ? BONEMASK_NOT : 0));
        }
        bonestrs.deletearrays();
        bonemask.sort();
        if(bonemask.length()) bonemask.add(BONEMASK_END);

        if(!p->addanimpart(bonemask.getbuf())) conoutf("too many animation parts");
    }

    static void setadjust(char *name, float *yaw, float *pitch, float *roll, float *tx, float *ty, float *tz)
    {
        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf("not loading an %s", MDL::formatname()); return; }
        part &mdl = *(part *)MDL::loading->parts.last();

        if(!name[0]) return;
        int i = mdl.meshes ? ((meshgroup *)mdl.meshes)->skel->findbone(name) : -1;
        if(i < 0) {  conoutf("could not find bone %s to adjust", name); return; }
        while(!MDL::adjustments.inrange(i)) MDL::adjustments.add(skeladjustment(0, 0, 0, vec(0, 0, 0)));
        MDL::adjustments[i] = skeladjustment(*yaw, *pitch, *roll, vec(*tx/4, *ty/4, *tz/4));
    }

    static void sethitzone(int *id, char *maskstr)
    {
        if(!MDL::loading || MDL::loading->parts.empty()) { conoutf("not loading an %s", MDL::formatname()); return; }
        if(*id >= 0x80) { conoutf("invalid hit zone id %d", *id); return; }

        part *p = (part *)MDL::loading->parts.last();
        meshgroup *m = (meshgroup *)p->meshes;
        if(!m || m->hitdata) return;

        vector<char *> bonestrs;
        explodelist(maskstr, bonestrs);
        vector<ushort> bonemask;
        loopv(bonestrs)
        {
            char *bonestr = bonestrs[i];
            int bone = p->meshes ? ((meshgroup *)p->meshes)->skel->findbone(bonestr[0]=='!' ? bonestr+1 : bonestr) : -1;
            if(bone<0) { conoutf("could not find bone %s for hit zone mask [%s]", bonestr, maskstr); bonestrs.deletearrays(); return; }
            bonemask.add(bone | (bonestr[0]=='!' ? BONEMASK_NOT : 0));
        }
        bonestrs.deletearrays();
        if(bonemask.empty()) return;
        bonemask.sort();
        bonemask.add(BONEMASK_END);

        while(MDL::hitzones.length() < m->skel->numbones) MDL::hitzones.add(0xFF);
        m->skel->applybonemask(bonemask.getbuf(), MDL::hitzones.getbuf(), *id < 0 ? 0xFF : *id);
    }

    skelcommands()
    {
        if(MDL::multiparted()) this->modelcommand(loadpart, "load", "ssf");
        this->modelcommand(settag, "tag", "ssffffff");
        this->modelcommand(setpitch, "pitch", "sffff");
        this->modelcommand(setpitchtarget, "pitchtarget", "ssiff");
        this->modelcommand(setpitchcorrect, "pitchcorrect", "ssfff");
        this->modelcommand(sethitzone, "hitzone", "is");
        if(MDL::cananimate())
        {
            this->modelcommand(setanim, "anim", "ssfiii");
            this->modelcommand(setanimpart, "animpart", "s");
            this->modelcommand(setadjust, "adjust", "sffffff");
        }
    }
};

