/*
Copyright (C) 1996-2001 Id Software, Inc.
Copyright (C) 2002-2009 John Fitzgibbons and others
Copyright (C) 2007-2008 Kristian Duske
Copyright (C) 2010-2014 QuakeSpasm developers

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.

*/
// cl_parse.c  -- parse a message received from the server

#include <cstring>

#include "quakedef.hpp"
#include "bgmusic.hpp"

const char *svc_strings[] =
{
    "svc_bad",
    "svc_nop",
    "svc_disconnect",
    "svc_updatestat",
    "svc_version", // [long] server version
    "svc_setview", // [short] entity number
    "svc_sound", // <see code>
    "svc_time", // [float] server time
    "svc_print", // [string] null terminated string
    "svc_stufftext", // [string] stuffed into client's console buffer
    // the string should be \n terminated
    "svc_setangle", // [vec3] set the view angle to this absolute value

    "svc_serverinfo", // [long] version
    // [string] signon string
    // [string]..[0]model cache [string]...[0]sounds cache
    // [string]..[0]item cache
    "svc_lightstyle", // [byte] [string]
    "svc_updatename", // [byte] [string]
    "svc_updatefrags", // [byte] [short]
    "svc_clientdata", // <shortbits + data>
    "svc_stopsound", // <see code>
    "svc_updatecolors", // [byte] [byte]
    "svc_particle", // [vec3] <variable>
    "svc_damage", // [byte] impact [byte] blood [vec3] from

    "svc_spawnstatic",
    "OBSOLETE svc_spawnbinary",
    "svc_spawnbaseline",

    "svc_temp_entity", // <variable>
    "svc_setpause",
    "svc_signonnum",
    "svc_centerprint",
    "svc_killedmonster",
    "svc_foundsecret",
    "svc_spawnstaticsound",
    "svc_intermission",
    "svc_finale", // [string] music [string] text
    "svc_cdtrack", // [byte] track [byte] looptrack
    "svc_sellscreen",
    "svc_cutscene",
    //johnfitz -- new server messages
    "", // 35
    "", // 36
    "svc_skybox", // 37					// [string] skyname
    "svc_botchat", // 38 (2021 RE-RELEASE)
    "", // 39
    "svc_bf", // 40						// no data
    "svc_fog", // 41					// [byte] density [byte] red [byte] green [byte] blue [float] time
    "svc_spawnbaseline2", //42			// support for large modelindex, large framenum, alpha, using flags
    "svc_spawnstatic2", // 43			// support for large modelindex, large framenum, alpha, using flags
    "svc_spawnstaticsound2", //	44		// [coord3] [short] samp [byte] vol [byte] aten
    //johnfitz

    // 2021 RE-RELEASE:
    "svc_setviews", // 45
    "svc_updateping", // 46
    "svc_updatesocial", // 47
    "svc_updateplinfo", // 48
    "svc_rawprint", // 49
    "svc_servervars", // 50
    "svc_seq", // 51
    "svc_achievement", // 52
    "svc_chat", // 53
    "svc_levelcompleted", // 54
    "svc_backtolobby", // 55
    "svc_localsound" // 56
};
#define NUM_SVC_STRINGS std::size(svc_strings)

bool warn_about_nehahra_protocol; //johnfitz

extern vec3_t v_punchangles[2]; //johnfitz

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

/*
===============
CL_EntityNum

This error checks and tracks the total number of entities
===============
*/
entity_t *CL_EntityNum(int num) {
    //johnfitz -- check minimum number too
    if (num < 0)
        Host_Error("CL_EntityNum: %i is an invalid number", num);
    //john

    if (num >= cl.num_entities) {
        if (num >= cl_max_edicts) //johnfitz -- no more MAX_EDICTS
            Host_Error("CL_EntityNum: %i is an invalid number", num);
        while (cl.num_entities <= num) {
            cl_entities[cl.num_entities].colormap = vid.colormap;
            cl_entities[cl.num_entities].lerpflags |= LERP_RESETMOVE | LERP_RESETANIM; //johnfitz
            cl_entities[cl.num_entities].baseline.scale = ENTSCALE_DEFAULT;
            cl.num_entities++;
        }
    }

    return &cl_entities[num];
}


/*
==================
CL_ParseStartSoundPacket
==================
*/
void CL_ParseStartSoundPacket(void) {
    vec3_t pos;
    int channel, ent;
    int sound_num;
    int volume;
    int field_mask;
    float attenuation;
    int i;

    field_mask = net_message.read_byte().value();

    if (field_mask & SND_VOLUME)
        volume = net_message.read_byte().value();
    else
        volume = DEFAULT_SOUND_PACKET_VOLUME;

    if (field_mask & SND_ATTENUATION)
        attenuation = net_message.read_byte().value() / 64.0;
    else
        attenuation = DEFAULT_SOUND_PACKET_ATTENUATION;

    //johnfitz -- PROTOCOL_FITZQUAKE
    if (field_mask & SND_LARGEENTITY) {
        ent = (unsigned short) net_message.read_short().value();
        channel = net_message.read_byte().value();
    } else {
        channel = (unsigned short) net_message.read_short().value();
        ent = channel >> 3;
        channel &= 7;
    }

    if (field_mask & SND_LARGESOUND)
        sound_num = (unsigned short) net_message.read_short().value();
    else
        sound_num = net_message.read_byte().value();
    //johnfitz

    //johnfitz -- check soundnum
    if (sound_num >= MAX_SOUNDS)
        Host_Error("CL_ParseStartSoundPacket: %i > MAX_SOUNDS", sound_num);
    //johnfitz

    if (ent > cl_max_edicts) //johnfitz -- no more MAX_EDICTS
        Host_Error("CL_ParseStartSoundPacket: ent = %i", ent);

    for (i = 0; i < 3; i++)
        pos[i] = net_message.read_coord(static_cast<net::rmq_flags>(cl.protocolflags)).value();

    S_StartSound(ent, channel, cl.sound_precache[sound_num], pos, volume / 255.0, attenuation);
}

