/*
Copyright (C) 1996-2001 Id Software, Inc.
Copyright (C) 2002-2009 John Fitzgibbons and others
Copyright (C) 2010-2014 QuakeSpasm developers
Copyright (C) 2020-2021 Vittorio Romeo

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

*/
// models.c -- model loading and caching

// models are the only shared resource between a client and server running
// on the same machine.

#include <GL/glew.h>

#include "host.hpp"
#include "mathlib.hpp"
#include "quakedef.hpp"
#include "common.hpp"
#include "quakeglm_qvec3.hpp"
#include "quakeglm_qvec3_togl.hpp"
#include "console.hpp"
#include "quakedef_macros.hpp"
#include "image.hpp"
#include "glquake.hpp"
#include "byteorder.hpp"
#include "gl_texmgr.hpp"
#include "server.hpp"
#include "sys.hpp"
#include "srcformat.hpp"

qmodel_t* loadmodel;
char loadname[32]; // for hunk tags

void Mod_LoadSpriteModel(qmodel_t* mod, void* buffer);
void Mod_LoadBrushModel(qmodel_t* mod, void* buffer);
void Mod_LoadAliasModel(qmodel_t* mod, void* buffer, int pvtype);
void Mod_LoadMD3Model(qmodel_t* mod, void* buffer);
void Mod_LoadIQMModel(qmodel_t* mod, const void* buffer);
qmodel_t* Mod_LoadModel(qmodel_t* mod, bool crash);

cvar_t external_ents = {"external_ents", "1", CVAR_ARCHIVE};
cvar_t gl_load24bit = {"gl_load24bit", "1", CVAR_ARCHIVE};
cvar_t mod_ignorelmscale = {"mod_ignorelmscale", "0"};

static byte* mod_novis;
static int mod_novis_capacity;

static byte* mod_decompressed;
static int mod_decompressed_capacity;

#define MAX_MOD_KNOWN                                                          \
    8192 /*spike -- new value, was 2048 in qs, 512 in vanilla. Needs to be big \
            for big maps with many many inline models. */
qmodel_t mod_known[MAX_MOD_KNOWN];
int mod_numknown;

texture_t* r_notexture_mip;  // johnfitz -- moved here from r_main.c
texture_t* r_notexture_mip2; // johnfitz -- used for non-lightmapped surfs with
                             // a missing texture

/*
===============
Mod_Init
===============
*/
void Mod_Init()
{
    Cvar_RegisterVariable(&gl_subdivide_size);
    Cvar_RegisterVariable(&external_ents);
    Cvar_RegisterVariable(&gl_load24bit);
    Cvar_RegisterVariable(&mod_ignorelmscale);

    // johnfitz -- create notexture miptex
    r_notexture_mip =
        (texture_t*)Hunk_AllocName(sizeof(texture_t), "r_notexture_mip");
    strcpy(r_notexture_mip->name, "notexture");
    r_notexture_mip->height = r_notexture_mip->width = 32;

    r_notexture_mip2 =
        (texture_t*)Hunk_AllocName(sizeof(texture_t), "r_notexture_mip2");
    strcpy(r_notexture_mip2->name, "notexture2");
    r_notexture_mip2->height = r_notexture_mip2->width = 32;
    // johnfitz
}

/*
===============
Mod_Extradata

Caches the data if needed
===============
*/
void* Mod_Extradata(qmodel_t* mod)
{
    void* r = Cache_Check(&mod->cache);
    if(r)
    {
        return r;
    }

    Mod_LoadModel(mod, true);

    if(!mod->cache.data)
    {
        Sys_Error("Mod_Extradata: caching failed");
    }
    return mod->cache.data;
}

/*
===============
Mod_PointInLeaf
===============
*/
mleaf_t* Mod_PointInLeaf(const qvec3& p, qmodel_t* model)
{
    mnode_t* node;
    float d;
    mplane_t* plane;

    if(!model || !model->nodes)
    {
        Sys_Error("Mod_PointInLeaf: bad model");
    }

    node = model->nodes + model->hulls[0].firstclipnode;
    while(true)
    {
        if(node->contents < 0)
        {
            return (mleaf_t*)node;
        }
        plane = node->plane;
        d = DotProduct(p, plane->normal) - plane->dist;
        if(d > 0)
        {
            node = node->children[0];
        }
        else
        {
            node = node->children[1];
        }
    }

    return nullptr; // never reached
}


/*
===================
Mod_DecompressVis
===================
*/
byte* Mod_DecompressVis(byte* in, qmodel_t* model)
{
    int c;
    byte* out;
    byte* outend;
    int row;

    row = (model->numleafs + 7) >> 3;
    if(mod_decompressed == nullptr || row > mod_decompressed_capacity)
    {
        mod_decompressed_capacity = row;
        mod_decompressed =
            (byte*)realloc(mod_decompressed, mod_decompressed_capacity);
        if(!mod_decompressed)
        {
            Sys_Error("Mod_DecompressVis: realloc() failed on %d bytes",
                mod_decompressed_capacity);
        }
    }
    out = mod_decompressed;
    outend = mod_decompressed + row;

    if(!in)
    {
        // no vis info, so make all visible
        while(row)
        {
            *out++ = 0xff;
            row--;
        }
        return mod_decompressed;
    }

    do
    {
        if(*in)
        {
            *out++ = *in++;
            continue;
        }

        c = in[1];
        in += 2;
        if(c > row - (out - mod_decompressed))
        {
            c = row - (out - mod_decompressed); // now that we're dynamically
        }
        // allocating pvs buffers, we have to
        // be more careful to avoid heap
        // overflows with buggy maps.
        while(c)
        {
            if(out == outend)
            {
                if(!model->viswarn)
                {
                    model->viswarn = true;
                    Con_Warning(
                        "Mod_DecompressVis: output overrun on model \"%s\"\n",
                        model->name);
                }
                return mod_decompressed;
            }
            *out++ = 0;
            c--;
        }
    } while(out - mod_decompressed < row);

    return mod_decompressed;
}

byte* Mod_LeafPVS(mleaf_t* leaf, qmodel_t* model)
{
    if(leaf == model->leafs)
    {
        return Mod_NoVisPVS(model);
    }
    return Mod_DecompressVis(leaf->compressed_vis, model);
}

byte* Mod_NoVisPVS(qmodel_t* model)
{
    int pvsbytes;

    pvsbytes = (model->numleafs + 7) >> 3;
    if(mod_novis == nullptr || pvsbytes > mod_novis_capacity)
    {
        mod_novis_capacity = pvsbytes;
        mod_novis = (byte*)realloc(mod_novis, mod_novis_capacity);
        if(!mod_novis)
        {
            Sys_Error("Mod_NoVisPVS: realloc() failed on %d bytes",
                mod_novis_capacity);
        }

        memset(mod_novis, 0xff, mod_novis_capacity);
    }
    return mod_novis;
}

/*
===================
Mod_ClearAll
===================
*/
void Mod_ClearAll()
{
    int i;
    qmodel_t* mod;

    for(i = 0, mod = mod_known; i < mod_numknown; i++, mod++)
    {
        if(mod->type != mod_alias)
        {
            mod->needload = true;
            TexMgr_FreeTexturesForOwner(mod); // johnfitz
            PScript_ClearSurfaceParticles(mod);
        }
    }
}

void Mod_ResetAll()
{
    int i;
    qmodel_t* mod;

    // ericw -- free alias model VBOs
    GLMesh_DeleteVertexBuffers();

    for(i = 0, mod = mod_known; i < mod_numknown; i++, mod++)
    {
        if(!mod->needload) // otherwise Mod_ClearAll() did it already
        {
            TexMgr_FreeTexturesForOwner(mod);
            PScript_ClearSurfaceParticles(mod);
        }
        memset(mod, 0, sizeof(qmodel_t));
    }
    mod_numknown = 0;
}

/*
==================
Mod_FindName

==================
*/
qmodel_t* Mod_FindName(const char* name)
{
    if(!name[0])
    {
        Sys_Error("Mod_FindName: NULL name"); // johnfitz -- was "Mod_ForName"
    }

    //
    // search the currently loaded models
    //
    int i;
    qmodel_t* mod;
    for(i = 0, mod = mod_known; i < mod_numknown; i++, mod++)
    {
        if(!strcmp(mod->name, name))
        {
            break;
        }
    }

    if(i == mod_numknown)
    {
        if(mod_numknown == MAX_MOD_KNOWN)
        {
            Sys_Error("mod_numknown == MAX_MOD_KNOWN");
        }

        q_strlcpy(mod->name, name, MAX_QPATH);
        mod->needload = true;
        mod_numknown++;
    }

    return mod;
}

void Mod_ForAllKnownNames(void (*f)(const char*)) noexcept
{
    int i;
    qmodel_t* mod;
    for(i = 0, mod = mod_known; i < mod_numknown; ++i, ++mod)
    {
        f(mod->name);
    }
}

/*
==================
Mod_TouchModel

==================
*/
void Mod_TouchModel(const char* name)
{
    qmodel_t* mod = Mod_FindName(name);

    if(!mod->needload)
    {
        if(mod->type == mod_alias)
        {
            Cache_Check(&mod->cache);
        }
    }
}

/*
==================
Mod_LoadModel

Loads a model into the cache
==================
*/
qmodel_t* Mod_LoadModel(qmodel_t* mod, bool crash)
{
    byte* buf;
    byte stackbuf[1024]; // avoid dirtying the cache heap

    if(!mod->needload)
    {
        if(mod->type == mod_alias)
        {
            if(Cache_Check(&mod->cache))
            {
                return mod;
            }
        }
        else
        {
            return mod; // not cached at all
        }
    }

    //
    // load the file
    //
    if(*mod->name == '*')
    {
        buf = nullptr;
    }
    else
    {
        buf = COM_LoadStackFile(
            mod->name, stackbuf, sizeof(stackbuf), &mod->path_id);
    }
    if(!buf)
    {
        if(crash)
        {
            Host_Error("Mod_LoadModel: %s not found",
                mod->name); // johnfitz -- was "Mod_NumForName"
        }
        else if(mod->name[0] == '*' &&
                (mod->name[1] < '0' || mod->name[1] > '9'))
        {
            ; //*foo doesn't warn, unless its *NUM. inline models. gah.
        }
        else
        {
            Con_Warning("Mod_LoadModel: %s not found\n", mod->name);
        }

        // avoid crashes
        mod->needload = false;
        mod->type = mod_ext_invalid;
        mod->flags = 0;

        Mod_SetExtraFlags(mod); // johnfitz. spike -- moved this to be generic,
                                // because most of the flags are anyway.
        return mod;
    }

    //
    // allocate a new model
    //
    COM_FileBase(mod->name, loadname, sizeof(loadname));

    loadmodel = mod;

    //
    // fill it in
    //

    // call the apropriate loader
    mod->needload = false;

    const int mod_type =
        (buf[0] | (buf[1] << 8) | (buf[2] << 16) | (buf[3] << 24));

    switch(mod_type)
    {
        case IDPOLYHEADER:
            Mod_LoadAliasModel(mod, buf, aliashdr_t::PV_QUAKE1);
            break;
        case(('M' << 0) + ('D' << 8) + ('1' << 16) +
             ('6' << 24)): // QF 16bit variation
            Mod_LoadAliasModel(mod, buf, aliashdr_t::PV_QUAKEFORGE);
            break;

        case IDSPRITEHEADER: Mod_LoadSpriteModel(mod, buf); break;

        // Spike -- md3 support
        case(('I' << 0) + ('D' << 8) + ('P' << 16) + ('3' << 24)): // md3
            Mod_LoadMD3Model(mod, buf);
            break;

        // Spike -- iqm support
        case(('I' << 0) + ('N' << 8) + ('T' << 16) + ('E' << 24)): // iqm
            Mod_LoadIQMModel(mod, buf);
            break;

        // Spike -- added checks for a few other model types.
        // this is useful because of the number of models with renamed
        // extensions. that and its hard to test the extension stuff when this
        // was crashing.
        case(('R' << 0) + ('A' << 8) + ('P' << 16) + ('O' << 24)): // h2mp
            Con_Warning(
                "%s is a hexen2-missionpack model (unsupported)\n", mod->name);
            mod->type = mod_ext_invalid;
            break;
        case(('I' << 0) + ('D' << 8) + ('P' << 16) + ('2' << 24)): // md2
            Con_Warning("%s is an md2 (unsupported)\n", mod->name);
            mod->type = mod_ext_invalid;
            break;
        case(('D' << 0) + ('A' << 8) + ('R' << 16) + ('K' << 24)): // dpm
            Con_Warning("%s is an dpm (unsupported)\n", mod->name);
            mod->type = mod_ext_invalid;
            break;
        case(('A' << 0) + ('C' << 8) + ('T' << 16) + ('R' << 24)): // psk
            Con_Warning("%s is a psk (unsupported)\n", mod->name);
            mod->type = mod_ext_invalid;
            break;
        case(('I' << 0) + ('B' << 8) + ('S' << 16) + ('P' << 24)): // q2/q3bsp
            Con_Warning("%s is a q2/q3bsp (unsupported)\n", mod->name);
            mod->type = mod_ext_invalid;
            break;

        default: Mod_LoadBrushModel(mod, buf); break;
    }

    if(crash && mod->type == mod_ext_invalid)
    { // any of those formats for a world map will be screwed up.
        Sys_Error("Mod_LoadModel: couldn't load %s",
            mod->name); // johnfitz -- was "Mod_NumForName"
        return nullptr;
    }

    Mod_SetExtraFlags(mod); // johnfitz. spike -- moved this to be generic,
                            // because most of the flags are anyway.

    return mod;
}

/*
==================
Mod_ForName

Loads in a model for the given name
==================
*/
qmodel_t* Mod_ForName(const char* name, bool crash)
{
    return Mod_LoadModel(Mod_FindName(name), crash);
}


/*
==================
Mod_ForName_WithFallback

Loads in a model for the given name, falling back to another one on failure
==================
*/
qmodel_t* Mod_ForName_WithFallback(const char* name, const char* fallback)
{
    if(qmodel_t* const r = Mod_ForName(name, false /* crash */))
    {
        return r;
    }

    Host_Warn(
        "Mod_LoadModel: %s not found, falling back to %s", name, fallback);

    return Mod_ForName(fallback, true);
}


/*
===============================================================================

                    BRUSHMODEL LOADING

===============================================================================
*/

static byte* mod_base;


typedef struct
{
    char lumpname[24]; // up to 23 chars, zero-padded
    int fileofs;       // from file start
    int filelen;
} bspx_lump_t;

typedef struct
{
    char id[4]; // 'BSPX'
    int numlumps;
    bspx_lump_t lumps[1];
} bspx_header_t;

static char* bspxbase;
static bspx_header_t* bspxheader;
// supported lumps:
// RGBLIGHTING (.lit)
// LMSHIFT (.lit2)
// LMOFFSET (LMSHIFT helper)
// LMSTYLE (LMSHIFT helper)

// unsupported lumps ('documented' elsewhere):
// BRUSHLIST (because hulls suck)
// LIGHTINGDIR (.lux)
// LIGHTING_E5BGR9 (hdr lighting)
// VERTEXNORMALS (smooth shading with dlights/rtlights)
static void* Q1BSPX_FindLump(const char* lumpname, int* lumpsize)
{
    *lumpsize = 0;
    if(!bspxheader)
    {
        return nullptr;
    }

    for(int i = 0; i < bspxheader->numlumps; i++)
    {
        if(!strncmp(bspxheader->lumps[i].lumpname, lumpname, 24))
        {
            *lumpsize = bspxheader->lumps[i].filelen;
            return bspxbase + bspxheader->lumps[i].fileofs;
        }
    }

    return nullptr;
}

static void Q1BSPX_Setup(qmodel_t* mod, char* filebase, unsigned int filelen,
    lump_t* lumps, int numlumps)
{
    int i;
    int offs = 0;
    bspx_header_t* h;
    bool misaligned = false;

    bspxbase = filebase;
    bspxheader = nullptr;

    for(i = 0; i < numlumps; i++, lumps++)
    {
        if((lumps->fileofs & 3) && i != LUMP_ENTITIES)
        {
            misaligned = true;
        }
        if(offs < lumps->fileofs + lumps->filelen)
        {
            offs = lumps->fileofs + lumps->filelen;
        }
    }
    if(misaligned)
    {
        Con_DWarning("%s contains misaligned lumps\n", mod->name);
    }
    offs = (offs + 3) & ~3;
    if(offs + sizeof(*bspxheader) > filelen)
    {
        return; /*no space for it*/
    }
    h = (bspx_header_t*)(filebase + offs);

    i = LittleLong(h->numlumps);
    /*verify the header*/
    if(strncmp(h->id, "BSPX", 4) || i < 0 ||
        offs + sizeof(*h) + sizeof(h->lumps[0]) * (i - 1) > filelen)
    {
        return;
    }
    h->numlumps = i;
    while(i-- > 0)
    {
        h->lumps[i].fileofs = LittleLong(h->lumps[i].fileofs);
        h->lumps[i].filelen = LittleLong(h->lumps[i].filelen);
        if(h->lumps[i].fileofs & 3)
        {
            Con_DWarning("%s contains misaligned bspx limp %s\n", mod->name,
                h->lumps[i].lumpname);
        }
        if((unsigned int)h->lumps[i].fileofs +
                (unsigned int)h->lumps[i].filelen >
            filelen)
        {
            return;
        }
    }

    bspxheader = h;
}