/*
==================
CL_ParseLocalSound - for 2021 rerelease
==================
*/
void CL_ParseLocalSound(void) {
    int field_mask, sound_num;

    field_mask = net_message.read_byte().value();
    sound_num = (field_mask & SND_LARGESOUND) ? net_message.read_short().value() : net_message.read_byte().value();
    if (sound_num >= MAX_SOUNDS)
        Host_Error("CL_ParseLocalSound: %i > MAX_SOUNDS", sound_num);

    S_LocalSound(cl.sound_precache[sound_num]->name);
}

/*
==================
CL_KeepaliveMessage

When the client is taking a long time to load stuff, send keepalive messages
so the server doesn't disconnect.
==================
*/
static byte net_olddata[NET_MAXMESSAGE];

void CL_KeepaliveMessage(void) {
    float time;
    static float lastmsg;
    int ret;

    if (sv.active)
        return; // no need if server is local
    if (cls.demoplayback)
        return;

    // read messages from server, should just be nops
    auto old = net_message;

    do {
        ret = CL_GetMessage();
        switch (ret) {
            default:
                Host_Error("CL_KeepaliveMessage: CL_GetMessage failed");
            case 0:
                break; // nothing waiting
            case 1:
                Host_Error("CL_KeepaliveMessage: received a message");
                break;
            case 2:
                if (net_message.read_byte().value() != svc_nop)
                    Host_Error("CL_KeepaliveMessage: datagram wasn't a nop");
                break;
        }
    } while (ret);

    net_message = old;

    // check time
    time = Sys_DoubleTime();
    if (time - lastmsg < 5)
        return;
    lastmsg = time;

    // write out a nop
    console::info("--> client to server keepalive\n");

    cls.message.write_byte(clc_nop);
    NET_SendMessage(cls.netcon, cls.message);
    cls.message.clear();
}