/*
=================
Mod_CheckFullbrights -- johnfitz
=================
*/
bool Mod_CheckFullbrights(byte* pixels, int count)
{
    int i;
    for(i = 0; i < count; i++)
    {
        if(*pixels++ > 223)
        {
            return true;
        }
    }
    return false;
}

[[nodiscard]] static texture_t* Mod_LoadMipTex(miptex_t* mt, byte* lumpend,
    srcformat* fmt, unsigned int* width, unsigned int* height,
    unsigned int* pixelbytes)
{
    // if offsets[0] is 0, then we've no legacy data (offsets[3] signifies the
    // end of the extension data.
    byte* extdata;
    texture_t* tx;
    byte* srcdata = nullptr;
    size_t sz;

    if(!mt->offsets[0])
    { // the legacy data was omitted. we may still have
        // block-compression though.
        extdata = (byte*)(mt + 1);
    }
    else if(mt->offsets[0] == sizeof(miptex_t) &&
            mt->offsets[1] ==
                mt->offsets[0] + (mt->width >> 0) * (mt->height >> 0) &&
            mt->offsets[2] ==
                mt->offsets[1] + (mt->width >> 1) * (mt->height >> 1) &&
            mt->offsets[3] ==
                mt->offsets[2] + (mt->width >> 2) * (mt->height >> 2))
    { // miptex makes sense and matches the standard 4-mip-levels.
        extdata =
            (byte*)mt + mt->offsets[3] + (mt->width >> 3) * (mt->height >> 3);
        // FIXME: halflife - leshort=256, palette[256][3].
        // extdata += 2+256*3;
    }
    else
    { // the numbers don't match what we expect... something weird is going
        // on here... don't misinterpret it.
        extdata = lumpend;
    }

    if(extdata + 4 <= lumpend && extdata[0] == 0 && extdata[1] == 0xfb &&
        extdata[2] == 0x2b && extdata[3] == 0xaf)
    {
        for(extdata += 4; extdata + 8 < lumpend; extdata += sz)
        {
            sz = (extdata[0] << 0) | (extdata[1] << 8) | (extdata[2] << 16) |
                 (extdata[3] << 24);
            if(sz < 8 || sz > lumpend - extdata)
            {
                break; // bad! bad! bad!
            }
            else if(sz <= 16)
            {
                continue; // nope, no idea
            }

            *fmt = TexMgr_FormatForCode((char*)extdata + 4);
            if(*fmt == SRC_EXTERNAL)
            {
                continue; // nope, no idea
            }

            *width = (extdata[8] << 0) | (extdata[9] << 8) |
                     (extdata[10] << 16) | (extdata[11] << 24);
            *height = (extdata[12] << 0) | (extdata[13] << 8) |
                      (extdata[14] << 16) | (extdata[15] << 24);

            if(*width != TexMgr_SafeTextureSize(*width) ||
                *width != TexMgr_SafeTextureSize(*width))
            {
                continue; // nope, can't use that. drivers are too lame (or
            }
            // gl_max_size is too low).

            *pixelbytes = TexMgr_ImageSize(*width, *height, *fmt);
            if(16 + *pixelbytes == sz)
            {
                srcdata = extdata + 16;
            }
            break;
        }
    }

    if(!srcdata)
    { // no replacements, load the 8bit data.
        *fmt = SRC_INDEXED;
        *width = mt->width;
        *height = mt->height;
        *pixelbytes = mt->width * mt->height;
        if(LittleLong(mt->offsets[0]))
        {
            srcdata = (byte*)mt + LittleLong(mt->offsets[0]);
        }
    }

    tx = (texture_t*)Hunk_AllocName(sizeof(texture_t) + *pixelbytes, loadname);
    memcpy(tx->name, mt->name, sizeof(tx->name));
    tx->name[sizeof(tx->name) - 1] = 0; // just in case...
    tx->width = mt->width;
    tx->height = mt->height;

    if(srcdata)
    {
        // ericw -- check for pixels extending past the end of the lump.
        // appears in the wild; e.g. jam2_tronyn.bsp (func_mapjam2),
        // kellbase1.bsp (quoth), and can lead to a segfault if we read past
        // the end of the .bsp file buffer
        if((srcdata + *pixelbytes) > lumpend)
        {
            Con_DPrintf("Texture %s extends past end of lump\n", mt->name);
            *pixelbytes = q_max(0, lumpend - srcdata);
        }

        memcpy(tx + 1, srcdata, *pixelbytes);
    }
    else
    {
        size_t x, y;
        for(y = 0; y < tx->width; y++)
        {
            for(x = 0; x < tx->width; x++)
            {
                ((byte*)(tx + 1))[y * tx->width + x] =
                    (((x >> 2) ^ (y >> 2)) & 1) ? 6 : 2;
            }
        }
    }
    return tx;
}

/*
=================
Mod_LoadTextures
=================
*/
void Mod_LoadTextures(lump_t* l)
{
    int i, j, num, maxanim, altmax;
    miptex_t* mt;
    texture_t *tx, *tx2;
    texture_t* anims[10];
    texture_t* altanims[10];
    dmiptexlump_t* m;
    // johnfitz -- more variables
    char texturename[64];
    int nummiptex;
    src_offset_t offset;
    int mark, fwidth, fheight;
    char filename[MAX_OSPATH], filename2[MAX_OSPATH], mapname[MAX_OSPATH];
    byte* data;
    extern byte* hunk_base;
    // johnfitz
    bool malloced; // spike
    srcformat fmt; // spike
    unsigned int imgwidth, imgheight, imgpixels;
    unsigned int mipend;

    // johnfitz -- don't return early if no textures; still need to create dummy
    // texture
    if(!l->filelen)
    {
        Con_Printf("Mod_LoadTextures: no textures in bsp file\n");
        nummiptex = 0;
        m = nullptr; // avoid bogus compiler warning
    }
    else
    {
        m = (dmiptexlump_t*)(mod_base + l->fileofs);
        m->nummiptex = LittleLong(m->nummiptex);
        nummiptex = m->nummiptex;
    }
    // johnfitz

    loadmodel->numtextures =
        nummiptex +
        2; // johnfitz -- need 2 dummy texture chains for missing textures
    loadmodel->textures = (texture_t**)Hunk_AllocName(
        loadmodel->numtextures * sizeof(*loadmodel->textures), loadname);

    // spike -- rewrote this loop to run backwards (to make it easier to track
    // the end of the miptex) and added handling for extra texture block
    // compression.
    for(i = nummiptex, mipend = l->filelen; i-- > 0;)
    {
        m->dataofs[i] = LittleLong(m->dataofs[i]);
        if(m->dataofs[i] == -1)
        {
            continue;
        }
        if(m->dataofs[i] >= mipend)
        {
            mipend = l->filelen; // o.O something weird!
        }
        mt = (miptex_t*)((byte*)m + m->dataofs[i]);
        mt->width = LittleLong(mt->width);
        mt->height = LittleLong(mt->height);
        for(j = 0; j < MIPLEVELS; j++)
        {
            mt->offsets[j] = LittleLong(mt->offsets[j]);
        }

        if((mt->width & 15) || (mt->height & 15))
        {
            Sys_Error("Texture %s is not 16 aligned", mt->name);
        }

        tx = Mod_LoadMipTex(mt, (mod_base + l->fileofs + mipend), &fmt,
            &imgwidth, &imgheight, &imgpixels);
        loadmodel->textures[i] = tx;

        tx->update_warp = false;  // johnfitz
        tx->warpimage = nullptr;  // johnfitz
        tx->fullbright = nullptr; // johnfitz

        mipend = m->dataofs[i];

        // johnfitz -- lots of changes
        if(!isDedicated) // no texture uploading for dedicated server
        {
            if(!q_strncasecmp(tx->name, "sky", 3))
            { // sky texture //also note -- was
                // Q_strncmp, changed to match qbsp
                Sky_LoadTexture(tx, fmt, imgwidth, imgheight);
            }
            else if(tx->name[0] == '*') // warping texture
            {
                srcformat rfmt = SRC_RGBA;
                fwidth = fheight = 0;
                malloced = false;
                // external textures -- first look in "textures/mapname/" then
                // look in "textures/"
                mark = Hunk_LowMark();
                COM_StripExtension(
                    loadmodel->name + 5, mapname, sizeof(mapname));
                q_snprintf(filename, sizeof(filename), "textures/%s/#%s",
                    mapname,
                    tx->name + 1); // this also replaces the '*' with a '#'
                data = !gl_load24bit.value ? nullptr
                                           : Image_LoadImage(filename, &fwidth,
                                                 &fheight, &rfmt, &malloced);
                if(!data)
                {
                    q_snprintf(filename, sizeof(filename), "textures/#%s",
                        tx->name + 1);
                    data = !gl_load24bit.value
                               ? nullptr
                               : Image_LoadImage(filename, &fwidth, &fheight,
                                     &rfmt, &malloced);
                }

                // now load whatever we found
                if(data) // load external image
                {
                    q_strlcpy(texturename, filename, sizeof(texturename));
                    tx->gltexture = TexMgr_LoadImage(loadmodel, texturename,
                        fwidth, fheight, rfmt, data, filename, 0, TEXPREF_NONE);
                }
                else // use the texture from the bsp file
                {
                    q_snprintf(texturename, sizeof(texturename), "%s:%s",
                        loadmodel->name, tx->name);
                    offset = (src_offset_t)(mt + 1) - (src_offset_t)mod_base;
                    tx->gltexture = TexMgr_LoadImage(loadmodel, texturename,
                        imgwidth, imgheight, fmt, (byte*)(tx + 1),
                        loadmodel->name, offset, TEXPREF_NONE);
                }

                // now create the warpimage, using dummy data from the
                // hunk to create the initial image
                (void)Hunk_Alloc(gl_warpimagesize * gl_warpimagesize *
                                 4); // make sure hunk is big enough so we
                                     // don't reach an illegal address
                Hunk_FreeToLowMark(mark);
                q_snprintf(
                    texturename, sizeof(texturename), "%s_warp", texturename);
                tx->warpimage = TexMgr_LoadImage(loadmodel, texturename,
                    gl_warpimagesize, gl_warpimagesize, SRC_RGBA, hunk_base, "",
                    (src_offset_t)hunk_base,
                    TEXPREF_NOPICMIP | TEXPREF_WARPIMAGE);
                tx->update_warp = true;
                if(malloced)
                {
                    free(data);
                }
            }
            else // regular texture
            {
                // ericw -- fence textures
                int extraflags;
                srcformat rfmt = SRC_RGBA;
                fwidth = fheight = 0;
                malloced = false;

                extraflags = 0;
                if(tx->name[0] == '{')
                {
                    extraflags |= TEXPREF_ALPHA;
                }
                // ericw

                // external textures -- first look in "textures/mapname/" then
                // look in "textures/"
                mark = Hunk_LowMark();
                COM_StripExtension(
                    loadmodel->name + 5, mapname, sizeof(mapname));
                q_snprintf(filename, sizeof(filename), "textures/%s/%s",
                    mapname, tx->name);
                data = !gl_load24bit.value ? nullptr
                                           : Image_LoadImage(filename, &fwidth,
                                                 &fheight, &rfmt, &malloced);
                if(!data)
                {
                    q_snprintf(
                        filename, sizeof(filename), "textures/%s", tx->name);
                    data = !gl_load24bit.value
                               ? nullptr
                               : Image_LoadImage(filename, &fwidth, &fheight,
                                     &rfmt, &malloced);
                }

                // now load whatever we found
                if(data) // load external image
                {
                    tx->gltexture = TexMgr_LoadImage(loadmodel, filename,
                        fwidth, fheight, rfmt, data, filename, 0,
                        TEXPREF_MIPMAP | extraflags);

                    // now try to load glow/luma image from the same place
                    if(malloced)
                    {
                        free(data);
                    }
                    Hunk_FreeToLowMark(mark);
                    q_snprintf(
                        filename2, sizeof(filename2), "%s_glow", filename);
                    data = !gl_load24bit.value
                               ? nullptr
                               : Image_LoadImage(filename2, &fwidth, &fheight,
                                     &rfmt, &malloced);
                    if(!data)
                    {
                        q_snprintf(
                            filename2, sizeof(filename2), "%s_luma", filename);
                        data = !gl_load24bit.value
                                   ? nullptr
                                   : Image_LoadImage(filename2, &fwidth,
                                         &fheight, &rfmt, &malloced);
                    }

                    if(data)
                    {
                        tx->fullbright = TexMgr_LoadImage(loadmodel, filename2,
                            fwidth, fheight, rfmt, data, filename, 0,
                            TEXPREF_MIPMAP | extraflags);
                    }
                }
                else // use the texture from the bsp file
                {
                    q_snprintf(texturename, sizeof(texturename), "%s:%s",
                        loadmodel->name, tx->name);
                    offset = (src_offset_t)(mt + 1) - (src_offset_t)mod_base;
                    if(fmt == SRC_INDEXED &&
                        Mod_CheckFullbrights((byte*)(tx + 1), imgpixels))
                    {
                        tx->gltexture = TexMgr_LoadImage(loadmodel, texturename,
                            imgwidth, imgheight, fmt, (byte*)(tx + 1),
                            loadmodel->name, offset,
                            TEXPREF_MIPMAP | TEXPREF_NOBRIGHT | extraflags);
                        q_snprintf(texturename, sizeof(texturename),
                            "%s:%s_glow", loadmodel->name, tx->name);
                        tx->fullbright = TexMgr_LoadImage(loadmodel,
                            texturename, imgwidth, imgheight, fmt,
                            (byte*)(tx + 1), loadmodel->name, offset,
                            TEXPREF_MIPMAP | TEXPREF_FULLBRIGHT | extraflags);
                    }
                    else
                    {
                        tx->gltexture = TexMgr_LoadImage(loadmodel, texturename,
                            imgwidth, imgheight, fmt, (byte*)(tx + 1),
                            loadmodel->name, offset,
                            TEXPREF_MIPMAP | extraflags);
                    }
                }
                if(malloced)
                {
                    free(data);
                }
                Hunk_FreeToLowMark(mark);
            }
        }
        // johnfitz
    }

    // johnfitz -- last 2 slots in array should be filled with dummy textures
    loadmodel->textures[loadmodel->numtextures - 2] =
        r_notexture_mip; // for lightmapped surfs
    loadmodel->textures[loadmodel->numtextures - 1] =
        r_notexture_mip2; // for SURF_DRAWTILED surfs

    //
    // sequence the animations
    //
    for(i = 0; i < nummiptex; i++)
    {
        tx = loadmodel->textures[i];
        if(!tx || tx->name[0] != '+')
        {
            continue;
        }
        if(tx->anim_next)
        {
            continue; // allready sequenced
        }

        // find the number of frames in the animation
        memset(anims, 0, sizeof(anims));
        memset(altanims, 0, sizeof(altanims));

        maxanim = tx->name[1];
        altmax = 0;
        if(maxanim >= 'a' && maxanim <= 'z')
        {
            maxanim -= 'a' - 'A';
        }
        if(maxanim >= '0' && maxanim <= '9')
        {
            maxanim -= '0';
            altmax = 0;
            anims[maxanim] = tx;
            maxanim++;
        }
        else if(maxanim >= 'A' && maxanim <= 'J')
        {
            altmax = maxanim - 'A';
            maxanim = 0;
            altanims[altmax] = tx;
            altmax++;
        }
        else
        {
            Sys_Error("Bad animating texture %s", tx->name);
        }

        for(j = i + 1; j < nummiptex; j++)
        {
            tx2 = loadmodel->textures[j];
            if(!tx2 || tx2->name[0] != '+')
            {
                continue;
            }
            if(strcmp(tx2->name + 2, tx->name + 2))
            {
                continue;
            }

            num = tx2->name[1];
            if(num >= 'a' && num <= 'z')
            {
                num -= 'a' - 'A';
            }
            if(num >= '0' && num <= '9')
            {
                num -= '0';
                anims[num] = tx2;
                if(num + 1 > maxanim)
                {
                    maxanim = num + 1;
                }
            }
            else if(num >= 'A' && num <= 'J')
            {
                num -= 'A';
                altanims[num] = tx2;
                if(num + 1 > altmax)
                {
                    altmax = num + 1;
                }
            }
            else
            {
                Sys_Error("Bad animating texture %s", tx->name);
            }
        }

#define ANIM_CYCLE 2
        // link them all together
        for(j = 0; j < maxanim; j++)
        {
            tx2 = anims[j];
            if(!tx2)
            {
                Sys_Error("Missing frame %i of %s", j, tx->name);
            }
            tx2->anim_total = maxanim * ANIM_CYCLE;
            tx2->anim_min = j * ANIM_CYCLE;
            tx2->anim_max = (j + 1) * ANIM_CYCLE;
            tx2->anim_next = anims[(j + 1) % maxanim];
            if(altmax)
            {
                tx2->alternate_anims = altanims[0];
            }
        }
        for(j = 0; j < altmax; j++)
        {
            tx2 = altanims[j];
            if(!tx2)
            {
                Sys_Error("Missing frame %i of %s", j, tx->name);
            }
            tx2->anim_total = altmax * ANIM_CYCLE;
            tx2->anim_min = j * ANIM_CYCLE;
            tx2->anim_max = (j + 1) * ANIM_CYCLE;
            tx2->anim_next = altanims[(j + 1) % altmax];
            if(maxanim)
            {
                tx2->alternate_anims = anims[0];
            }
        }
    }
}

/*
=================
Mod_LoadLighting -- johnfitz -- replaced with lit support code via lordhavoc
=================
*/
void Mod_LoadLighting(lump_t* l)
{
    int i, mark;
    byte *in, *out, *data;
    byte d;
    char litfilename[MAX_OSPATH];
    unsigned int path_id;
    int bspxsize;

    loadmodel->lightdata = nullptr;
    // LordHavoc: check for a .lit file
    q_strlcpy(litfilename, loadmodel->name, sizeof(litfilename));
    COM_StripExtension(litfilename, litfilename, sizeof(litfilename));
    q_strlcat(litfilename, ".lit", sizeof(litfilename));
    mark = Hunk_LowMark();
    data = (byte*)COM_LoadHunkFile(litfilename, &path_id);
    if(data)
    {
        // use lit file only from the same gamedir as the map
        // itself or from a searchpath with higher priority.
        if(path_id < loadmodel->path_id)
        {
            Hunk_FreeToLowMark(mark);
            Con_DPrintf(
                "ignored %s from a gamedir with lower priority\n", litfilename);
        }
        else if(data[0] == 'Q' && data[1] == 'L' && data[2] == 'I' &&
                data[3] == 'T')
        {
            i = LittleLong(((int*)data)[1]);
            if(i == 1)
            {
                if(8 + l->filelen * 3 == com_filesize)
                {
                    Con_DPrintf2("%s loaded\n", litfilename);
                    loadmodel->lightdata = data + 8;
                    return;
                }
                Hunk_FreeToLowMark(mark);
                Con_Printf(
                    "Outdated .lit file (%s should be %u bytes, not %u)\n",
                    litfilename, 8 + l->filelen * 3, com_filesize);
            }
            else
            {
                Hunk_FreeToLowMark(mark);
                Con_Printf("Unknown .lit file version (%d)\n", i);
            }
        }
        else
        {
            Hunk_FreeToLowMark(mark);
            Con_Printf("Corrupt .lit file (old version?), ignoring\n");
        }
    }
    // LordHavoc: no .lit found, expand the white lighting data to color
    if(!l->filelen)
    {
        return;
    }
    in = (byte*)Q1BSPX_FindLump("RGBLIGHTING", &bspxsize);
    if(loadmodel->lightdata && bspxsize == l->filelen * 3)
    {
        loadmodel->lightdata = out =
            (byte*)Hunk_AllocName(l->filelen * 3, litfilename);
        memcpy(out, in, l->filelen * 3);
        Con_DPrintf("bspx lighting loaded\n");
    }
    else
    {
        in = (byte*)Q1BSPX_FindLump("LIGHTING_E5BGR9", &bspxsize);
        if(in && bspxsize == l->filelen * 4)
        { // we don't really support hdr lighting, but we downgrade it to ldr
          // whenever there's no rgb data.
            // FIXME: don't convert this stuff here. upload the data to the gpu
            // with GL_EXT_shared_exponent (core in gl3)
            loadmodel->lightdata =
                (byte*)Hunk_AllocName(l->filelen * 3, litfilename);
            out = loadmodel->lightdata;
            Con_DPrintf("bspx hdr->ldr lighting loaded\n");
            for(i = 0; i < l->filelen; i++, in += 4)
            {
                static constexpr float rgb9e5tab[32] = {
                    // multipliers for the 9-bit mantissa, according to the
                    // biased mantissa
                    // aka: pow(2, biasedexponent - bias-bits) where bias is 15
                    // and bits is 9
                    1.0 / (1 << 24),
                    1.0 / (1 << 23),
                    1.0 / (1 << 22),
                    1.0 / (1 << 21),
                    1.0 / (1 << 20),
                    1.0 / (1 << 19),
                    1.0 / (1 << 18),
                    1.0 / (1 << 17),
                    1.0 / (1 << 16),
                    1.0 / (1 << 15),
                    1.0 / (1 << 14),
                    1.0 / (1 << 13),
                    1.0 / (1 << 12),
                    1.0 / (1 << 11),
                    1.0 / (1 << 10),
                    1.0 / (1 << 9),
                    1.0 / (1 << 8),
                    1.0 / (1 << 7),
                    1.0 / (1 << 6),
                    1.0 / (1 << 5),
                    1.0 / (1 << 4),
                    1.0 / (1 << 3),
                    1.0 / (1 << 2),
                    1.0 / (1 << 1),
                    1.0,
                    1.0 * (1 << 1),
                    1.0 * (1 << 2),
                    1.0 * (1 << 3),
                    1.0 * (1 << 4),
                    1.0 * (1 << 5),
                    1.0 * (1 << 6),
                    1.0 * (1 << 7),
                };
                unsigned int e5bgr9 = *(unsigned int*)in;
                float e = rgb9e5tab[e5bgr9 >> 27] *
                          (1 << 7); // we're converting to a scale that holds
                                    // overbrights, so 1->128, its 2->255ish
                *out++ = q_min(255, e * ((e5bgr9 >> 0) & 0x1ff));  // red
                *out++ = q_min(255, e * ((e5bgr9 >> 9) & 0x1ff));  // green
                *out++ = q_min(255, e * ((e5bgr9 >> 18) & 0x1ff)); // blue
            }
        }
        else
        {
            loadmodel->lightdata =
                (byte*)Hunk_AllocName(l->filelen * 3, litfilename);
            in = loadmodel->lightdata +
                 l->filelen * 2; // place the file at the end, so it will not be
                                 // overwritten until the very last write
            out = loadmodel->lightdata;
            memcpy(in, mod_base + l->fileofs, l->filelen);
            for(i = 0; i < l->filelen; i++)
            {
                d = *in++;
                *out++ = d;
                *out++ = d;
                *out++ = d;
            }
        }
    }
}


/*
=================
Mod_LoadVisibility
=================
*/
void Mod_LoadVisibility(lump_t* l)
{
    loadmodel->viswarn = false;
    if(!l->filelen)
    {
        loadmodel->visdata = nullptr;
        return;
    }
    loadmodel->visdata = (byte*)Hunk_AllocName(l->filelen, loadname);
    memcpy(loadmodel->visdata, mod_base + l->fileofs, l->filelen);
}


/*
=================
Mod_LoadEntities
=================
*/
void Mod_LoadEntities(lump_t* l)
{
    char entfilename[MAX_QPATH];
    char* ents;
    int mark;
    unsigned int path_id;

    if(!external_ents.value)
    {
        goto _load_embedded;
    }

    q_strlcpy(entfilename, loadmodel->name, sizeof(entfilename));
    COM_StripExtension(entfilename, entfilename, sizeof(entfilename));
    q_strlcat(entfilename, ".ent", sizeof(entfilename));
    Con_DPrintf2("trying to load %s\n", entfilename);
    mark = Hunk_LowMark();
    ents = (char*)COM_LoadHunkFile(entfilename, &path_id);
    if(ents)
    {
        // use ent file only from the same gamedir as the map
        // itself or from a searchpath with higher priority.
        if(path_id < loadmodel->path_id)
        {
            Hunk_FreeToLowMark(mark);
            Con_DPrintf(
                "ignored %s from a gamedir with lower priority\n", entfilename);
        }
        else
        {
            loadmodel->entities = ents;
            Con_DPrintf("Loaded external entity file %s\n", entfilename);
            return;
        }
    }

_load_embedded:
    if(!l->filelen)
    {
        loadmodel->entities = nullptr;
        return;
    }
    loadmodel->entities = (char*)Hunk_AllocName(l->filelen, loadname);
    memcpy(loadmodel->entities, mod_base + l->fileofs, l->filelen);
}


/*
=================
Mod_ParseWorldspawnKey
=================
(Blame Spike)
This just quickly scans the worldspawn entity for a single key. Returning both
_prefixed and non prefixed keys. (wantkey argument should not have a _prefix.)
*/
const char* Mod_ParseWorldspawnKey(
    qmodel_t* mod, const char* wantkey, char* buffer, size_t sizeofbuffer)
{
    char foundkey[128];
    const char* data = COM_Parse(mod->entities);

    if(data && com_token[0] == '{')
    {
        while(1)
        {
            data = COM_Parse(data);
            if(!data)
            {
                break; // error
            }
            if(com_token[0] == '}')
            {
                break; // end of worldspawn
            }
            if(com_token[0] == '_')
            {
                strcpy(foundkey, com_token + 1);
            }
            else
            {
                strcpy(foundkey, com_token);
            }
            data = COM_Parse(data);
            if(!data)
            {
                break; // error
            }
            if(!strcmp(wantkey, foundkey))
            {
                q_strlcpy(buffer, com_token, sizeofbuffer);
                return buffer;
            }
        }
    }
    return nullptr;
}


/*
=================
Mod_LoadVertexes
=================
*/
void Mod_LoadVertexes(lump_t* l)
{
    dvertex_t* in;
    mvertex_t* out;
    int i, count;

    in = (dvertex_t*)(mod_base + l->fileofs);
    if(l->filelen % sizeof(*in))
    {
        Sys_Error("MOD_LoadBmodel: funny lump size in %s", loadmodel->name);
    }
    count = l->filelen / sizeof(*in);
    out = (mvertex_t*)Hunk_AllocName(count * sizeof(*out), loadname);

    loadmodel->vertexes = out;
    loadmodel->numvertexes = count;

    for(i = 0; i < count; i++, in++, out++)
    {
        out->position[0] = LittleFloat(in->point[0]);
        out->position[1] = LittleFloat(in->point[1]);
        out->position[2] = LittleFloat(in->point[2]);
    }
}

/*
=================
Mod_LoadEdges
=================
*/
void Mod_LoadEdges(lump_t* l, int bsp2)
{
    medge_t* out;
    int i, count;

    if(bsp2)
    {
        auto* in = (dledge_t*)(mod_base + l->fileofs);

        if(l->filelen % sizeof(*in))
        {
            Sys_Error("MOD_LoadBmodel: funny lump size in %s", loadmodel->name);
        }

        count = l->filelen / sizeof(*in);
        out = (medge_t*)Hunk_AllocName((count + 1) * sizeof(*out), loadname);

        loadmodel->edges = out;
        loadmodel->numedges = count;

        for(i = 0; i < count; i++, in++, out++)
        {
            out->v[0] = LittleLong(in->v[0]);
            out->v[1] = LittleLong(in->v[1]);
        }
    }
    else
    {
        auto* in = (dsedge_t*)(mod_base + l->fileofs);

        if(l->filelen % sizeof(*in))
        {
            Sys_Error("MOD_LoadBmodel: funny lump size in %s", loadmodel->name);
        }

        count = l->filelen / sizeof(*in);
        out = (medge_t*)Hunk_AllocName((count + 1) * sizeof(*out), loadname);

        loadmodel->edges = out;
        loadmodel->numedges = count;

        for(i = 0; i < count; i++, in++, out++)
        {
            out->v[0] = (unsigned short)LittleShort(in->v[0]);
            out->v[1] = (unsigned short)LittleShort(in->v[1]);
        }
    }
}

/*
=================
Mod_LoadTexinfo
=================
*/
void Mod_LoadTexinfo(lump_t* l)
{
    texinfo_t* in;
    mtexinfo_t* out;
    int i, j, count, miptex;
    float len1, len2;
    int missing = 0; // johnfitz

    in = (texinfo_t*)(mod_base + l->fileofs);
    if(l->filelen % sizeof(*in))
    {
        Sys_Error("MOD_LoadBmodel: funny lump size in %s", loadmodel->name);
    }
    count = l->filelen / sizeof(*in);
    out = (mtexinfo_t*)Hunk_AllocName(count * sizeof(*out), loadname);

    loadmodel->texinfo = out;
    loadmodel->numtexinfo = count;

    for(i = 0; i < count; i++, in++, out++)
    {
        for(j = 0; j < 4; j++)
        {
            out->vecs[0][j] = LittleFloat(in->vecs[0][j]);
            out->vecs[1][j] = LittleFloat(in->vecs[1][j]);
        }
        len1 = VectorLength(out->vecs[0]);
        len2 = VectorLength(out->vecs[1]);
        len1 = (len1 + len2) / 2;
        if(len1 < 0.32)
        {
            out->mipadjust = 4;
        }
        else if(len1 < 0.49)
        {
            out->mipadjust = 3;
        }
        else if(len1 < 0.99)
        {
            out->mipadjust = 2;
        }
        else
        {
            out->mipadjust = 1;
        }
#if 0
		if (len1 + len2 < 0.001)
			out->mipadjust = 1;		// don't crash
		else
			out->mipadjust = 1 / floor((len1+len2)/2 + 0.1);
#endif

        miptex = LittleLong(in->miptex);
        out->flags = LittleLong(in->flags);

        // johnfitz -- rewrote this section
        if(miptex >= loadmodel->numtextures - 1 || !loadmodel->textures[miptex])
        {
            if(out->flags & TEX_SPECIAL)
            {
                out->texture = loadmodel->textures[loadmodel->numtextures - 1];
            }
            else
            {
                out->texture = loadmodel->textures[loadmodel->numtextures - 2];
            }
            out->flags |= TEX_MISSING;
            missing++;
        }
        else
        {
            out->texture = loadmodel->textures[miptex];
        }
        // johnfitz
    }

    // johnfitz: report missing textures
    if(missing && loadmodel->numtextures > 1)
    {
        Con_Printf(
            "Mod_LoadTexinfo: %d texture(s) missing from BSP file\n", missing);
    }
    // johnfitz
}

/*
================
CalcSurfaceExtents

Fills in s->texturemins[] and s->extents[]
================
*/
void CalcSurfaceExtents(msurface_t* s)
{
    float mins[2], maxs[2], val;
    int i, j, e;
    mvertex_t* v;
    mtexinfo_t* tex;
    int bmins[2], bmaxs[2];
    int maxextent, lmscale;

    mins[0] = mins[1] = FLT_MAX;
    maxs[0] = maxs[1] = -FLT_MAX;

    tex = s->texinfo;

    for(i = 0; i < s->numedges; i++)
    {
        e = loadmodel->surfedges[s->firstedge + i];
        if(e >= 0)
        {
            v = &loadmodel->vertexes[loadmodel->edges[e].v[0]];
        }
        else
        {
            v = &loadmodel->vertexes[loadmodel->edges[-e].v[1]];
        }

        for(j = 0; j < 2; j++)
        {
            /* The following calculation is sensitive to floating-point
             * precision.  It needs to produce the same result that the
             * light compiler does, because R_BuildLightMap uses surf->
             * extents to know the width/height of a surface's lightmap,
             * and incorrect rounding here manifests itself as patches
             * of "corrupted" looking lightmaps.
             * Most light compilers are win32 executables, so they use
             * x87 floating point.  This means the multiplies and adds
             * are done at 80-bit precision, and the result is rounded
             * down to 32-bits and stored in val.
             * Adding the casts to double seems to be good enough to fix
             * lighting glitches when Quakespasm is compiled as x86_64
             * and using SSE2 floating-point.  A potential trouble spot
             * is the hallway at the beginning of mfxsp17.  -- ericw
             */
            val = ((double)v->position[0] * (double)tex->vecs[j][0]) +
                  ((double)v->position[1] * (double)tex->vecs[j][1]) +
                  ((double)v->position[2] * (double)tex->vecs[j][2]) +
                  (double)tex->vecs[j][3];

            if(val < mins[j])
            {
                mins[j] = val;
            }
            if(val > maxs[j])
            {
                maxs[j] = val;
            }
        }
    }

    lmscale = 1 << s->lmshift;
    maxextent = q_max(LMBLOCK_WIDTH, LMBLOCK_HEIGHT) * lmscale;

    for(i = 0; i < 2; i++)
    {
        bmins[i] = floor(mins[i] / lmscale);
        bmaxs[i] = ceil(maxs[i] / lmscale);

        s->texturemins[i] = bmins[i] * lmscale;
        s->extents[i] = (bmaxs[i] - bmins[i]) * lmscale;

        if(!(tex->flags & TEX_SPECIAL) &&
            s->extents[i] >
                maxextent) // johnfitz -- was 512 in glquake, 256 in winquake
        {
            s->extents[i] = 1;
        }
    }
}