/*
==================
CL_ParseServerInfo
==================
*/
void CL_ParseServerInfo(void) {
    int i;
    int nummodels, numsounds;
    char model_precache[MAX_MODELS][MAX_QPATH];
    char sound_precache[MAX_SOUNDS][MAX_QPATH];

    console::debug("Serverinfo packet received.\n");

    // ericw -- bring up loading plaque for map changes within a demo.
    //          it will be hidden in CL_SignonReply.
    if (cls.demoplayback)
        SCR_BeginLoadingPlaque();

    //
    // wipe the client_state_t struct
    //
    CL_ClearState();

    // parse protocol version number
    i = net_message.read_long().value();
    //johnfitz -- support multiple protocols
    if (i != PROTOCOL_NETQUAKE && i != PROTOCOL_FITZQUAKE && i != PROTOCOL_RMQ) {
        console::info("\n"); //because there's no newline after serverinfo print
        Host_Error("Server returned version %i, not %i or %i or %i", i, PROTOCOL_NETQUAKE, PROTOCOL_FITZQUAKE,
                   PROTOCOL_RMQ);
    }
    cl.protocol = i;
    //johnfitz

    if (cl.protocol == PROTOCOL_RMQ) {
        const unsigned int supportedflags = (
            PRFL_SHORTANGLE | PRFL_FLOATANGLE | PRFL_24BITCOORD | PRFL_FLOATCOORD | PRFL_EDICTSCALE | PRFL_INT32COORD);

        // mh - read protocol flags from server so that we know what protocol features to expect
        cl.protocolflags = (unsigned int) net_message.read_long().value();

        if (0 != (cl.protocolflags & (~supportedflags))) {
            console::warn("PROTOCOL_RMQ protocolflags %i contains unsupported flags\n", cl.protocolflags);
        }
    } else cl.protocolflags = 0;

    // parse maxclients
    cl.maxclients = net_message.read_byte().value();
    if (cl.maxclients < 1 || cl.maxclients > MAX_SCOREBOARD) {
        Host_Error("Bad maxclients (%u) from server", cl.maxclients);
    }
    cl.scores = (scoreboard_t *) Hunk_AllocName(cl.maxclients * sizeof(*cl.scores), "scores");

    // parse gametype
    cl.gametype = net_message.read_byte().value();

    // parse signon message
    auto str = net_message.read_string().value();
    std::strncpy(cl.levelname, str.c_str(), sizeof(cl.levelname));

    // seperate the printfs so the server message can have a color
    console::info("\n%s\n", Con_Quakebar(40)); //johnfitz
    console::info("%c%s\n", 2, str.c_str());

    //johnfitz -- tell user which protocol this is
    console::info("Using protocol %i\n", i);

    // first we go through and touch all of the precache data that still
    // happens to be in the cache, so precaching something else doesn't
    // needlessly purge it

    // precache models
    memset(cl.model_precache, 0, sizeof(cl.model_precache));
    for (nummodels = 1; ; nummodels++) {
        str = net_message.read_string().value();
        if (!str[0])
            break;
        if (nummodels == MAX_MODELS) {
            Host_Error("Server sent too many model precaches");
        }
        std::strncpy(model_precache[nummodels], str.c_str(), MAX_QPATH);
        Mod_TouchModel(str.c_str());
    }

    //johnfitz -- check for excessive models
    if (nummodels >= 256)
        console::dev_warn("%i models exceeds standard limit of 256 (max = %d).\n", nummodels, MAX_MODELS);
    //johnfitz

    // precache sounds
    memset(cl.sound_precache, 0, sizeof(cl.sound_precache));
    for (numsounds = 1; ; numsounds++) {
        str = net_message.read_string().value();
        if (!str[0])
            break;
        if (numsounds == MAX_SOUNDS) {
            Host_Error("Server sent too many sound precaches");
        }
        std::strncpy(sound_precache[numsounds], str.c_str(), MAX_QPATH);
        S_TouchSound(str.c_str());
    }

    //johnfitz -- check for excessive sounds
    if (numsounds >= 256)
        console::dev_warn("%i sounds exceeds standard limit of 256 (max = %d).\n", numsounds, MAX_SOUNDS);
    //johnfitz

    //
    // now we try to load everything else until a cache allocation fails
    //

    // copy the naked name of the map file to the cl structure -- O.S
    COM_StripExtension(COM_SkipPath(model_precache[1]), cl.mapname, sizeof(cl.mapname));

    for (i = 1; i < nummodels; i++) {
        cl.model_precache[i] = Mod_ForName(model_precache[i], false);
        if (cl.model_precache[i] == NULL) {
            Host_Error("Model %s not found", model_precache[i]);
        }
        CL_KeepaliveMessage();
    }

    S_BeginPrecaching();
    for (i = 1; i < numsounds; i++) {
        cl.sound_precache[i] = S_PrecacheSound(sound_precache[i]);
        CL_KeepaliveMessage();
    }
    S_EndPrecaching();

    // local state
    cl_entities[0].model = cl.worldmodel = cl.model_precache[1];

    R_NewMap();

    //johnfitz -- clear out string; we don't consider identical
    //messages to be duplicates if the map has changed in between
    console::last_center_string.clear();
    //johnfitz

    Hunk_Check(); // make sure nothing is hurt

    noclip_anglehack = false; // noclip is turned off at start

    warn_about_nehahra_protocol = true; //johnfitz -- warn about nehahra protocol hack once per server connection

    //johnfitz -- reset developer stats
    memset(&dev_stats, 0, sizeof(dev_stats));
    memset(&dev_peakstats, 0, sizeof(dev_peakstats));
    memset(&dev_overflows, 0, sizeof(dev_overflows));
}