/*
================
Mod_PolyForUnlitSurface -- johnfitz -- creates polys for unlightmapped surfaces
(sky and water)

TODO: merge this into BuildSurfaceDisplayList?
================
*/
void Mod_PolyForUnlitSurface(msurface_t* fa)
{
    vec3_t verts[64];
    int numverts, i, lindex;
    qfloat* vec;
    glpoly_t* poly;
    float texscale;

    if(fa->flags & (SURF_DRAWTURB | SURF_DRAWSKY))
    {
        texscale = (1.0 / 128.0); // warp animation repeats every 128
    }
    else
    {
        texscale = (1.0 / 32.0); // to match r_notexture_mip
    }

    // convert edges back to a normal polygon
    numverts = 0;
    for(i = 0; i < fa->numedges; i++)
    {
        lindex = loadmodel->surfedges[fa->firstedge + i];

        if(lindex > 0)
        {
            vec = toGlVec(
                loadmodel->vertexes[loadmodel->edges[lindex].v[0]].position);
        }
        else
        {
            vec = toGlVec(
                loadmodel->vertexes[loadmodel->edges[-lindex].v[1]].position);
        }
        VectorCopy(vec, verts[numverts]);
        numverts++;
    }

    // create the poly
    poly = (glpoly_t*)Hunk_Alloc(
        sizeof(glpoly_t) + (numverts - 4) * VERTEXSIZE * sizeof(float));
    poly->next = nullptr;
    fa->polys = poly;
    poly->numverts = numverts;
    for(i = 0, vec = (float*)verts; i < numverts; i++, vec += 3)
    {
        VectorCopy(vec, poly->verts[i]);
        poly->verts[i][3] = DotProduct(vec, fa->texinfo->vecs[0]) * texscale;
        poly->verts[i][4] = DotProduct(vec, fa->texinfo->vecs[1]) * texscale;
    }
}

/*
=================
Mod_CalcSurfaceBounds -- johnfitz -- calculate bounding box for per-surface
frustum culling
=================
*/
void Mod_CalcSurfaceBounds(msurface_t* s)
{
    int i, e;
    mvertex_t* v;

    s->mins[0] = s->mins[1] = s->mins[2] = FLT_MAX;
    s->maxs[0] = s->maxs[1] = s->maxs[2] = -FLT_MAX;

    for(i = 0; i < s->numedges; i++)
    {
        e = loadmodel->surfedges[s->firstedge + i];
        if(e >= 0)
        {
            v = &loadmodel->vertexes[loadmodel->edges[e].v[0]];
        }
        else
        {
            v = &loadmodel->vertexes[loadmodel->edges[-e].v[1]];
        }

        if(s->mins[0] > v->position[0])
        {
            s->mins[0] = v->position[0];
        }
        if(s->mins[1] > v->position[1])
        {
            s->mins[1] = v->position[1];
        }
        if(s->mins[2] > v->position[2])
        {
            s->mins[2] = v->position[2];
        }

        if(s->maxs[0] < v->position[0])
        {
            s->maxs[0] = v->position[0];
        }
        if(s->maxs[1] < v->position[1])
        {
            s->maxs[1] = v->position[1];
        }
        if(s->maxs[2] < v->position[2])
        {
            s->maxs[2] = v->position[2];
        }
    }
}

/*
=================
Mod_LoadFaces
=================
*/
void Mod_LoadFaces(lump_t* l, bool bsp2)
{
    dsface_t* ins;
    dlface_t* inl;
    msurface_t* out;
    int i, count, surfnum, lofs, shift;
    int planenum, side, texinfon;

    unsigned char *lmshift = nullptr, defaultshift = 4;
    unsigned int* lmoffset = nullptr;
    unsigned char *lmstyle8 = nullptr, stylesperface = 4;
    unsigned short* lmstyle16 = nullptr;
    int lumpsize;
    char scalebuf[16];

    if(bsp2)
    {
        ins = nullptr;
        inl = (dlface_t*)(mod_base + l->fileofs);
        if(l->filelen % sizeof(*inl))
        {
            Sys_Error("MOD_LoadBmodel: funny lump size in %s", loadmodel->name);
        }
        count = l->filelen / sizeof(*inl);
    }
    else
    {
        ins = (dsface_t*)(mod_base + l->fileofs);
        inl = nullptr;
        if(l->filelen % sizeof(*ins))
        {
            Sys_Error("MOD_LoadBmodel: funny lump size in %s", loadmodel->name);
        }
        count = l->filelen / sizeof(*ins);
    }
    out = (msurface_t*)Hunk_AllocName(count * sizeof(*out), loadname);

    // johnfitz -- warn mappers about exceeding old limits
    if(count > 32767 && !bsp2)
    {
        Con_DWarning("%i faces exceeds standard limit of 32767.\n", count);
    }
    // johnfitz

    if(!mod_ignorelmscale.value)
    {
        lmshift = (unsigned char*)Q1BSPX_FindLump("LMSHIFT", &lumpsize);
        if(lumpsize != sizeof(*lmshift) * count)
        {
            lmshift = nullptr;
        }
        lmoffset = (unsigned int*)Q1BSPX_FindLump("LMOFFSET", &lumpsize);
        if(lumpsize != sizeof(*lmoffset) * count)
        {
            lmoffset = nullptr;
        }
        lmstyle16 = (unsigned short*)Q1BSPX_FindLump("LMSTYLE16", &lumpsize);
        stylesperface = lumpsize / (sizeof(*lmstyle16) * count);
        if(lumpsize != sizeof(*lmstyle16) * stylesperface * count)
        {
            lmstyle16 = nullptr;
        }
        if(!lmstyle16)
        {
            lmstyle8 = (unsigned char*)Q1BSPX_FindLump("LMSTYLE", &lumpsize);
            stylesperface = lumpsize / (sizeof(*lmstyle8) * count);
            if(lumpsize != sizeof(*lmstyle8) * stylesperface * count)
            {
                lmstyle8 = nullptr;
            }
        }

        if(Mod_ParseWorldspawnKey(
               loadmodel, "lightmap_scale", scalebuf, sizeof(scalebuf)))
        {
            i = atoi(scalebuf);
            for(defaultshift = 0; i > 1; defaultshift++)
            {
                i >>= 1;
            }
        }
    }

    loadmodel->surfaces = out;
    loadmodel->numsurfaces = count;

    for(surfnum = 0; surfnum < count; surfnum++, out++)
    {
        if(bsp2)
        { // 32bit datatypes
            out->firstedge = LittleLong(inl->firstedge);
            out->numedges = LittleLong(inl->numedges);
            planenum = LittleLong(inl->planenum);
            side = LittleLong(inl->side);
            texinfon = LittleLong(inl->texinfo);
            for(i = 0; i < 4; i++)
            {
                out->styles[i] = ((inl->styles[i] == INVALID_LIGHTSTYLE_OLD)
                                      ? INVALID_LIGHTSTYLE
                                      : inl->styles[i]);
            }
            lofs = LittleLong(inl->lightofs);
            inl++;
        }
        else
        { // 16bit datatypes
            out->firstedge = LittleLong(ins->firstedge);
            out->numedges = LittleShort(ins->numedges);
            planenum = LittleShort(ins->planenum);
            side = LittleShort(ins->side);
            texinfon = LittleShort(ins->texinfo);
            for(i = 0; i < 4; i++)
            {
                out->styles[i] = ((ins->styles[i] == INVALID_LIGHTSTYLE_OLD)
                                      ? INVALID_LIGHTSTYLE
                                      : ins->styles[i]);
            }
            lofs = LittleLong(ins->lightofs);
            ins++;
        }
        shift = defaultshift;
        // bspx overrides (for lmscale)
        if(lmshift)
        {
            shift = lmshift[surfnum];
        }
        if(lmoffset)
        {
            lofs = LittleLong(lmoffset[surfnum]);
        }
        if(lmstyle16)
        {
            for(i = 0; i < stylesperface; i++)
            {
                out->styles[i] = lmstyle16[surfnum * stylesperface + i];
            }
        }
        else if(lmstyle8)
        {
            for(i = 0; i < stylesperface; i++)
            {
                out->styles[i] = lmstyle8[surfnum * stylesperface + i];
                if(out->styles[i] == INVALID_LIGHTSTYLE_OLD)
                {
                    out->styles[i] = INVALID_LIGHTSTYLE;
                }
            }
        }
        for(; i < MAXLIGHTMAPS; i++)
        {
            out->styles[i] = INVALID_LIGHTSTYLE;
        }

        out->flags = 0;

        if(side)
        {
            out->flags |= SURF_PLANEBACK;
        }

        out->plane = loadmodel->planes + planenum;
        out->texinfo = loadmodel->texinfo + texinfon;
        out->lmshift = shift;

        CalcSurfaceExtents(out);

        Mod_CalcSurfaceBounds(
            out); // johnfitz -- for per-surface frustum culling

        // lighting info
        if(lofs == -1)
        {
            out->samples = nullptr;
        }
        else
        {
            out->samples =
                loadmodel->lightdata +
                (lofs * 3); // johnfitz -- lit support via lordhavoc (was "+ i")
        }

        // johnfitz -- this section rewritten
        if(!q_strncasecmp(out->texinfo->texture->name, "sky",
               3)) // sky surface //also note -- was Q_strncmp, changed to match
                   // qbsp
        {
            out->flags |= (SURF_DRAWSKY | SURF_DRAWTILED);
            Mod_PolyForUnlitSurface(out); // no more subdivision
        }
        else if(out->texinfo->texture->name[0] == '*') // warp surface
        {
            out->flags |= (SURF_DRAWTURB | SURF_DRAWTILED);

            // detect special liquid types
            if(!strncmp(out->texinfo->texture->name, "*lava", 5))
            {
                out->flags |= SURF_DRAWLAVA;
            }
            else if(!strncmp(out->texinfo->texture->name, "*slime", 6))
            {
                out->flags |= SURF_DRAWSLIME;
            }
            else if(!strncmp(out->texinfo->texture->name, "*tele", 5))
            {
                out->flags |= SURF_DRAWTELE;
            }
            else
            {
                out->flags |= SURF_DRAWWATER;
            }

            Mod_PolyForUnlitSurface(out);
            GL_SubdivideSurface(out);
        }
        else if(out->texinfo->texture->name[0] ==
                '{') // ericw -- fence textures
        {
            out->flags |= SURF_DRAWFENCE;
        }
        else if(out->texinfo->flags &
                TEX_MISSING) // texture is missing from bsp
        {
            if(out->samples) // lightmapped
            {
                out->flags |= SURF_NOTEXTURE;
            }
            else // not lightmapped
            {
                out->flags |= (SURF_NOTEXTURE | SURF_DRAWTILED);
                Mod_PolyForUnlitSurface(out);
            }
        }
        // johnfitz
    }
}


/*
=================
Mod_SetParent
=================
*/
void Mod_SetParent(mnode_t* node, mnode_t* parent)
{
    node->parent = parent;
    if(node->contents < 0)
    {
        return;
    }
    Mod_SetParent(node->children[0], node);
    Mod_SetParent(node->children[1], node);
}

/*
=================
Mod_LoadNodes
=================
*/
void Mod_LoadNodes_S(lump_t* l)
{
    int i, j, count, p;
    dsnode_t* in;
    mnode_t* out;

    in = (dsnode_t*)(mod_base + l->fileofs);
    if(l->filelen % sizeof(*in))
    {
        Sys_Error("MOD_LoadBmodel: funny lump size in %s", loadmodel->name);
    }
    count = l->filelen / sizeof(*in);
    out = (mnode_t*)Hunk_AllocName(count * sizeof(*out), loadname);

    // johnfitz -- warn mappers about exceeding old limits
    if(count > 32767)
    {
        Con_DWarning("%i nodes exceeds standard limit of 32767.\n", count);
    }
    // johnfitz

    loadmodel->nodes = out;
    loadmodel->numnodes = count;

    for(i = 0; i < count; i++, in++, out++)
    {
        for(j = 0; j < 3; j++)
        {
            out->minmaxs[j] = LittleShort(in->mins[j]);
            out->minmaxs[3 + j] = LittleShort(in->maxs[j]);
        }

        p = LittleLong(in->planenum);
        out->plane = loadmodel->planes + p;

        out->firstsurface = (unsigned short)LittleShort(
            in->firstface); // johnfitz -- explicit cast as unsigned short
        out->numsurfaces = (unsigned short)LittleShort(
            in->numfaces); // johnfitz -- explicit cast as unsigned short

        for(j = 0; j < 2; j++)
        {
            // johnfitz -- hack to handle nodes > 32k, adapted from darkplaces
            p = (unsigned short)LittleShort(in->children[j]);
            if(p < count)
            {
                out->children[j] = loadmodel->nodes + p;
            }
            else
            {
                p = 65535 -
                    p; // note this uses 65535 intentionally, -1 is leaf 0
                if(p < loadmodel->numleafs)
                {
                    out->children[j] = (mnode_t*)(loadmodel->leafs + p);
                }
                else
                {
                    Con_Printf(
                        "Mod_LoadNodes: invalid leaf index %i (file has only "
                        "%i leafs)\n",
                        p, loadmodel->numleafs);
                    out->children[j] =
                        (mnode_t*)(loadmodel
                                       ->leafs); // map it to the solid leaf
                }
            }
            // johnfitz
        }
    }
}

void Mod_LoadNodes_L1(lump_t* l)
{
    int i, j, count, p;
    dl1node_t* in;
    mnode_t* out;

    in = (dl1node_t*)(mod_base + l->fileofs);
    if(l->filelen % sizeof(*in))
    {
        Sys_Error("Mod_LoadNodes: funny lump size in %s", loadmodel->name);
    }

    count = l->filelen / sizeof(*in);
    out = (mnode_t*)Hunk_AllocName(count * sizeof(*out), loadname);

    loadmodel->nodes = out;
    loadmodel->numnodes = count;

    for(i = 0; i < count; i++, in++, out++)
    {
        for(j = 0; j < 3; j++)
        {
            out->minmaxs[j] = LittleShort(in->mins[j]);
            out->minmaxs[3 + j] = LittleShort(in->maxs[j]);
        }

        p = LittleLong(in->planenum);
        out->plane = loadmodel->planes + p;

        out->firstsurface = LittleLong(
            in->firstface); // johnfitz -- explicit cast as unsigned short
        out->numsurfaces = LittleLong(
            in->numfaces); // johnfitz -- explicit cast as unsigned short

        for(j = 0; j < 2; j++)
        {
            // johnfitz -- hack to handle nodes > 32k, adapted from darkplaces
            p = LittleLong(in->children[j]);
            if(p >= 0 && p < count)
            {
                out->children[j] = loadmodel->nodes + p;
            }
            else
            {
                p = 0xffffffff -
                    p; // note this uses 65535 intentionally, -1 is leaf 0
                if(p >= 0 && p < loadmodel->numleafs)
                {
                    out->children[j] = (mnode_t*)(loadmodel->leafs + p);
                }
                else
                {
                    Con_Printf(
                        "Mod_LoadNodes: invalid leaf index %i (file has only "
                        "%i leafs)\n",
                        p, loadmodel->numleafs);
                    out->children[j] =
                        (mnode_t*)(loadmodel
                                       ->leafs); // map it to the solid leaf
                }
            }
            // johnfitz
        }
    }
}

void Mod_LoadNodes_L2(lump_t* l)
{
    int i, j, count, p;
    dl2node_t* in;
    mnode_t* out;

    in = (dl2node_t*)(mod_base + l->fileofs);
    if(l->filelen % sizeof(*in))
    {
        Sys_Error("Mod_LoadNodes: funny lump size in %s", loadmodel->name);
    }

    count = l->filelen / sizeof(*in);
    out = (mnode_t*)Hunk_AllocName(count * sizeof(*out), loadname);

    loadmodel->nodes = out;
    loadmodel->numnodes = count;

    for(i = 0; i < count; i++, in++, out++)
    {
        for(j = 0; j < 3; j++)
        {
            out->minmaxs[j] = LittleFloat(in->mins[j]);
            out->minmaxs[3 + j] = LittleFloat(in->maxs[j]);
        }

        p = LittleLong(in->planenum);
        out->plane = loadmodel->planes + p;

        out->firstsurface = LittleLong(
            in->firstface); // johnfitz -- explicit cast as unsigned short
        out->numsurfaces = LittleLong(
            in->numfaces); // johnfitz -- explicit cast as unsigned short

        for(j = 0; j < 2; j++)
        {
            // johnfitz -- hack to handle nodes > 32k, adapted from darkplaces
            p = LittleLong(in->children[j]);
            if(p > 0 && p < count)
            {
                out->children[j] = loadmodel->nodes + p;
            }
            else
            {
                p = 0xffffffff -
                    p; // note this uses 65535 intentionally, -1 is leaf 0
                if(p >= 0 && p < loadmodel->numleafs)
                {
                    out->children[j] = (mnode_t*)(loadmodel->leafs + p);
                }
                else
                {
                    Con_Printf(
                        "Mod_LoadNodes: invalid leaf index %i (file has only "
                        "%i leafs)\n",
                        p, loadmodel->numleafs);
                    out->children[j] =
                        (mnode_t*)(loadmodel
                                       ->leafs); // map it to the solid leaf
                }
            }
            // johnfitz
        }
    }
}

void Mod_LoadNodes(lump_t* l, int bsp2)
{
    if(bsp2 == 2)
    {
        Mod_LoadNodes_L2(l);
    }
    else if(bsp2)
    {
        Mod_LoadNodes_L1(l);
    }
    else
    {
        Mod_LoadNodes_S(l);
    }

    //	Mod_SetParent (loadmodel->nodes, nullptr);	// sets nodes and leafs
}

void Mod_ProcessLeafs_S(dsleaf_t* in, int filelen)
{
    mleaf_t* out;
    int i, j, count, p;

    if(filelen % sizeof(*in))
    {
        Sys_Error("Mod_ProcessLeafs: funny lump size in %s", loadmodel->name);
    }
    count = filelen / sizeof(*in);
    out = (mleaf_t*)Hunk_AllocName(count * sizeof(*out), loadname);

    // johnfitz
    if(count > 32767)
    {
        Host_Error("Mod_LoadLeafs: %i leafs exceeds limit of 32767.\n", count);
    }
    // johnfitz

    loadmodel->leafs = out;
    loadmodel->numleafs = count;

    for(i = 0; i < count; i++, in++, out++)
    {
        for(j = 0; j < 3; j++)
        {
            out->minmaxs[j] = LittleShort(in->mins[j]);
            out->minmaxs[3 + j] = LittleShort(in->maxs[j]);
        }

        p = LittleLong(in->contents);
        out->contents = p;

        out->firstmarksurface =
            loadmodel->marksurfaces +
            (unsigned short)LittleShort(
                in->firstmarksurface); // johnfitz -- unsigned short
        out->nummarksurfaces = (unsigned short)LittleShort(
            in->nummarksurfaces); // johnfitz -- unsigned short

        p = LittleLong(in->visofs);
        if(p == -1)
        {
            out->compressed_vis = nullptr;
        }
        else
        {
            out->compressed_vis = loadmodel->visdata + p;
        }
        out->efrags = nullptr;

        for(j = 0; j < 4; j++)
        {
            out->ambient_sound_level[j] = in->ambient_level[j];
        }

        // johnfitz -- removed code to mark surfaces as SURF_UNDERWATER
    }
}

void Mod_ProcessLeafs_L1(dl1leaf_t* in, int filelen)
{
    mleaf_t* out;
    int i, j, count, p;

    if(filelen % sizeof(*in))
    {
        Sys_Error("Mod_ProcessLeafs: funny lump size in %s", loadmodel->name);
    }

    count = filelen / sizeof(*in);

    out = (mleaf_t*)Hunk_AllocName(count * sizeof(*out), loadname);

    loadmodel->leafs = out;
    loadmodel->numleafs = count;

    for(i = 0; i < count; i++, in++, out++)
    {
        for(j = 0; j < 3; j++)
        {
            out->minmaxs[j] = LittleShort(in->mins[j]);
            out->minmaxs[3 + j] = LittleShort(in->maxs[j]);
        }

        p = LittleLong(in->contents);
        out->contents = p;

        out->firstmarksurface =
            loadmodel->marksurfaces +
            LittleLong(in->firstmarksurface); // johnfitz -- unsigned short
        out->nummarksurfaces =
            LittleLong(in->nummarksurfaces); // johnfitz -- unsigned short

        p = LittleLong(in->visofs);
        if(p == -1)
        {
            out->compressed_vis = nullptr;
        }
        else
        {
            out->compressed_vis = loadmodel->visdata + p;
        }
        out->efrags = nullptr;

        for(j = 0; j < 4; j++)
        {
            out->ambient_sound_level[j] = in->ambient_level[j];
        }

        // johnfitz -- removed code to mark surfaces as SURF_UNDERWATER
    }
}

void Mod_ProcessLeafs_L2(dl2leaf_t* in, int filelen)
{
    mleaf_t* out;
    int i, j, count, p;

    if(filelen % sizeof(*in))
    {
        Sys_Error("Mod_ProcessLeafs: funny lump size in %s", loadmodel->name);
    }

    count = filelen / sizeof(*in);

    out = (mleaf_t*)Hunk_AllocName(count * sizeof(*out), loadname);

    loadmodel->leafs = out;
    loadmodel->numleafs = count;

    for(i = 0; i < count; i++, in++, out++)
    {
        for(j = 0; j < 3; j++)
        {
            out->minmaxs[j] = LittleFloat(in->mins[j]);
            out->minmaxs[3 + j] = LittleFloat(in->maxs[j]);
        }

        p = LittleLong(in->contents);
        out->contents = p;

        out->firstmarksurface =
            loadmodel->marksurfaces +
            LittleLong(in->firstmarksurface); // johnfitz -- unsigned short
        out->nummarksurfaces =
            LittleLong(in->nummarksurfaces); // johnfitz -- unsigned short

        p = LittleLong(in->visofs);
        if(p == -1)
        {
            out->compressed_vis = nullptr;
        }
        else
        {
            out->compressed_vis = loadmodel->visdata + p;
        }
        out->efrags = nullptr;

        for(j = 0; j < 4; j++)
        {
            out->ambient_sound_level[j] = in->ambient_level[j];
        }

        // johnfitz -- removed code to mark surfaces as SURF_UNDERWATER
    }
}

/*
=================
Mod_LoadLeafs
=================
*/
void Mod_LoadLeafs(lump_t* l, int bsp2)
{
    void* in = (void*)(mod_base + l->fileofs);

    if(bsp2 == 2)
    {
        Mod_ProcessLeafs_L2((dl2leaf_t*)in, l->filelen);
    }
    else if(bsp2)
    {
        Mod_ProcessLeafs_L1((dl1leaf_t*)in, l->filelen);
    }
    else
    {
        Mod_ProcessLeafs_S((dsleaf_t*)in, l->filelen);
    }
}

void Mod_CheckWaterVis()
{
    mleaf_t *leaf, *other;
    int i, j, k;
    int numclusters = loadmodel->submodels[0].visleafs;
    int contentfound = 0;
    int contenttransparent = 0;
    int contenttype;

    if(r_novis.value)
    { // all can be
        loadmodel->contentstransparent =
            (SURF_DRAWWATER | SURF_DRAWTELE | SURF_DRAWSLIME | SURF_DRAWLAVA);
        return;
    }

    // pvs is 1-based. leaf 0 sees all (the solid leaf).
    // leaf 0 has no pvs, and does not appear in other leafs either, so watch
    // out for the biases.
    for(i = 0, leaf = loadmodel->leafs + 1; i < numclusters; i++, leaf++)
    {
        byte* vis;
        if(leaf->contents == CONTENTS_WATER)
        {
            if((contenttransparent & (SURF_DRAWWATER | SURF_DRAWTELE)) ==
                (SURF_DRAWWATER | SURF_DRAWTELE))
            {
                continue;
            }
            // this check is somewhat risky, but we should be able to get away
            // with it.
            for(contenttype = 0, i = 0; i < leaf->nummarksurfaces; i++)
            {
                if(leaf->firstmarksurface[i]->flags &
                    (SURF_DRAWWATER | SURF_DRAWTELE))
                {
                    contenttype = leaf->firstmarksurface[i]->flags &
                                  (SURF_DRAWWATER | SURF_DRAWTELE);
                    break;
                }
            }
            // its possible that this leaf has absolutely no surfaces in it,
            // turb or otherwise.
            if(contenttype == 0)
            {
                continue;
            }
        }
        else if(leaf->contents == CONTENTS_SLIME)
        {
            contenttype = SURF_DRAWSLIME;
        }
        else if(leaf->contents == CONTENTS_LAVA)
        {
            contenttype = SURF_DRAWLAVA;
            // fixme: tele
        }
        else
        {
            continue;
        }
        if(contenttransparent & contenttype)
        {
        nextleaf:
            continue; // found one of this type already
        }
        contentfound |= contenttype;
        vis = Mod_DecompressVis(leaf->compressed_vis, loadmodel);
        for(j = 0; j < (numclusters + 7) / 8; j++)
        {
            if(vis[j])
            {
                for(k = 0; k < 8; k++)
                {
                    if(vis[j] & (1u << k))
                    {
                        other = &loadmodel->leafs[(j << 3) + k + 1];
                        if(leaf->contents != other->contents)
                        {
                            //							Con_Printf("%p:%i sees
                            //%p:%i\n", leaf, leaf->contents, other,
                            // other->contents);
                            contenttransparent |= contenttype;
                            goto nextleaf;
                        }
                    }
                }
            }
        }
    }

    if(!contenttransparent)
    {
        Con_DPrintf("%s is not watervised\n", loadmodel->name);
    }
    else
    {
        Con_DPrintf("%s is vised for transparent", loadmodel->name);
        if(contenttransparent & SURF_DRAWWATER)
        {
            Con_DPrintf(" water");
        }
        if(contenttransparent & SURF_DRAWTELE)
        {
            Con_DPrintf(" tele");
        }
        if(contenttransparent & SURF_DRAWLAVA)
        {
            Con_DPrintf(" lava");
        }
        if(contenttransparent & SURF_DRAWSLIME)
        {
            Con_DPrintf(" slime");
        }
        Con_DPrintf("\n");
    }
    // any types that we didn't find are assumed to be transparent.
    // this allows submodels to work okay (eg: ad uses func_illusionary
    // teleporters for some reason).
    loadmodel->contentstransparent =
        contenttransparent |
        (~contentfound &
            (SURF_DRAWWATER | SURF_DRAWTELE | SURF_DRAWSLIME | SURF_DRAWLAVA));
}

/*
=================
Mod_LoadClipnodes
=================
*/
void Mod_LoadClipnodes(lump_t* l, bool bsp2)
{
    dsclipnode_t* ins;
    dlclipnode_t* inl;

    mclipnode_t* out; // johnfitz -- was dclipnode_t
    int i, count;
    hull_t* hull;

    if(bsp2)
    {
        ins = nullptr;
        inl = (dlclipnode_t*)(mod_base + l->fileofs);
        if(l->filelen % sizeof(*inl))
        {
            Sys_Error(
                "Mod_LoadClipnodes: funny lump size in %s", loadmodel->name);
        }

        count = l->filelen / sizeof(*inl);
    }
    else
    {
        ins = (dsclipnode_t*)(mod_base + l->fileofs);
        inl = nullptr;
        if(l->filelen % sizeof(*ins))
        {
            Sys_Error(
                "Mod_LoadClipnodes: funny lump size in %s", loadmodel->name);
        }

        count = l->filelen / sizeof(*ins);
    }
    if(count)
    {
        out = (mclipnode_t*)Hunk_AllocName(count * sizeof(*out), loadname);
    }
    else
    {
        out = nullptr; // will use rnodes.
    }

    // johnfitz -- warn about exceeding old limits
    if(count > 32767 && !bsp2)
    {
        Con_DWarning("%i clipnodes exceeds standard limit of 32767.\n", count);
    }
    // johnfitz

    loadmodel->clipnodes = out;
    loadmodel->numclipnodes = count;

    // ------------------------------------------------------------------------
    // VR: This is where the hardcoded hulls are.
    hull = &loadmodel->hulls[1];
    hull->clipnodes = out;
    hull->firstclipnode = 0;
    hull->lastclipnode = count - 1;
    hull->planes = loadmodel->planes;
    hull->clip_mins[0] = -16;
    hull->clip_mins[1] = -16;
    hull->clip_mins[2] = -24;
    hull->clip_maxs[0] = 16;
    hull->clip_maxs[1] = 16;
    hull->clip_maxs[2] = 32;

    hull = &loadmodel->hulls[2];
    hull->clipnodes = out;
    hull->firstclipnode = 0;
    hull->lastclipnode = count - 1;
    hull->planes = loadmodel->planes;
    hull->clip_mins[0] = -32;
    hull->clip_mins[1] = -32;
    hull->clip_mins[2] = -24;
    hull->clip_maxs[0] = 32;
    hull->clip_maxs[1] = 32;
    hull->clip_maxs[2] = 64;
    // ------------------------------------------------------------------------

    if(bsp2)
    {
        for(i = 0; i < count; i++, out++, inl++)
        {
            out->planenum = LittleLong(inl->planenum);

            // johnfitz -- bounds check
            if(out->planenum < 0 || out->planenum >= loadmodel->numplanes)
            {
                Host_Error("Mod_LoadClipnodes: planenum out of bounds");
            }
            // johnfitz

            out->children[0] = LittleLong(inl->children[0]);
            out->children[1] = LittleLong(inl->children[1]);
            // Spike: FIXME: bounds check
        }
    }
    else
    {
        for(i = 0; i < count; i++, out++, ins++)
        {
            out->planenum = LittleLong(ins->planenum);

            // johnfitz -- bounds check
            if(out->planenum < 0 || out->planenum >= loadmodel->numplanes)
            {
                Host_Error("Mod_LoadClipnodes: planenum out of bounds");
            }
            // johnfitz

            // johnfitz -- support clipnodes > 32k
            out->children[0] = (unsigned short)LittleShort(ins->children[0]);
            out->children[1] = (unsigned short)LittleShort(ins->children[1]);

            if(out->children[0] >= count)
            {
                out->children[0] -= 65536;
            }
            if(out->children[1] >= count)
            {
                out->children[1] -= 65536;
            }
            // johnfitz
        }
    }
}

/*
=================
Mod_MakeHull0

Duplicate the drawing hull structure as a clipping hull
=================
*/
void Mod_MakeHull0()
{
    mnode_t *in, *child;
    mclipnode_t* out; // johnfitz -- was dclipnode_t
    int i, j, count;
    hull_t* hull;

    hull = &loadmodel->hulls[0];

    in = loadmodel->nodes;
    count = loadmodel->numnodes;
    out = (mclipnode_t*)Hunk_AllocName(count * sizeof(*out), loadname);

    hull->clipnodes = out;
    hull->firstclipnode = 0;
    hull->lastclipnode = count - 1;
    hull->planes = loadmodel->planes;

    for(i = 0; i < count; i++, out++, in++)
    {
        out->planenum = in->plane - loadmodel->planes;
        for(j = 0; j < 2; j++)
        {
            child = in->children[j];
            if(child->contents < 0)
            {
                out->children[j] = child->contents;
            }
            else
            {
                out->children[j] = child - loadmodel->nodes;
            }
        }
    }

    // if qbsp was run with -noclip, make sure the extra hulls use the rnodes
    // instead of the missing clipnodes this won't 'fix' it, but it will stop it
    // from crashing if it was just quickly built for debugging or whatever.
    if(!loadmodel->hulls[1].clipnodes)
    { // hulls will be point-sized.
        // bias that point so that its mid,mid,bottom instead of at the absmin
        // or origin. this will retain view offsets.
        loadmodel->hulls[1].clip_maxs[2] -= loadmodel->hulls[1].clip_mins[2];
        loadmodel->hulls[1].clip_mins[2] = 0;
        loadmodel->hulls[1].clipnodes = hull->clipnodes;
        loadmodel->hulls[1].firstclipnode = hull->firstclipnode;
        loadmodel->hulls[1].lastclipnode = hull->lastclipnode;
    }
    if(!loadmodel->hulls[2].clipnodes)
    {
        loadmodel->hulls[2].clip_maxs[2] -= loadmodel->hulls[2].clip_mins[2];
        loadmodel->hulls[2].clip_mins[2] = 0;
        loadmodel->hulls[2].clipnodes = loadmodel->hulls[1].clipnodes;
        loadmodel->hulls[2].firstclipnode = loadmodel->hulls[1].firstclipnode;
        loadmodel->hulls[2].lastclipnode = loadmodel->hulls[1].lastclipnode;
    }
}