/*
==================
CL_ParseUpdate

Parse an entity update message from the server
If an entities model or origin changes from frame to frame, it must be
relinked.  Other attributes can change without relinking.
==================
*/
void CL_ParseUpdate(int bits) {
    int i;
    qmodel_t *model;
    int modnum;
    bool forcelink;
    entity_t *ent;
    int num;
    int skin;

    if (cls.signon == SIGNONS - 1) {
        // first update is the final signon stage
        cls.signon = SIGNONS;
        CL_SignonReply();
    }

    if (bits & U_MOREBITS) {
        i = net_message.read_byte().value();
        bits |= (i << 8);
    }

    //johnfitz -- PROTOCOL_FITZQUAKE
    if (cl.protocol == PROTOCOL_FITZQUAKE || cl.protocol == PROTOCOL_RMQ) {
        if (bits & U_EXTEND1)
            bits |= net_message.read_byte().value() << 16;
        if (bits & U_EXTEND2)
            bits |= net_message.read_byte().value() << 24;
    }
    //johnfitz

    if (bits & U_LONGENTITY)
        num = net_message.read_short().value();
    else
        num = net_message.read_byte().value();

    ent = CL_EntityNum(num);

    if (ent->msgtime != cl.mtime[1])
        forcelink = true; // no previous frame to lerp from
    else
        forcelink = false;

    //johnfitz -- lerping
    if (ent->msgtime + 0.2 < cl.mtime[0])
        //more than 0.2 seconds since the last message (most entities think every 0.1 sec)
        ent->lerpflags |= LERP_RESETANIM; //if we missed a think, we'd be lerping from the wrong frame
    //johnfitz

    ent->msgtime = cl.mtime[0];

    if (bits & U_MODEL) {
        modnum = net_message.read_byte().value();
        if (modnum >= MAX_MODELS)
            Host_Error("CL_ParseModel: bad modnum");
    } else
        modnum = ent->baseline.modelindex;

    if (bits & U_FRAME)
        ent->frame = net_message.read_byte().value();
    else
        ent->frame = ent->baseline.frame;

    if (bits & U_COLORMAP)
        i = net_message.read_byte().value();
    else
        i = ent->baseline.colormap;
    if (!i)
        ent->colormap = vid.colormap;
    else {
        if (i > cl.maxclients)
            Sys_Error("i >= cl.maxclients");
        ent->colormap = cl.scores[i - 1].translations;
    }
    if (bits & U_SKIN)
        skin = net_message.read_byte().value();
    else
        skin = ent->baseline.skin;
    if (skin != ent->skinnum) {
        ent->skinnum = skin;
        if (num > 0 && num <= cl.maxclients)
            R_TranslateNewPlayerSkin(num - 1); //johnfitz -- was R_TranslatePlayerSkin
    }
    if (bits & U_EFFECTS)
        ent->effects = net_message.read_byte().value();
    else
        ent->effects = ent->baseline.effects;

    // shift the known values for interpolation
    VectorCopy(ent->msg_origins[0], ent->msg_origins[1]);
    VectorCopy(ent->msg_angles[0], ent->msg_angles[1]);

    if (bits & U_ORIGIN1)
        ent->msg_origins[0][0] = net_message.read_coord(static_cast<net::rmq_flags>(cl.protocolflags)).value();
    else
        ent->msg_origins[0][0] = ent->baseline.origin[0];
    if (bits & U_ANGLE1)
        ent->msg_angles[0][0] = net_message.read_angle(static_cast<net::rmq_flags>(cl.protocolflags)).value();
    else
        ent->msg_angles[0][0] = ent->baseline.angles[0];

    if (bits & U_ORIGIN2)
        ent->msg_origins[0][1] = net_message.read_coord(static_cast<net::rmq_flags>(cl.protocolflags)).value();
    else
        ent->msg_origins[0][1] = ent->baseline.origin[1];
    if (bits & U_ANGLE2)
        ent->msg_angles[0][1] = net_message.read_angle(static_cast<net::rmq_flags>(cl.protocolflags)).value();
    else
        ent->msg_angles[0][1] = ent->baseline.angles[1];

    if (bits & U_ORIGIN3)
        ent->msg_origins[0][2] = net_message.read_coord(static_cast<net::rmq_flags>(cl.protocolflags)).value();
    else
        ent->msg_origins[0][2] = ent->baseline.origin[2];
    if (bits & U_ANGLE3)
        ent->msg_angles[0][2] = net_message.read_angle(static_cast<net::rmq_flags>(cl.protocolflags)).value();
    else
        ent->msg_angles[0][2] = ent->baseline.angles[2];

    //johnfitz -- lerping for movetype_step entities
    if (bits & U_STEP) {
        ent->lerpflags |= LERP_MOVESTEP;
        ent->forcelink = true;
    } else
        ent->lerpflags &= ~LERP_MOVESTEP;
    //johnfitz

    //johnfitz -- PROTOCOL_FITZQUAKE and PROTOCOL_NEHAHRA
    if (cl.protocol == PROTOCOL_FITZQUAKE || cl.protocol == PROTOCOL_RMQ) {
        if (bits & U_ALPHA)
            ent->alpha = net_message.read_byte().value();
        else
            ent->alpha = ent->baseline.alpha;
        if (bits & U_SCALE)
            ent->scale = net_message.read_byte().value();
        else
            ent->scale = ent->baseline.scale;
        if (bits & U_FRAME2)
            ent->frame = (ent->frame & 0x00FF) | (net_message.read_byte().value() << 8);
        if (bits & U_MODEL2)
            modnum = (modnum & 0x00FF) | (net_message.read_byte().value() << 8);
        if (bits & U_LERPFINISH) {
            ent->lerpfinish = ent->msgtime + ((float) (net_message.read_byte().value()) / 255);
            ent->lerpflags |= LERP_FINISH;
        } else
            ent->lerpflags &= ~LERP_FINISH;
    } else if (cl.protocol == PROTOCOL_NETQUAKE) {
        //HACK: if this bit is set, assume this is PROTOCOL_NEHAHRA
        if (bits & U_TRANS) {
            float a, b;

            if (warn_about_nehahra_protocol) {
                console::warn("nonstandard update bit, assuming Nehahra protocol\n");
                warn_about_nehahra_protocol = false;
            }

            a = net_message.read_float().value();
            b = net_message.read_float().value(); //alpha
            if (a == 2)
                net_message.read_float().value(); //fullbright (not using this yet)
            ent->alpha = ENTALPHA_ENCODE(b);
        } else
            ent->alpha = ent->baseline.alpha;
        ent->scale = ent->baseline.scale;
    }
    //johnfitz

    //johnfitz -- moved here from above
    model = cl.model_precache[modnum];
    if (model != ent->model) {
        ent->model = model;
        // automatic animation (torches, etc) can be either all together
        // or randomized
        if (model) {
            if (model->synctype == ST_RAND)
                ent->syncbase = (float) (rand() & 0x7fff) / 0x7fff;
            else
                ent->syncbase = 0.0;
        } else
            forcelink = true; // hack to make null model players work
        if (num > 0 && num <= cl.maxclients)
            R_TranslateNewPlayerSkin(num - 1); //johnfitz -- was R_TranslatePlayerSkin

        ent->lerpflags |= LERP_RESETANIM; //johnfitz -- don't lerp animation across model changes
    }
    //johnfitz

    if (forcelink) {
        // didn't have an update last message
        VectorCopy(ent->msg_origins[0], ent->msg_origins[1]);
        VectorCopy(ent->msg_origins[0], ent->origin);
        VectorCopy(ent->msg_angles[0], ent->msg_angles[1]);
        VectorCopy(ent->msg_angles[0], ent->angles);
        ent->forcelink = true;
    }
}