/*
=================
Mod_LoadMarksurfaces
=================
*/
void Mod_LoadMarksurfaces(lump_t* l, int bsp2)
{
    int i, j, count;
    msurface_t** out;
    if(bsp2)
    {
        unsigned int* in = (unsigned int*)(mod_base + l->fileofs);

        if(l->filelen % sizeof(*in))
        {
            Host_Error(
                "Mod_LoadMarksurfaces: funny lump size in %s", loadmodel->name);
        }

        count = l->filelen / sizeof(*in);
        out = (msurface_t**)Hunk_AllocName(count * sizeof(*out), loadname);

        loadmodel->marksurfaces = out;
        loadmodel->nummarksurfaces = count;

        for(i = 0; i < count; i++)
        {
            j = LittleLong(in[i]);
            if(j >= loadmodel->numsurfaces)
            {
                Host_Error("Mod_LoadMarksurfaces: bad surface number");
            }
            out[i] = loadmodel->surfaces + j;
        }
    }
    else
    {
        short* in = (short*)(mod_base + l->fileofs);

        if(l->filelen % sizeof(*in))
        {
            Host_Error(
                "Mod_LoadMarksurfaces: funny lump size in %s", loadmodel->name);
        }

        count = l->filelen / sizeof(*in);
        out = (msurface_t**)Hunk_AllocName(count * sizeof(*out), loadname);

        loadmodel->marksurfaces = out;
        loadmodel->nummarksurfaces = count;

        // johnfitz -- warn mappers about exceeding old limits
        if(count > 32767)
        {
            Con_DWarning(
                "%i marksurfaces exceeds standard limit of 32767.\n", count);
        }
        // johnfitz

        for(i = 0; i < count; i++)
        {
            j = (unsigned short)LittleShort(
                in[i]); // johnfitz -- explicit cast as unsigned short
            if(j >= loadmodel->numsurfaces)
            {
                Sys_Error("Mod_LoadMarksurfaces: bad surface number");
            }
            out[i] = loadmodel->surfaces + j;
        }
    }
}

/*
=================
Mod_LoadSurfedges
=================
*/
void Mod_LoadSurfedges(lump_t* l)
{
    int i, count;
    int *in, *out;

    in = (int*)(mod_base + l->fileofs);
    if(l->filelen % sizeof(*in))
    {
        Sys_Error("MOD_LoadBmodel: funny lump size in %s", loadmodel->name);
    }
    count = l->filelen / sizeof(*in);
    out = (int*)Hunk_AllocName(count * sizeof(*out), loadname);

    loadmodel->surfedges = out;
    loadmodel->numsurfedges = count;

    for(i = 0; i < count; i++)
    {
        out[i] = LittleLong(in[i]);
    }
}


/*
=================
Mod_LoadPlanes
=================
*/
void Mod_LoadPlanes(lump_t* l)
{
    int i, j;
    mplane_t* out;
    dplane_t* in;
    int count;
    int bits;

    in = (dplane_t*)(mod_base + l->fileofs);
    if(l->filelen % sizeof(*in))
    {
        Sys_Error("MOD_LoadBmodel: funny lump size in %s", loadmodel->name);
    }
    count = l->filelen / sizeof(*in);
    out = (mplane_t*)Hunk_AllocName(count * 2 * sizeof(*out), loadname);

    loadmodel->planes = out;
    loadmodel->numplanes = count;

    for(i = 0; i < count; i++, in++, out++)
    {
        bits = 0;
        for(j = 0; j < 3; j++)
        {
            out->normal[j] = LittleFloat(in->normal[j]);
            if(out->normal[j] < 0)
            {
                bits |= 1 << j;
            }
        }

        out->dist = LittleFloat(in->dist);
        out->type = LittleLong(in->type);
        out->signbits = bits;
    }
}

/*
=================
RadiusFromBounds
=================
*/
float RadiusFromBounds(const qvec3& mins, const qvec3& maxs)
{
    qvec3 corner;

    for(int i = 0; i < 3; i++)
    {
        corner[i] =
            fabs(mins[i]) > fabs(maxs[i]) ? fabs(mins[i]) : fabs(maxs[i]);
    }

    return glm::length(corner);
}

/*
=================
Mod_LoadSubmodels
=================
*/
void Mod_LoadSubmodels(lump_t* l)
{
    mmodel_t* out;
    size_t i, j, count;

    // detect whether this is a hexen2 8-hull map or a quake 4-hull map
    dmodelq1_t* inq1 = (dmodelq1_t*)(mod_base + l->fileofs);
    dmodelh2_t* inh2 = (dmodelh2_t*)(mod_base + l->fileofs);
    // the numfaces is a bit of a hack. hexen2 only actually uses 6 of its 8
    // hulls and we depend upon this. this means that the 7th and 8th are null.
    // q1.numfaces of the world equates to h2.hull[6], so should have a value
    // for q1, and be 0 for hexen2. this should work even for maps that have
    // enough submodels to realign the size. note that even if the map loads,
    // you're on your own regarding the palette (hurrah for retexturing
    // projects?). fixme: we don't fix up the clipnodes yet, the player is fine,
    // shamblers/ogres/fiends/vores will have issues. unfortunately c doesn't do
    // templates, which would make all this code a bit less copypastay
    if((size_t)l->filelen >= sizeof(*inh2) && !(l->filelen % sizeof(*inh2)) &&
        !inq1->numfaces && inq1[1].firstface)
    {
        dmodelh2_t* in = inh2;
        if(l->filelen % sizeof(*in))
        {
            Sys_Error("MOD_LoadBmodel: funny lump size in %s", loadmodel->name);
        }
        count = l->filelen / sizeof(*in);
        out = (mmodel_t*)Hunk_AllocName(count * sizeof(*out), loadname);

        loadmodel->submodels = out;
        loadmodel->numsubmodels = count;

        for(i = 0; i < count; i++, in++, out++)
        {
            for(j = 0; j < 3; j++)
            { // spread the mins / maxs by a pixel
                out->mins[j] = LittleFloat(in->mins[j]) - 1;
                out->maxs[j] = LittleFloat(in->maxs[j]) + 1;
                out->origin[j] = LittleFloat(in->origin[j]);
            }
            for(j = 0; j < MAX_MAP_HULLS &&
                       j < sizeof(in->headnode) / sizeof(in->headnode[0]);
                j++)
            {
                out->headnode[j] = LittleLong(in->headnode[j]);
            }
            for(; j < MAX_MAP_HULLS; j++)
            {
                out->headnode[j] = 0;
            }
            out->visleafs = LittleLong(in->visleafs);
            out->firstface = LittleLong(in->firstface);
            out->numfaces = LittleLong(in->numfaces);
        }
    }
    else
    {
        dmodelq1_t* in = inq1;
        if(l->filelen % sizeof(*in))
        {
            Sys_Error("MOD_LoadBmodel: funny lump size in %s", loadmodel->name);
        }
        count = l->filelen / sizeof(*in);
        out = (mmodel_t*)Hunk_AllocName(count * sizeof(*out), loadname);

        loadmodel->submodels = out;
        loadmodel->numsubmodels = count;

        for(i = 0; i < count; i++, in++, out++)
        {
            for(j = 0; j < 3; j++)
            { // spread the mins / maxs by a pixel
                out->mins[j] = LittleFloat(in->mins[j]) - 1;
                out->maxs[j] = LittleFloat(in->maxs[j]) + 1;
                out->origin[j] = LittleFloat(in->origin[j]);
            }
            for(j = 0; j < MAX_MAP_HULLS &&
                       j < sizeof(in->headnode) / sizeof(in->headnode[0]);
                j++)
            {
                out->headnode[j] = LittleLong(in->headnode[j]);
            }
            for(; j < MAX_MAP_HULLS; j++)
            {
                out->headnode[j] = 0;
            }
            out->visleafs = LittleLong(in->visleafs);
            out->firstface = LittleLong(in->firstface);
            out->numfaces = LittleLong(in->numfaces);
        }
    }

    // johnfitz -- check world visleafs -- adapted from bjp
    out = loadmodel->submodels;

    if(out->visleafs > 8192)
    {
        Con_DWarning(
            "%i visleafs exceeds standard limit of 8192.\n", out->visleafs);
    }
    // johnfitz
}

/*
=================
Mod_BoundsFromClipNode -- johnfitz

update the model's clipmins and clipmaxs based on each node's plane.

This works because of the way brushes are expanded in hull generation.
Each brush will include all six axial planes, which bound that brush.
Therefore, the bounding box of the hull can be constructed entirely
from axial planes found in the clipnodes for that hull.
=================
*/
void Mod_BoundsFromClipNode(qmodel_t* mod, int hull, int nodenum)
{
    mplane_t* plane;
    mclipnode_t* node;

    if(nodenum < 0)
    {
        return; // hit a leafnode
    }

    node = &mod->clipnodes[nodenum];
    plane = mod->hulls[hull].planes + node->planenum;
    switch(plane->type)
    {

        case PLANE_X:
            if(plane->signbits == 1)
            {
                mod->clipmins[0] = q_min(mod->clipmins[0],
                    -plane->dist - mod->hulls[hull].clip_mins[0]);
            }
            else
            {
                mod->clipmaxs[0] = q_max(mod->clipmaxs[0],
                    plane->dist - mod->hulls[hull].clip_maxs[0]);
            }
            break;
        case PLANE_Y:
            if(plane->signbits == 2)
            {
                mod->clipmins[1] = q_min(mod->clipmins[1],
                    -plane->dist - mod->hulls[hull].clip_mins[1]);
            }
            else
            {
                mod->clipmaxs[1] = q_max(mod->clipmaxs[1],
                    plane->dist - mod->hulls[hull].clip_maxs[1]);
            }
            break;
        case PLANE_Z:
            if(plane->signbits == 4)
            {
                mod->clipmins[2] = q_min(mod->clipmins[2],
                    -plane->dist - mod->hulls[hull].clip_mins[2]);
            }
            else
            {
                mod->clipmaxs[2] = q_max(mod->clipmaxs[2],
                    plane->dist - mod->hulls[hull].clip_maxs[2]);
            }
            break;
        default:
            // skip nonaxial planes; don't need them
            break;
    }

    Mod_BoundsFromClipNode(mod, hull, node->children[0]);
    Mod_BoundsFromClipNode(mod, hull, node->children[1]);
}

/*
=================
Mod_LoadBrushModel
=================
*/
void Mod_LoadBrushModel(qmodel_t* mod, void* buffer)
{
    int i, j;
    int bsp2;
    dheader_t* header;
    mmodel_t* bm;
    float radius; // johnfitz

    loadmodel->type = mod_brush;

    header = (dheader_t*)buffer;

    mod->bspversion = LittleLong(header->version);

    switch(mod->bspversion)
    {
        case BSPVERSION: bsp2 = false; break;
        case BSP2VERSION_2PSB:
            bsp2 = 1; // first iteration
            break;
        case BSP2VERSION_BSP2:
            bsp2 = 2; // sanitised revision
            break;
        default:
            loadmodel->type = mod_ext_invalid;
            Con_Warning(
                "Mod_LoadBrushModel: %s has wrong version number (%i should be "
                "%i)\n",
                mod->name, mod->bspversion, BSPVERSION);
            return;
    }

    // swap all the lumps
    mod_base = (byte*)header;

    for(i = 0; i < (int)sizeof(dheader_t) / 4; i++)
    {
        ((int*)header)[i] = LittleLong(((int*)header)[i]);
    }

    Q1BSPX_Setup(mod, (char*)buffer, com_filesize, header->lumps, HEADER_LUMPS);

    // load into heap

    Mod_LoadVertexes(&header->lumps[LUMP_VERTEXES]);
    Mod_LoadEdges(&header->lumps[LUMP_EDGES], bsp2);
    Mod_LoadSurfedges(&header->lumps[LUMP_SURFEDGES]);
    Mod_LoadTextures(&header->lumps[LUMP_TEXTURES]);
    Mod_LoadLighting(&header->lumps[LUMP_LIGHTING]);
    Mod_LoadPlanes(&header->lumps[LUMP_PLANES]);
    Mod_LoadTexinfo(&header->lumps[LUMP_TEXINFO]);
    Mod_LoadEntities(
        &header->lumps[LUMP_ENTITIES]); // Spike: moved this earlier, so that we
                                        // can parse worldspawn keys earlier.
    Mod_LoadFaces(&header->lumps[LUMP_FACES], bsp2);
    Mod_LoadMarksurfaces(&header->lumps[LUMP_MARKSURFACES], bsp2);
    Mod_LoadVisibility(&header->lumps[LUMP_VISIBILITY]);
    Mod_LoadLeafs(&header->lumps[LUMP_LEAFS], bsp2);
    Mod_LoadNodes(&header->lumps[LUMP_NODES], bsp2);
    Mod_LoadClipnodes(&header->lumps[LUMP_CLIPNODES], bsp2);
    Mod_LoadSubmodels(&header->lumps[LUMP_MODELS]);

    Mod_MakeHull0();

    mod->numframes = 2; // regular and alternate animation

    Mod_CheckWaterVis();

    //
    // set up the submodels (FIXME: this is confusing)
    //

    // johnfitz -- okay, so that i stop getting confused every time i look at
    // this loop, here's how it works: we're looping through the submodels
    // starting at 0.  Submodel 0 is the main model, so we don't have to worry
    // about clobbering data the first time through, since it's the same data.
    // At the end of the loop, we create a new copy of the data to use the next
    // time through.
    for(i = 0; i < mod->numsubmodels; i++)
    {
        bm = &mod->submodels[i];

        mod->hulls[0].firstclipnode = bm->headnode[0];
        for(j = 1; j < MAX_MAP_HULLS; j++)
        {
            mod->hulls[j].firstclipnode = bm->headnode[j];
            if(mod->hulls[j].clipnodes == mod->hulls[0].clipnodes)
            {
                mod->hulls[j].lastclipnode = mod->hulls[0].lastclipnode;
            }
            else
            {
                mod->hulls[j].lastclipnode = mod->numclipnodes - 1;
            }
        }

        mod->firstmodelsurface = bm->firstface;
        mod->nummodelsurfaces = bm->numfaces;

        VectorCopy(bm->maxs, mod->maxs);
        VectorCopy(bm->mins, mod->mins);

        // johnfitz -- calculate rotate bounds and yaw bounds
        radius = RadiusFromBounds(mod->mins, mod->maxs);
        mod->rmaxs[0] = mod->rmaxs[1] = mod->rmaxs[2] = mod->ymaxs[0] =
            mod->ymaxs[1] = mod->ymaxs[2] = radius;
        mod->rmins[0] = mod->rmins[1] = mod->rmins[2] = mod->ymins[0] =
            mod->ymins[1] = mod->ymins[2] = -radius;
        // johnfitz

        // johnfitz -- correct physics cullboxes so that outlying clip brushes
        // on doors and stuff are handled right
        if(i > 0 || strcmp(mod->name, sv.modelname) !=
                        0) // skip submodel 0 of qcvm->worldmodel, which is the
                           // actual world
        {
            // start with the hull0 bounds
            VectorCopy(mod->maxs, mod->clipmaxs);
            VectorCopy(mod->mins, mod->clipmins);

            // process hull1 (we don't need to process hull2 becuase there's
            // no such thing as a brush that appears in hull2 but not hull1)
            // Mod_BoundsFromClipNode (mod, 1, mod->hulls[1].firstclipnode); //
            // (disabled for now becuase it fucks up on rotating models)
        }
        // johnfitz

        mod->numleafs = bm->visleafs;

        if(i < mod->numsubmodels - 1)
        { // duplicate the basic information
            char name[10];

            sprintf(name, "*%i", i + 1);
            loadmodel = Mod_FindName(name);
            *loadmodel = *mod;
            strcpy(loadmodel->name, name);
            mod = loadmodel;

            Mod_SetExtraFlags(mod);
        }
    }
}

/*
==============================================================================

ALIAS MODELS

==============================================================================
*/

aliashdr_t* pheader;

stvert_t stverts[MAXALIASVERTS];
mtriangle_t* triangles;
int max_triangles;

// a pose is a single set of vertexes.  a frame may be
// an animating sequence of poses
trivertx_t* poseverts_mdl[MAXALIASFRAMES];
int posenum;

byte** player_8bit_texels_tbl;
byte* player_8bit_texels;

/*
=================
Mod_LoadAliasFrame
=================
*/
static void* Mod_LoadAliasFrame(void* pin, maliasframedesc_t* frame, int pvtype)
{
    trivertx_t* pinframe;
    int i;
    daliasframe_t* pdaliasframe;

    if(posenum >= MAXALIASFRAMES)
    {
        Sys_Error("posenum >= MAXALIASFRAMES");
    }

    pdaliasframe = (daliasframe_t*)pin;

    strcpy(frame->name, pdaliasframe->name);
    frame->firstpose = posenum;
    frame->numposes = 1;

    for(i = 0; i < 3; i++)
    {
        // these are byte values, so we don't have to worry about
        // endianness
        frame->bboxmin.v[i] = pdaliasframe->bboxmin.v[i];
        frame->bboxmax.v[i] = pdaliasframe->bboxmax.v[i];
    }


    pinframe = (trivertx_t*)(pdaliasframe + 1);

    poseverts_mdl[posenum] = pinframe;
    posenum++;

    pinframe +=
        pheader->numverts * (pvtype == aliashdr_t::PV_QUAKEFORGE ? 2 : 1);

    return (void*)pinframe;
}