/*
==================
CL_ParseBaseline
==================
*/
void CL_ParseBaseline(entity_t *ent, int version) //johnfitz -- added argument
{
    int i;
    int bits; //johnfitz

    //johnfitz -- PROTOCOL_FITZQUAKE
    bits = (version == 2) ? net_message.read_byte().value() : 0;
    ent->baseline.modelindex = (bits & B_LARGEMODEL) ? net_message.read_short().value() : net_message.read_byte().value();
    ent->baseline.frame = (bits & B_LARGEFRAME) ? net_message.read_short().value() : net_message.read_byte().value();
    //johnfitz

    ent->baseline.colormap = net_message.read_byte().value();
    ent->baseline.skin = net_message.read_byte().value();
    for (i = 0; i < 3; i++) {
        ent->baseline.origin[i] = net_message.read_coord(static_cast<net::rmq_flags>(cl.protocolflags)).value();
        ent->baseline.angles[i] = net_message.read_angle(static_cast<net::rmq_flags>(cl.protocolflags)).value();
    }

    ent->baseline.alpha = (bits & B_ALPHA) ? net_message.read_byte().value() : ENTALPHA_DEFAULT; //johnfitz -- PROTOCOL_FITZQUAKE
    ent->baseline.scale = (bits & B_SCALE) ? net_message.read_byte().value() : ENTSCALE_DEFAULT;
}


/*
==================
CL_ParseClientdata

Server information pertaining to this client only
==================
*/
void CL_ParseClientdata(void) {
    int i, j;
    int bits; //johnfitz

    bits = (unsigned short) net_message.read_short().value(); //johnfitz -- read bits here isntead of in CL_ParseServerMessage()

    //johnfitz -- PROTOCOL_FITZQUAKE
    if (bits & SU_EXTEND1)
        bits |= (net_message.read_byte().value() << 16);
    if (bits & SU_EXTEND2)
        bits |= (net_message.read_byte().value() << 24);
    //johnfitz

    if (bits & SU_VIEWHEIGHT)
        cl.viewheight = net_message.read_char().value();
    else
        cl.viewheight = DEFAULT_VIEWHEIGHT;

    if (bits & SU_IDEALPITCH)
        cl.idealpitch = net_message.read_char().value();
    else
        cl.idealpitch = 0;

    VectorCopy(cl.mvelocity[0], cl.mvelocity[1]);
    for (i = 0; i < 3; i++) {
        if (bits & (SU_PUNCH1 << i))
            cl.punchangle[i] = net_message.read_char().value();
        else
            cl.punchangle[i] = 0;

        if (bits & (SU_VELOCITY1 << i))
            cl.mvelocity[0][i] = net_message.read_char().value() * 16;
        else
            cl.mvelocity[0][i] = 0;
    }

    //johnfitz -- update v_punchangles
    if (v_punchangles[0][0] != cl.punchangle[0] || v_punchangles[0][1] != cl.punchangle[1] || v_punchangles[0][2] != cl.
        punchangle[2]) {
        VectorCopy(v_punchangles[0], v_punchangles[1]);
        VectorCopy(cl.punchangle, v_punchangles[0]);
    }
    //johnfitz

    // [always sent]	if (bits & SU_ITEMS)
    i = net_message.read_long().value();

    if (cl.items != i) {
        // set flash times
        Sbar_Changed();
        for (j = 0; j < 32; j++)
            if ((i & (1 << j)) && !(cl.items & (1 << j)))
                cl.item_gettime[j] = cl.time;
        cl.items = i;
    }

    cl.onground = (bits & SU_ONGROUND) != 0;
    cl.inwater = (bits & SU_INWATER) != 0;

    if (bits & SU_WEAPONFRAME)
        cl.stats[STAT_WEAPONFRAME] = net_message.read_byte().value();
    else
        cl.stats[STAT_WEAPONFRAME] = 0;

    if (bits & SU_ARMOR)
        i = net_message.read_byte().value();
    else
        i = 0;
    if (cl.stats[STAT_ARMOR] != i) {
        cl.stats[STAT_ARMOR] = i;
        Sbar_Changed();
    }

    if (bits & SU_WEAPON)
        i = net_message.read_byte().value();
    else
        i = 0;
    if (cl.stats[STAT_WEAPON] != i) {
        cl.stats[STAT_WEAPON] = i;
        Sbar_Changed();
    }

    i = net_message.read_short().value();
    if (cl.stats[STAT_HEALTH] != i) {
        cl.stats[STAT_HEALTH] = i;
        Sbar_Changed();
    }

    i = net_message.read_byte().value();
    if (cl.stats[STAT_AMMO] != i) {
        cl.stats[STAT_AMMO] = i;
        Sbar_Changed();
    }

    for (i = 0; i < 4; i++) {
        j = net_message.read_byte().value();
        if (cl.stats[STAT_SHELLS + i] != j) {
            cl.stats[STAT_SHELLS + i] = j;
            Sbar_Changed();
        }
    }

    i = net_message.read_byte().value();

    if (standard_quake) {
        if (cl.stats[STAT_ACTIVEWEAPON] != i) {
            cl.stats[STAT_ACTIVEWEAPON] = i;
            Sbar_Changed();
        }
    } else {
        if (cl.stats[STAT_ACTIVEWEAPON] != (1 << i)) {
            cl.stats[STAT_ACTIVEWEAPON] = (1 << i);
            Sbar_Changed();
        }
    }

    //johnfitz -- PROTOCOL_FITZQUAKE
    if (bits & SU_WEAPON2)
        cl.stats[STAT_WEAPON] |= (net_message.read_byte().value() << 8);
    if (bits & SU_ARMOR2)
        cl.stats[STAT_ARMOR] |= (net_message.read_byte().value() << 8);
    if (bits & SU_AMMO2)
        cl.stats[STAT_AMMO] |= (net_message.read_byte().value() << 8);
    if (bits & SU_SHELLS2)
        cl.stats[STAT_SHELLS] |= (net_message.read_byte().value() << 8);
    if (bits & SU_NAILS2)
        cl.stats[STAT_NAILS] |= (net_message.read_byte().value() << 8);
    if (bits & SU_ROCKETS2)
        cl.stats[STAT_ROCKETS] |= (net_message.read_byte().value() << 8);
    if (bits & SU_CELLS2)
        cl.stats[STAT_CELLS] |= (net_message.read_byte().value() << 8);
    if (bits & SU_WEAPONFRAME2)
        cl.stats[STAT_WEAPONFRAME] |= (net_message.read_byte().value() << 8);
    if (bits & SU_WEAPONALPHA)
        cl.viewent.alpha = net_message.read_byte().value();
    else
        cl.viewent.alpha = ENTALPHA_DEFAULT;
    //johnfitz

    //johnfitz -- lerping
    //ericw -- this was done before the upper 8 bits of cl.stats[STAT_WEAPON] were filled in, breaking on large maps like zendar.bsp
    if (cl.viewent.model != cl.model_precache[cl.stats[STAT_WEAPON]]) {
        cl.viewent.lerpflags |= LERP_RESETANIM; //don't lerp animation across model changes
    }
    //johnfitz
}