/*
=================
Mod_LoadAliasGroup
=================
*/
static void* Mod_LoadAliasGroup(void* pin, maliasframedesc_t* frame, int pvtype)
{
    daliasgroup_t* pingroup;
    int i, numframes;
    daliasinterval_t* pin_intervals;
    void* ptemp;

    pingroup = (daliasgroup_t*)pin;

    numframes = LittleLong(pingroup->numframes);

    frame->firstpose = posenum;
    frame->numposes = numframes;

    for(i = 0; i < 3; i++)
    {
        // these are byte values, so we don't have to worry about endianness
        frame->bboxmin.v[i] = pingroup->bboxmin.v[i];
        frame->bboxmax.v[i] = pingroup->bboxmax.v[i];
    }


    pin_intervals = (daliasinterval_t*)(pingroup + 1);

    frame->interval = LittleFloat(pin_intervals->interval);

    pin_intervals += numframes;

    ptemp = (void*)pin_intervals;

    for(i = 0; i < numframes; i++)
    {
        if(posenum >= MAXALIASFRAMES)
        {
            Sys_Error("posenum >= MAXALIASFRAMES");
        }

        poseverts_mdl[posenum] = (trivertx_t*)((daliasframe_t*)ptemp + 1);
        posenum++;

        ptemp =
            (trivertx_t*)((daliasframe_t*)ptemp + 1) +
            pheader->numverts * (pvtype == aliashdr_t::PV_QUAKEFORGE ? 2 : 1);
    }

    return ptemp;
}

//=========================================================


/*
=================
Mod_FloodFillSkin

Fill background pixels so mipmapping doesn't have haloes - Ed
=================
*/

typedef struct
{
    short x, y;
} floodfill_t;

// must be a power of 2
#define FLOODFILL_FIFO_SIZE 0x1000
#define FLOODFILL_FIFO_MASK (FLOODFILL_FIFO_SIZE - 1)

#define FLOODFILL_STEP(off, dx, dy)                           \
    do                                                        \
    {                                                         \
        if(pos[off] == fillcolor)                             \
        {                                                     \
            pos[off] = 255;                                   \
            fifo[inpt].x = x + (dx), fifo[inpt].y = y + (dy); \
            inpt = (inpt + 1) & FLOODFILL_FIFO_MASK;          \
        }                                                     \
        else if(pos[off] != 255)                              \
            fdc = pos[off];                                   \
    } while(0)

void Mod_FloodFillSkin(byte* skin, int skinwidth, int skinheight)
{
    byte fillcolor = *skin; // assume this is the pixel to fill
    floodfill_t fifo[FLOODFILL_FIFO_SIZE];
    int inpt = 0, outpt = 0;
    int filledcolor = -1;
    int i;

    if(filledcolor == -1)
    {
        filledcolor = 0;
        // attempt to find opaque black
        for(i = 0; i < 256; ++i)
        {
            if(d_8to24table[i] ==
                (unsigned int)LittleLong(255 << 24)) // alpha 1.0
            {
                filledcolor = i;
                break;
            }
        }
    }

    // can't fill to filled color or to transparent color (used as visited
    // marker)
    if((fillcolor == filledcolor) || (fillcolor == 255))
    {
        // printf( "not filling skin from %d to %d\n", fillcolor, filledcolor );
        return;
    }

    fifo[inpt].x = 0, fifo[inpt].y = 0;
    inpt = (inpt + 1) & FLOODFILL_FIFO_MASK;

    while(outpt != inpt)
    {
        int x = fifo[outpt].x, y = fifo[outpt].y;
        int fdc = filledcolor;
        byte* pos = &skin[x + skinwidth * y];

        outpt = (outpt + 1) & FLOODFILL_FIFO_MASK;

        if(x > 0)
        {
            FLOODFILL_STEP(-1, -1, 0);
        }
        if(x < skinwidth - 1)
        {
            FLOODFILL_STEP(1, 1, 0);
        }
        if(y > 0)
        {
            FLOODFILL_STEP(-skinwidth, 0, -1);
        }
        if(y < skinheight - 1)
        {
            FLOODFILL_STEP(skinwidth, 0, 1);
        }
        skin[x + skinwidth * y] = fdc;
    }
}

/*
===============
Mod_LoadAllSkins
===============
*/
void* Mod_LoadAllSkins(int numskins, daliasskintype_t* pskintype)
{
    int i, j, k, size, groupskins;
    char name[MAX_QPATH];
    byte *skin, *texels;
    daliasskingroup_t* pinskingroup;
    daliasskininterval_t* pinskinintervals;
    char fbr_mask_name[MAX_QPATH]; // johnfitz -- added for fullbright support
    src_offset_t offset;           // johnfitz
    unsigned int texflags = TEXPREF_PAD;

    skin = (byte*)(pskintype + 1);

    if(numskins < 1 || numskins > MAX_SKINS)
    {
        Sys_Error("Mod_LoadAliasModel: Invalid # of skins: %d\n", numskins);
    }

    size = pheader->skinwidth * pheader->skinheight;

    if(loadmodel->flags & MF_HOLEY)
    {
        texflags |= TEXPREF_ALPHA;
    }

    for(i = 0; i < numskins; i++)
    {
        if(pskintype->type == ALIAS_SKIN_SINGLE)
        {
            Mod_FloodFillSkin(skin, pheader->skinwidth, pheader->skinheight);

            // save 8 bit texels for the player model to remap
            texels = (byte*)Hunk_AllocName(size, loadname);
            pheader->texels[i] = texels - (byte*)pheader;
            memcpy(texels, (byte*)(pskintype + 1), size);

            // spike - external model textures with dp naming -- eg
            // progs/foo.mdl_0.tga always use the alpha channel for external
            // images. gpus prefer aligned data anyway.
            int mark = Hunk_LowMark();
            char filename[MAX_QPATH];
            char filename2[MAX_QPATH];
            byte* data;
            int fwidth = 0, fheight = 0;
            bool malloced = false;
            srcformat fmt = SRC_RGBA;
            q_snprintf(filename, sizeof(filename), "%s_%i", loadmodel->name, i);
            data = !gl_load24bit.value ? nullptr
                                       : Image_LoadImage(filename, &fwidth,
                                             &fheight, &fmt, &malloced);
            // now load whatever we found
            if(data) // load external image
            {
                pheader->gltextures[i][0] = TexMgr_LoadImage(loadmodel,
                    filename, fwidth, fheight, fmt, data, filename, 0,
                    TEXPREF_ALPHA | texflags | TEXPREF_MIPMAP);

                // now try to load glow/luma image from the same place
                if(malloced)
                {
                    free(data);
                }
                Hunk_FreeToLowMark(mark);
                q_snprintf(filename2, sizeof(filename2), "%s_glow", filename);
                data = !gl_load24bit.value ? nullptr
                                           : Image_LoadImage(filename2, &fwidth,
                                                 &fheight, &fmt, &malloced);
                if(!data)
                {
                    q_snprintf(
                        filename2, sizeof(filename2), "%s_luma", filename);
                    data = !gl_load24bit.value
                               ? nullptr
                               : Image_LoadImage(filename2, &fwidth, &fheight,
                                     &fmt, &malloced);
                }

                if(data)
                {
                    pheader->fbtextures[i][0] = TexMgr_LoadImage(loadmodel,
                        filename2, fwidth, fheight, fmt, data, filename, 0,
                        TEXPREF_ALPHA | texflags | TEXPREF_MIPMAP);
                }
                else
                {
                    pheader->fbtextures[i][0] = nullptr;
                }
            }
            else
            {
                // johnfitz -- rewritten
                q_snprintf(
                    name, sizeof(name), "%s:frame%i", loadmodel->name, i);
                offset = (src_offset_t)(pskintype + 1) - (src_offset_t)mod_base;
                if(Mod_CheckFullbrights((byte*)(pskintype + 1), size))
                {
                    pheader->gltextures[i][0] = TexMgr_LoadImage(loadmodel,
                        name, pheader->skinwidth, pheader->skinheight,
                        SRC_INDEXED, (byte*)(pskintype + 1), loadmodel->name,
                        offset, texflags | TEXPREF_NOBRIGHT);
                    q_snprintf(fbr_mask_name, sizeof(fbr_mask_name),
                        "%s:frame%i_glow", loadmodel->name, i);
                    pheader->fbtextures[i][0] = TexMgr_LoadImage(loadmodel,
                        fbr_mask_name, pheader->skinwidth, pheader->skinheight,
                        SRC_INDEXED, (byte*)(pskintype + 1), loadmodel->name,
                        offset, texflags | TEXPREF_FULLBRIGHT);
                }
                else
                {
                    pheader->gltextures[i][0] = TexMgr_LoadImage(loadmodel,
                        name, pheader->skinwidth, pheader->skinheight,
                        SRC_INDEXED, (byte*)(pskintype + 1), loadmodel->name,
                        offset, texflags);
                    pheader->fbtextures[i][0] = nullptr;
                }
            }

            pheader->gltextures[i][3] = pheader->gltextures[i][2] =
                pheader->gltextures[i][1] = pheader->gltextures[i][0];
            pheader->fbtextures[i][3] = pheader->fbtextures[i][2] =
                pheader->fbtextures[i][1] = pheader->fbtextures[i][0];
            // johnfitz

            pskintype = (daliasskintype_t*)((byte*)(pskintype + 1) + size);
        }
        else
        {
            // animating skin group.  yuck.
            pskintype++;
            pinskingroup = (daliasskingroup_t*)pskintype;
            groupskins = LittleLong(pinskingroup->numskins);
            pinskinintervals = (daliasskininterval_t*)(pinskingroup + 1);

            pskintype = (daliasskintype_t*)(pinskinintervals + groupskins);

            for(j = 0; j < groupskins; j++)
            {
                Mod_FloodFillSkin(
                    skin, pheader->skinwidth, pheader->skinheight);
                if(j == 0)
                {
                    texels = (byte*)Hunk_AllocName(size, loadname);
                    pheader->texels[i] = texels - (byte*)pheader;
                    memcpy(texels, (byte*)(pskintype), size);
                }

                // johnfitz -- rewritten
                q_snprintf(
                    name, sizeof(name), "%s:frame%i_%i", loadmodel->name, i, j);
                offset = (src_offset_t)(pskintype) -
                         (src_offset_t)mod_base; // johnfitz
                if(Mod_CheckFullbrights((byte*)(pskintype), size))
                {
                    pheader->gltextures[i][j & 3] = TexMgr_LoadImage(loadmodel,
                        name, pheader->skinwidth, pheader->skinheight,
                        SRC_INDEXED, (byte*)(pskintype), loadmodel->name,
                        offset, texflags | TEXPREF_NOBRIGHT);
                    q_snprintf(fbr_mask_name, sizeof(fbr_mask_name),
                        "%s:frame%i_%i_glow", loadmodel->name, i, j);
                    pheader->fbtextures[i][j & 3] = TexMgr_LoadImage(loadmodel,
                        fbr_mask_name, pheader->skinwidth, pheader->skinheight,
                        SRC_INDEXED, (byte*)(pskintype), loadmodel->name,
                        offset, texflags | TEXPREF_FULLBRIGHT);
                }
                else
                {
                    pheader->gltextures[i][j & 3] = TexMgr_LoadImage(loadmodel,
                        name, pheader->skinwidth, pheader->skinheight,
                        SRC_INDEXED, (byte*)(pskintype), loadmodel->name,
                        offset, texflags);
                    pheader->fbtextures[i][j & 3] = nullptr;
                }
                // johnfitz

                pskintype = (daliasskintype_t*)((byte*)(pskintype) + size);
            }
            k = j;
            for(/**/; j < 4; j++)
            {
                pheader->gltextures[i][j & 3] = pheader->gltextures[i][j - k];
            }
        }
    }

    return (void*)pskintype;
}

//=========================================================================

/*
=================
Mod_CalcAliasBounds -- johnfitz -- calculate bounds of alias model for
nonrotated, yawrotated, and fullrotated cases
=================
*/
void Mod_CalcAliasBounds(aliashdr_t* a)
{
    int i, j, k;
    float dist, yawradius, radius;
    vec3_t v;

    // clear out all data
    for(i = 0; i < 3; i++)
    {
        loadmodel->mins[i] = loadmodel->ymins[i] = loadmodel->rmins[i] =
            FLT_MAX;
        loadmodel->maxs[i] = loadmodel->ymaxs[i] = loadmodel->rmaxs[i] =
            -FLT_MAX;
    }
    radius = yawradius = 0;

    for(;;)
    {
        if(a->nummorphposes && a->numverts)
        {
            switch(a->poseverttype)
            {
                case aliashdr_t::PV_QUAKE1:
                    // process verts
                    for(i = 0; i < a->nummorphposes; i++)
                    {
                        for(j = 0; j < a->numverts; j++)
                        {
                            for(k = 0; k < 3; k++)
                            {
                                v[k] = poseverts_mdl[i][j].v[k] *
                                           pheader->scale[k] +
                                       pheader->scale_origin[k];
                            }

                            for(k = 0; k < 3; k++)
                            {
                                loadmodel->mins[k] =
                                    q_min(loadmodel->mins[k], v[k]);
                                loadmodel->maxs[k] =
                                    q_max(loadmodel->maxs[k], v[k]);
                            }
                            dist = v[0] * v[0] + v[1] * v[1];
                            if(yawradius < dist)
                            {
                                yawradius = dist;
                            }
                            dist += v[2] * v[2];
                            if(radius < dist)
                            {
                                radius = dist;
                            }
                        }
                    }
                    break;
                case aliashdr_t::PV_QUAKEFORGE:
                    // process verts
                    for(i = 0; i < a->nummorphposes; i++)
                    {
                        for(j = 0; j < a->numverts; j++)
                        {
                            for(k = 0; k < 3; k++)
                            {
                                v[k] = (poseverts_mdl[i][j].v[k] *
                                           pheader->scale[k]) +
                                       (poseverts_mdl[i][j + a->numverts].v[k] *
                                           pheader->scale[k] / 256.f) +
                                       (pheader->scale_origin[k]);
                            }

                            for(k = 0; k < 3; k++)
                            {
                                loadmodel->mins[k] =
                                    q_min(loadmodel->mins[k], v[k]);
                                loadmodel->maxs[k] =
                                    q_max(loadmodel->maxs[k], v[k]);
                            }
                            dist = v[0] * v[0] + v[1] * v[1];
                            if(yawradius < dist)
                            {
                                yawradius = dist;
                            }
                            dist += v[2] * v[2];
                            if(radius < dist)
                            {
                                radius = dist;
                            }
                        }
                    }
                    break;
                case aliashdr_t::PV_QUAKE3:
                    // process verts
                    for(i = 0; i < a->nummorphposes; i++)
                    {
                        md3XyzNormal_t* pv =
                            (md3XyzNormal_t*)((byte*)a + a->vertexes) +
                            i * a->numverts;
                        for(j = 0; j < a->numverts; j++)
                        {
                            for(k = 0; k < 3; k++)
                            {
                                v[k] = pv[j].xyz[k] * 1 / 64.0;
                            }

                            for(k = 0; k < 3; k++)
                            {
                                loadmodel->mins[k] =
                                    q_min(loadmodel->mins[k], v[k]);
                                loadmodel->maxs[k] =
                                    q_max(loadmodel->maxs[k], v[k]);
                            }
                            dist = v[0] * v[0] + v[1] * v[1];
                            if(yawradius < dist)
                            {
                                yawradius = dist;
                            }
                            dist += v[2] * v[2];
                            if(radius < dist)
                            {
                                radius = dist;
                            }
                        }
                    }
                    break;
                case aliashdr_t::PV_IQM:
                    // process verts
                    for(i = 0; i < a->nummorphposes; i++)
                    {
                        const iqmvert_t* pv =
                            (const iqmvert_t*)((byte*)a + a->vertexes) +
                            i * a->numverts;
                        for(j = 0; j < a->numverts; j++)
                        {
                            for(k = 0; k < 3; k++)
                            {
                                v[k] = pv[j].xyz[k];
                            }

                            for(k = 0; k < 3; k++)
                            {
                                loadmodel->mins[k] =
                                    q_min(loadmodel->mins[k], v[k]);
                                loadmodel->maxs[k] =
                                    q_max(loadmodel->maxs[k], v[k]);
                            }
                            dist = v[0] * v[0] + v[1] * v[1];
                            if(yawradius < dist)
                            {
                                yawradius = dist;
                            }
                            dist += v[2] * v[2];
                            if(radius < dist)
                            {
                                radius = dist;
                            }
                        }
                    }
                    break;
            }
        }

        if(!a->nextsurface)
        {
            break;
        }
        a = (aliashdr_t*)((byte*)a + a->nextsurface);
    }

    // dodgy model that lacks any frames or verts
    for(i = 0; i < 3; i++)
    {
        if(loadmodel->mins[i] > loadmodel->maxs[i])
        { // set sizes to 0 if its invalid.
            loadmodel->mins[i] = 0;
            loadmodel->maxs[i] = 0;
        }
    }

    // rbounds will be used when entity has nonzero pitch or roll
    radius = sqrt(radius);
    loadmodel->rmins[0] = loadmodel->rmins[1] = loadmodel->rmins[2] = -radius;
    loadmodel->rmaxs[0] = loadmodel->rmaxs[1] = loadmodel->rmaxs[2] = radius;

    // ybounds will be used when entity has nonzero yaw
    yawradius = sqrt(yawradius);
    loadmodel->ymins[0] = loadmodel->ymins[1] = -yawradius;
    loadmodel->ymaxs[0] = loadmodel->ymaxs[1] = yawradius;
    loadmodel->ymins[2] = loadmodel->mins[2];
    loadmodel->ymaxs[2] = loadmodel->maxs[2];
}