/*
=====================
CL_NewTranslation
=====================
*/
void CL_NewTranslation(int slot) {
    int i, j;
    int top, bottom;
    byte *dest, *source;

    if (slot > cl.maxclients)
        Sys_Error("CL_NewTranslation: slot > cl.maxclients");
    dest = cl.scores[slot].translations;
    source = vid.colormap;
    memcpy(dest, vid.colormap, sizeof(cl.scores[slot].translations));
    top = cl.scores[slot].colors & 0xf0;
    bottom = (cl.scores[slot].colors & 15) << 4;
    R_TranslatePlayerSkin(slot);

    for (i = 0; i < VID_GRADES; i++, dest += 256, source += 256) {
        if (top < 128) // the artists made some backwards ranges.  sigh.
            memcpy(dest + TOP_RANGE, source + top, 16);
        else {
            for (j = 0; j < 16; j++)
                dest[TOP_RANGE + j] = source[top + 15 - j];
        }

        if (bottom < 128)
            memcpy(dest + BOTTOM_RANGE, source + bottom, 16);
        else {
            for (j = 0; j < 16; j++)
                dest[BOTTOM_RANGE + j] = source[bottom + 15 - j];
        }
    }
}

/*
=====================
CL_ParseStatic
=====================
*/
void CL_ParseStatic(int version) //johnfitz -- added a parameter
{
    entity_t *ent;
    int i;

    i = cl.num_statics;
    if (i >= MAX_STATIC_ENTITIES)
        Host_Error("Too many static entities");

    ent = &cl_static_entities[i];
    cl.num_statics++;
    CL_ParseBaseline(ent, version); //johnfitz -- added second parameter

    // copy it to the current state

    ent->model = cl.model_precache[ent->baseline.modelindex];
    ent->lerpflags |= LERP_RESETANIM; //johnfitz -- lerping
    ent->frame = ent->baseline.frame;

    ent->colormap = vid.colormap;
    ent->skinnum = ent->baseline.skin;
    ent->effects = ent->baseline.effects;
    ent->alpha = ent->baseline.alpha; //johnfitz -- alpha
    ent->scale = ent->baseline.scale;
    VectorCopy(ent->baseline.origin, ent->origin);
    VectorCopy(ent->baseline.angles, ent->angles);
    R_AddEfrags(ent);
}

/*
===================
CL_ParseStaticSound
===================
*/
void CL_ParseStaticSound(int version) //johnfitz -- added argument
{
    vec3_t org;
    int sound_num, vol, atten;
    int i;

    for (i = 0; i < 3; i++)
        org[i] = net_message.read_coord(static_cast<net::rmq_flags>(cl.protocolflags)).value();

    //johnfitz -- PROTOCOL_FITZQUAKE
    if (version == 2)
        sound_num = net_message.read_short().value();
    else
        sound_num = net_message.read_byte().value();
    //johnfitz

    vol = net_message.read_byte().value();
    atten = net_message.read_byte().value();

    S_StaticSound(cl.sound_precache[sound_num], org, vol, atten);
}


#if 0	/* for debugging. from fteqw. */
static void CL_DumpPacket(void) {
    int i, pos;
    unsigned char *packet = net_message.data;

    console::info("CL_DumpPacket, BEGIN:\n");
    pos = 0;
    while (pos < net_message.cursize) {
        console::info("%5i ", pos);
        for (i = 0; i < 16; i++) {
            if (pos >= net_message.cursize)
                console::info(" X ");
            else console::info("%2x ", packet[pos]);
            pos++;
        }
        pos -= 16;
        for (i = 0; i < 16; i++) {
            if (pos >= net_message.cursize)
                console::info("X");
            else if (packet[pos] == 0)
                console::info(".");
            else console::info("%c", packet[pos]);
            pos++;
        }
        console::info("\n");
    }

    console::info("CL_DumpPacket, --- END ---\n");
}
#endif	/* CL_DumpPacket */

#define SHOWNET(x) if(cl_shownet.value==2)console::info ("%3i:%s\n", net_message.offset()-1, x);

/*
=====================
CL_ParseServerMessage
=====================
*/
void CL_ParseServerMessage(void) {
    int cmd;
    int i;
    std::string str{}; //johnfitz
    int total, j, lastcmd; //johnfitz

    //
    // if recording demos, copy the message out
    //
    if (cl_shownet.value == 1)
        console::info("%i ", net_message.size());
    else if (cl_shownet.value == 2)
        console::info("------------------\n");

    //	cl.onground = false;	// unless the server says otherwise

    //
    // parse the message
    //
    net_message.begin_reading();

    lastcmd = 0;
    while (true) {
        if (net_message.eos())
            Host_Error("CL_ParseServerMessage: Bad server message");

        auto cmdv = net_message.read_byte();

        if (!cmdv.has_value()) {
            SHOWNET("END OF MESSAGE");
            return; // end of message
        }

        cmd = cmdv.value();

        // if the high bit of the command byte is set, it is a fast update
        if (cmd & U_SIGNAL) //johnfitz -- was 128, changed for clarity
        {
            SHOWNET("fast update");
            CL_ParseUpdate(cmd & 127);
            continue;
        }

        if (cmd < (int) NUM_SVC_STRINGS) {
            SHOWNET(svc_strings[cmd]);
        }

        // other commands
        switch (cmd) {
            default:
                //	CL_DumpPacket ();
                Host_Error("Illegible server message %d (previous was %s)", cmd, svc_strings[lastcmd]);
                //johnfitz -- added svc_strings[lastcmd]
                break;

            case svc_nop:
                //	console::info ("svc_nop\n");
                break;

            case svc_time:
                cl.mtime[1] = cl.mtime[0];
                cl.mtime[0] = net_message.read_float().value();
                break;

            case svc_clientdata:
                CL_ParseClientdata();
                //johnfitz -- removed bits parameter, we will read this inside CL_ParseClientdata()
                break;

            case svc_version:
                i = net_message.read_long().value();
                //johnfitz -- support multiple protocols
                if (i != PROTOCOL_NETQUAKE && i != PROTOCOL_FITZQUAKE && i != PROTOCOL_RMQ)
                    Host_Error("Server returned version %i, not %i or %i or %i", i, PROTOCOL_NETQUAKE,
                               PROTOCOL_FITZQUAKE, PROTOCOL_RMQ);
                cl.protocol = i;
                //johnfitz
                break;

            case svc_disconnect:
                Host_EndGame("Server disconnected\n");

            case svc_print:
                console::info("%s", net_message.read_string().value().c_str());
                break;

            case svc_centerprint:
                //johnfitz -- log centerprints to console
                str = net_message.read_string().value();
                SCR_CenterPrint(str.c_str());
                Con_LogCenterPrint(str.c_str());
                //johnfitz
                break;

            case svc_stufftext:
                command::buffer::add_text(net_message.read_string().value());
                break;

            case svc_damage:
                V_ParseDamage();
                break;

            case svc_serverinfo:
                CL_ParseServerInfo();
                vid.recalc_refdef = true; // leave intermission full screen
                break;

            case svc_setangle:
                for (i = 0; i < 3; i++)
                    cl.viewangles[i] = net_message.read_angle(static_cast<net::rmq_flags>(cl.protocolflags)).value();
                break;

            case svc_setview:
                cl.viewentity = net_message.read_short().value();
                break;

            case svc_lightstyle:
                i = net_message.read_byte().value();
                if (i >= MAX_LIGHTSTYLES)
                    Sys_Error("svc_lightstyle > MAX_LIGHTSTYLES");
                q_strlcpy(cl_lightstyle[i].map, net_message.read_string().value().c_str(), MAX_STYLESTRING);
                cl_lightstyle[i].length = std::strlen(cl_lightstyle[i].map);
                //johnfitz -- save extra info
                if (cl_lightstyle[i].length) {
                    total = 0;
                    cl_lightstyle[i].peak = 'a';
                    for (j = 0; j < cl_lightstyle[i].length; j++) {
                        total += cl_lightstyle[i].map[j] - 'a';
                        cl_lightstyle[i].peak = std::max(cl_lightstyle[i].peak, cl_lightstyle[i].map[j]);
                    }
                    cl_lightstyle[i].average = total / cl_lightstyle[i].length + 'a';
                } else
                    cl_lightstyle[i].average = cl_lightstyle[i].peak = 'm';
                //johnfitz
                break;

            case svc_sound:
                CL_ParseStartSoundPacket();
                break;

            case svc_stopsound:
                i = net_message.read_short().value();
                S_StopSound(i >> 3, i & 7);
                break;

            case svc_updatename:
                Sbar_Changed();
                i = net_message.read_byte().value();
                if (i >= cl.maxclients)
                    Host_Error("CL_ParseServerMessage: svc_updatename > MAX_SCOREBOARD");
                q_strlcpy(cl.scores[i].name, net_message.read_string().value().c_str(), MAX_SCOREBOARDNAME);
                break;

            case svc_updatefrags:
                Sbar_Changed();
                i = net_message.read_byte().value();
                if (i >= cl.maxclients)
                    Host_Error("CL_ParseServerMessage: svc_updatefrags > MAX_SCOREBOARD");
                cl.scores[i].frags = net_message.read_short().value();
                break;

            case svc_updatecolors:
                Sbar_Changed();
                i = net_message.read_byte().value();
                if (i >= cl.maxclients)
                    Host_Error("CL_ParseServerMessage: svc_updatecolors > MAX_SCOREBOARD");
                cl.scores[i].colors = net_message.read_byte().value();
                CL_NewTranslation(i);
                break;

            case svc_particle:
                R_ParseParticleEffect();
                break;

            case svc_spawnbaseline:
                i = net_message.read_short().value();
                // must use CL_EntityNum() to force cl.num_entities up
                CL_ParseBaseline(CL_EntityNum(i), 1); // johnfitz -- added second parameter
                break;

            case svc_spawnstatic:
                CL_ParseStatic(1); //johnfitz -- added parameter
                break;

            case svc_temp_entity:
                CL_ParseTEnt();
                break;

            case svc_setpause:
                cl.paused = net_message.read_byte().value();
                if (cl.paused) {
                    CDAudio_Pause();
                    music::pause();
                } else {
                    CDAudio_Resume();
                    music::resume();
                }
                break;

            case svc_signonnum:
                i = net_message.read_byte().value();
                if (i <= cls.signon)
                    Host_Error("Received signon %i when at %i", i, cls.signon);
                cls.signon = i;
                //johnfitz -- if signonnum==2, signon packet has been fully parsed, so check for excessive static ents and efrags
                if (i == 2) {
                    if (cl.num_statics > 128)
                        console::dev_warn("%i static entities exceeds standard limit of 128 (max = %d).\n", cl.num_statics,
                                     MAX_STATIC_ENTITIES);
                    R_CheckEfrags();
                }
                //johnfitz
                CL_SignonReply();
                break;

            case svc_killedmonster:
                cl.stats[STAT_MONSTERS]++;
                break;

            case svc_foundsecret:
                cl.stats[STAT_SECRETS]++;
                break;

            case svc_updatestat:
                i = net_message.read_byte().value();
                if (i < 0 || i >= MAX_CL_STATS)
                    Sys_Error("svc_updatestat: %i is invalid", i);
                cl.stats[i] = net_message.read_long().value();;
                break;

            case svc_spawnstaticsound:
                CL_ParseStaticSound(1); //johnfitz -- added parameter
                break;

            case svc_cdtrack:
                cl.cdtrack = net_message.read_byte().value();
                cl.looptrack = net_message.read_byte().value();
                if ((cls.demoplayback || cls.demorecording) && (cls.forcetrack != -1))
                    music::play_cd_track((byte) cls.forcetrack, true);
                else
                    music::play_cd_track((byte) cl.cdtrack, true);
                break;

            case svc_intermission:
                cl.intermission = 1;
                cl.completed_time = cl.time;
                vid.recalc_refdef = true; // go to full screen
                V_RestoreAngles();
                break;

            case svc_finale:
                cl.intermission = 2;
                cl.completed_time = cl.time;
                vid.recalc_refdef = true; // go to full screen
                //johnfitz -- log centerprints to console
                str = net_message.read_string().value();
                SCR_CenterPrint(str.c_str());
                Con_LogCenterPrint(str.c_str());
                //johnfitz
                V_RestoreAngles();
                break;

            case svc_cutscene:
                cl.intermission = 3;
                cl.completed_time = cl.time;
                vid.recalc_refdef = true; // go to full screen
                //johnfitz -- log centerprints to console
                str = net_message.read_string().value();
                SCR_CenterPrint(str.c_str());
                Con_LogCenterPrint(str.c_str());
                //johnfitz
                V_RestoreAngles();
                break;

            case svc_sellscreen:
                command::execute_string("help", command::source::command);
                break;

            //johnfitz -- new svc types
            case svc_skybox:
                Sky_LoadSkyBox(net_message.read_string().value().c_str());
                break;

            case svc_bf:
                command::execute_string("bf", command::source::command);
                break;

            case svc_fog:
                Fog_ParseServerMessage();
                break;

            case svc_spawnbaseline2: //PROTOCOL_FITZQUAKE
                i = net_message.read_short().value();
                // must use CL_EntityNum() to force cl.num_entities up
                CL_ParseBaseline(CL_EntityNum(i), 2);
                break;

            case svc_spawnstatic2: //PROTOCOL_FITZQUAKE
                CL_ParseStatic(2);
                break;

            case svc_spawnstaticsound2: //PROTOCOL_FITZQUAKE
                CL_ParseStaticSound(2);
                break;
            //johnfitz

            //used by the 2021 rerelease
            case svc_achievement:
                str = net_message.read_string().value();
                console::debug("Ignoring svc_achievement (%s)\n", str.c_str());
                break;
            case svc_localsound:
                CL_ParseLocalSound();
                break;
        }

        lastcmd = cmd; //johnfitz
    }
}