static bool nameInList(const char* list, const char* name)
{
    const char* s;
    char tmp[MAX_QPATH];
    int i;

    s = list;

    while(*s)
    {
        // make a copy until the next comma or end of string
        i = 0;
        while(*s && *s != ',')
        {
            if(i < MAX_QPATH - 1)
            {
                tmp[i++] = *s;
            }
            s++;
        }
        tmp[i] = '\0';
        // compare it to the model name
        if(!strcmp(name, tmp))
        {
            return true;
        }
        // search forwards to the next comma or end of string
        while(*s && *s == ',')
        {
            s++;
        }
    }
    return false;
}

/*
=================
Mod_SetExtraFlags -- johnfitz -- set up extra flags that aren't in the mdl
=================
*/
void Mod_SetExtraFlags(qmodel_t* mod)
{
    extern cvar_t r_nolerp_list, r_noshadow_list;

    if(!mod)
    {
        return;
    }

    mod->flags &= (0xFF | MF_HOLEY); // only preserve first byte, plus MF_HOLEY

    if(mod->type == mod_alias)
    {
        // nolerp flag
        if(nameInList(r_nolerp_list.string, mod->name))
        {
            mod->flags |= MOD_NOLERP;
        }

        // noshadow flag
        if(nameInList(r_noshadow_list.string, mod->name))
        {
            mod->flags |= MOD_NOSHADOW;
        }

        // fullbright hack (TODO: make this a cvar list)
        if(!strcmp(mod->name, "progs/flame2.mdl") ||
            !strcmp(mod->name, "progs/flame.mdl") ||
            !strcmp(mod->name, "progs/boss.mdl"))
        {
            mod->flags |= MOD_FBRIGHTHACK;
        }
    }

#ifdef PSET_SCRIPT
    PScript_UpdateModelEffects(mod);
#endif
}

/*
=================
Mod_LoadAliasModel
=================
*/
void Mod_LoadAliasModel(qmodel_t* mod, void* buffer, int pvtype)
{
    int i, j;
    mdl_t* pinmodel;
    stvert_t* pinstverts;
    dtriangle_t* pintriangles;
    int version, numframes;
    int size;
    daliasframetype_t* pframetype;
    daliasskintype_t* pskintype;
    int start, end, total;

    start = Hunk_LowMark();

    pinmodel = (mdl_t*)buffer;
    mod_base = (byte*)buffer; // johnfitz

    version = LittleLong(pinmodel->version);
    if(version != ALIAS_VERSION)
    {
        Sys_Error("%s has wrong version number (%i should be %i)", mod->name,
            version, ALIAS_VERSION);
    }

    //
    // allocate space for a working header, plus all the data except the frames,
    // skin and group info
    //
    size = sizeof(aliashdr_t) +
           (LittleLong(pinmodel->numframes) - 1) * sizeof(pheader->frames[0]);
    pheader = (aliashdr_t*)Hunk_AllocName(size, loadname);

    mod->flags = LittleLong(pinmodel->flags);

    //
    // endian-adjust and copy the data, starting with the alias model header
    //
    pheader->boundingradius = LittleFloat(pinmodel->boundingradius);
    pheader->numskins = LittleLong(pinmodel->numskins);
    pheader->skinwidth = LittleLong(pinmodel->skinwidth);
    pheader->skinheight = LittleLong(pinmodel->skinheight);

    if(pheader->skinheight > MAX_LBM_HEIGHT)
    {
        Con_DWarning("model %s has a skin taller than %d\n", mod->name,
            MAX_LBM_HEIGHT); // Spike -- this was always a bogus error in gl
    }
    // renderers. its width*height that really matters.

    pheader->numverts = LittleLong(pinmodel->numverts);

    if(pheader->numverts <= 0)
    {
        Sys_Error("model %s has no vertices", mod->name);
    }
    if(pheader->numverts > MAXALIASVERTS)
    { // Spike -- made this more tollerant. its still an error of course.
        Con_Warning("model %s has too many vertices (%i > %i)\n", mod->name,
            pheader->numverts, MAXALIASVERTS);
        mod->type = mod_ext_invalid;
        return;
    }
    if(pheader->numverts > VANILLA_MAXALIASVERTS)
    {
        Con_DWarning("model %s exceeds standard vertex limit (%i > %i)\n",
            mod->name, pheader->numverts, VANILLA_MAXALIASVERTS);
    }

    pheader->numtris = LittleLong(pinmodel->numtris);

    if(pheader->numtris <= 0)
    {
        Sys_Error("model %s has no triangles", mod->name);
    }
    if(pheader->numtris > max_triangles)
    {
        auto* n = (mtriangle_t*)malloc(sizeof(*triangles) * pheader->numtris);
        if(n)
        {
            free(triangles);
            triangles = n;
            max_triangles = pheader->numtris;
        }
        else
        {
            max_triangles = 0;
            // Spike -- added this check, because I'm segfaulting out.
            Con_Warning("model %s has too many triangles (%i)\n", mod->name,
                pheader->numtris);
            mod->type = mod_ext_invalid;
            return;
        }
    }

    pheader->numframes = LittleLong(pinmodel->numframes);
    numframes = pheader->numframes;
    if(numframes < 1)
    {
        Sys_Error("Mod_LoadAliasModel: Invalid # of frames: %d\n", numframes);
    }
    if(numframes > MAXALIASFRAMES)
    {
        numframes = MAXALIASFRAMES;
        Con_Warning("model %s has too many frames (%i > %i)\n", mod->name,
            numframes, MAXALIASFRAMES);
    }

    pheader->size = LittleFloat(pinmodel->size) * ALIAS_BASE_SIZE_RATIO;
    mod->synctype = (synctype_t)LittleLong(pinmodel->synctype);
    mod->numframes = pheader->numframes;

    for(i = 0; i < 3; i++)
    {
        pheader->scale[i] = LittleFloat(pinmodel->scale[i]);
        pheader->scale_origin[i] = LittleFloat(pinmodel->scale_origin[i]);
        pheader->eyeposition[i] = LittleFloat(pinmodel->eyeposition[i]);
    }

    pheader->original_scale = pheader->scale;
    pheader->original_scale_origin = pheader->scale_origin;

    //
    // load the skins
    //
    pskintype = (daliasskintype_t*)&pinmodel[1];
    pskintype =
        (daliasskintype_t*)Mod_LoadAllSkins(pheader->numskins, pskintype);

    //
    // load base s and t vertices
    //
    pinstverts = (stvert_t*)pskintype;

    for(i = 0; i < pheader->numverts; i++)
    {
        stverts[i].onseam = LittleLong(pinstverts[i].onseam);
        stverts[i].s = LittleLong(pinstverts[i].s);
        stverts[i].t = LittleLong(pinstverts[i].t);
    }

    //
    // load triangle lists
    //
    pintriangles = (dtriangle_t*)&pinstverts[pheader->numverts];

    for(i = 0; i < pheader->numtris; i++)
    {
        triangles[i].facesfront = LittleLong(pintriangles[i].facesfront);

        for(j = 0; j < 3; j++)
        {
            triangles[i].vertindex[j] =
                LittleLong(pintriangles[i].vertindex[j]);
        }
    }

    //
    // load the frames
    //
    posenum = 0;
    pframetype = (daliasframetype_t*)&pintriangles[pheader->numtris];

    for(i = 0; i < numframes; i++)
    {
        aliasframetype_t frametype;
        frametype = (aliasframetype_t)LittleLong(pframetype->type);
        if(frametype == ALIAS_SINGLE)
        {
            pframetype = (daliasframetype_t*)Mod_LoadAliasFrame(
                pframetype + 1, &pheader->frames[i], pvtype);
        }
        else
        {
            pframetype = (daliasframetype_t*)Mod_LoadAliasGroup(
                pframetype + 1, &pheader->frames[i], pvtype);
        }
    }

    pheader->nummorphposes = posenum;
    pheader->poseverttype = aliashdr_t::PoseVertType(
        pvtype); // it would be safe to always store PV_QUAKE1 here if you
                 // wanted to drop the low-order data.

    mod->type = mod_alias;

    Mod_CalcAliasBounds(pheader); // johnfitz

    // Spike: for setmodel compat with vanilla
    mod->clipmins[0] = mod->clipmins[1] = mod->clipmins[2] = -16;
    mod->clipmaxs[0] = mod->clipmaxs[1] = mod->clipmaxs[2] = 16;

    //
    // build the draw lists
    //
    GL_MakeAliasModelDisplayLists(mod, pheader);
    GLMesh_LoadVertexBuffer(mod, pheader);

    //
    // move the complete, relocatable alias model to the cache
    //
    end = Hunk_LowMark();
    total = end - start;

    Cache_Alloc(&mod->cache, total, loadname);
    if(!mod->cache.data)
    {
        return;
    }
    memcpy(mod->cache.data, pheader, total);

    Hunk_FreeToLowMark(start);
}

//=============================================================================

/*
=================
Mod_LoadSpriteFrame
=================
*/
void* Mod_LoadSpriteFrame(
    void* pin, mspriteframe_t** ppframe, int framenum, srcformat fmt)
{
    dspriteframe_t* pinframe;
    mspriteframe_t* pspriteframe;
    int width, height, size, origin[2];
    char name[64];
    src_offset_t offset; // johnfitz

    pinframe = (dspriteframe_t*)pin;

    width = LittleLong(pinframe->width);
    height = LittleLong(pinframe->height);
    size = width * height;
    if(fmt == SRC_RGBA)
    {
        size *= 4;
    }

    pspriteframe =
        (mspriteframe_t*)Hunk_AllocName(sizeof(mspriteframe_t), loadname);
    *ppframe = pspriteframe;

    pspriteframe->width = width;
    pspriteframe->height = height;
    origin[0] = LittleLong(pinframe->origin[0]);
    origin[1] = LittleLong(pinframe->origin[1]);

    pspriteframe->up = origin[1];
    pspriteframe->down = origin[1] - height;
    pspriteframe->left = origin[0];
    pspriteframe->right = width + origin[0];

    // johnfitz -- image might be padded
    pspriteframe->smax = (float)width / (float)TexMgr_PadConditional(width);
    pspriteframe->tmax = (float)height / (float)TexMgr_PadConditional(height);
    // johnfitz

    q_snprintf(name, sizeof(name), "%s:frame%i", loadmodel->name, framenum);
    offset = (src_offset_t)(pinframe + 1) - (src_offset_t)mod_base; // johnfitz
    pspriteframe->gltexture = TexMgr_LoadImage(loadmodel, name, width, height,
        fmt, (byte*)(pinframe + 1), loadmodel->name, offset,
        TEXPREF_PAD | TEXPREF_ALPHA | TEXPREF_NOPICMIP); // johnfitz -- TexMgr

    return (void*)((byte*)pinframe + sizeof(dspriteframe_t) + size);
}


/*
=================
Mod_LoadSpriteGroup
=================
*/
void* Mod_LoadSpriteGroup(
    void* pin, mspriteframe_t** ppframe, int framenum, srcformat fmt)
{
    dspritegroup_t* pingroup;
    mspritegroup_t* pspritegroup;
    int i, numframes;
    dspriteinterval_t* pin_intervals;
    float* poutintervals;
    void* ptemp;
    float prevtime;

    pingroup = (dspritegroup_t*)pin;

    numframes = LittleLong(pingroup->numframes);

    pspritegroup = (mspritegroup_t*)Hunk_AllocName(
        sizeof(mspritegroup_t) +
            (numframes - 1) * sizeof(pspritegroup->frames[0]),
        loadname);

    pspritegroup->numframes = numframes;

    *ppframe = (mspriteframe_t*)pspritegroup;

    pin_intervals = (dspriteinterval_t*)(pingroup + 1);

    poutintervals = (float*)Hunk_AllocName(numframes * sizeof(float), loadname);

    pspritegroup->intervals = poutintervals;

    for(i = 0, prevtime = 0; i < numframes; i++)
    {
        *poutintervals = LittleFloat(pin_intervals->interval);
        if(*poutintervals <= 0.0)
        {
            Sys_Error("Mod_LoadSpriteGroup: interval<=0");
        }
        // Spike -- we need to accumulate the previous time too, so we get
        // actual timestamps, otherwise spritegroups won't animate (vanilla
        // bug).
        prevtime = *poutintervals = prevtime + *poutintervals;

        poutintervals++;
        pin_intervals++;
    }

    ptemp = (void*)pin_intervals;

    for(i = 0; i < numframes; i++)
    {
        ptemp = Mod_LoadSpriteFrame(
            ptemp, &pspritegroup->frames[i], framenum * 100 + i, fmt);
    }

    return ptemp;
}


/*
=================
Mod_LoadSpriteModel
=================
*/
void Mod_LoadSpriteModel(qmodel_t* mod, void* buffer)
{
    int i;
    int version;
    dsprite_t* pin;
    msprite_t* psprite;
    int numframes;
    int size;
    dspriteframetype_t* pframetype;
    srcformat fmt = SRC_INDEXED;

    pin = (dsprite_t*)buffer;
    mod_base = (byte*)buffer; // johnfitz

    version = LittleLong(pin->version);
    if(version == 32)
    {
        fmt =
            SRC_RGBA; // Spike -- spr32 is identical to regular sprites, but
                      // uses rgba instead of indexed values. should probably
                      // also blend these sprites instead of alphatest, but meh.
    }
    else if(version != SPRITE_VERSION)
    {
        // Spike -- made this more tolerant. its still an error, it just won't
        // crash us out
        Con_Printf(
            "%s has wrong version number "
            "(%i should be %i)\n",
            mod->name, version, SPRITE_VERSION);
        mod->type = mod_ext_invalid;
        return;
    }

    numframes = LittleLong(pin->numframes);

    size = sizeof(msprite_t) + (numframes - 1) * sizeof(psprite->frames);

    psprite = (msprite_t*)Hunk_AllocName(size, loadname);

    mod->cache.data = psprite;

    psprite->type = LittleLong(pin->type);
    psprite->maxwidth = LittleLong(pin->width);
    psprite->maxheight = LittleLong(pin->height);
    psprite->beamlength = LittleFloat(pin->beamlength);
    mod->synctype = (synctype_t)LittleLong(pin->synctype);
    psprite->numframes = numframes;

    mod->mins[0] = mod->mins[1] = -psprite->maxwidth / 2;
    mod->maxs[0] = mod->maxs[1] = psprite->maxwidth / 2;
    mod->mins[2] = -psprite->maxheight / 2;
    mod->maxs[2] = psprite->maxheight / 2;
    VectorCopy(mod->mins, mod->clipmins);
    VectorCopy(mod->maxs, mod->clipmaxs);

    //
    // load the frames
    //
    if(numframes < 1)
    {
        Sys_Error("Mod_LoadSpriteModel: Invalid # of frames: %d\n", numframes);
    }

    mod->numframes = numframes;

    pframetype = (dspriteframetype_t*)(pin + 1);

    for(i = 0; i < numframes; i++)
    {
        spriteframetype_t frametype;

        frametype = (spriteframetype_t)LittleLong(pframetype->type);
        psprite->frames[i].type = frametype;

        if(frametype == SPR_SINGLE)
        {
            pframetype = (dspriteframetype_t*)Mod_LoadSpriteFrame(
                pframetype + 1, &psprite->frames[i].frameptr, i, fmt);
        }
        else
        {
            pframetype = (dspriteframetype_t*)Mod_LoadSpriteGroup(
                pframetype + 1, &psprite->frames[i].frameptr, i, fmt);
        }
    }

    mod->type = mod_sprite;
}

//=============================================================================

/*
================
Mod_Print
================
*/
void Mod_Print()
{
    int i;
    qmodel_t* mod;

    Con_SafePrintf("Cached models:\n"); // johnfitz -- safeprint instead of
                                        // print
    for(i = 0, mod = mod_known; i < mod_numknown; i++, mod++)
    {
        Con_SafePrintf("%8p : %s\n", mod->cache.data,
            mod->name); // johnfitz -- safeprint instead of print
    }
    Con_Printf("%i models\n", mod_numknown); // johnfitz -- print the total too
}
