#include "coduo_engine_structs.h"
#include "../abi/vm_private.h"
#include "../animation/dobj_private.h"
#include "../animation/xanim_private.h"
#include "../core_command/cmd_private.h"
#include "../core_cvar/cvar_private.h"
#include "../core_info/info_private.h"
#include "../core_runtime/core_runtime_private.h"
#include "../filesystem/fs_private.h"
#include "../networking/msg_private.h"
#include "../networking/netchan_private.h"
#include "../networking/sv_snapshot_private.h"
#include "../punkbuster/pb_private.h"
#include "../scripting/script_runtime_private.h"
#include "server_private.h"
#include "sv_globals_private.h"

#include <stdint.h>
#include <stdlib.h>
#include <string.h>

enum {
    SV_OOB_MARKER_BYTES = 4,
    SV_CONNECT_COMPRESSED_PAYLOAD_OFFSET = 12,
    SV_PROTOCOL_VERSION = 22,
    SV_STATUS_KEYWORD_BUFFER_SIZE = 1024,
    SV_STATUS_INFO_BUFFER_SIZE = 1024,
    SV_STATUS_CLIENT_LINE_BUFFER_SIZE = 1024,
    SV_STATUS_TEXT_APPEND_LIMIT = CODUO_MAX_MSGLEN - 1,
    SV_INFO_BUFFER_SIZE = 1024,
    SV_PB_PREFIX_LENGTH = 3,
    SV_PB_MESSAGE_COMMAND_OFFSET = 7,
    SV_PB_SERVER_COMMAND = 13,
    SV_RCON_THROTTLE_MSEC = 500,
    SV_RCON_REDIRECT_BUFFER_SIZE = 32752,
    SV_RCON_OUTPUT_CHUNK_SIZE = 1300,
    SV_RCON_COMMAND_BUFFER_SIZE = 1024,
    SV_RCON_COMMAND_COPY_LIMIT = 1024,
    SV_MAX_PING = 999,
    SV_RELIABLE_COMMAND_WINDOW = CODUO_MAX_RELIABLE_COMMANDS,
    SV_TIMEOUT_SECONDS_TO_MSEC = 1000,
    SV_TIMEOUT_DROP_SCANS = 5,
    SV_FRAME_MSEC_BASE = 1000,
    SV_FRAME_WRAP_LIMIT = 0x70000000,
    SV_FRAME_MAPNAME_BUFFER_SIZE = 64,
    SV_FRAME_MAPNAME_COPY_LIMIT = 64,
    SV_FRAME_SERVERINFO_CONFIG_MASK =
        CODUO_CVAR_SERVERINFO | CODUO_CVAR_SCRIPT_SETCVAR_SERVERINFO,
    SV_FRAME_SYSTEMINFO_CONFIG_MASK = CODUO_CVAR_SYSTEMINFO,
    SV_FRAME_SCRIPT_SERVERINFO_CONFIG_MASK =
        CODUO_CVAR_SCRIPT_MAKE_SERVERINFO,
    SV_FRAME_MAX_SNAPSHOT_COUNTER = 0x7ffffffe,
    SV_FRAME_MAX_CACHED_SNAPSHOT_ENTITIES = 0x7fffbffe,
    SV_FRAME_MAX_CACHED_SNAPSHOT_CLIENTS = 0x7fffeffe,
    SV_FRAME_MAX_ARCHIVED_SNAPSHOT_FRAMES = 0x7ffffb4e,
    SV_FRAME_MAX_ARCHIVED_SNAPSHOT_BUFFER = 0x7dfffffe,
    SV_FRAME_MAX_CACHED_SNAPSHOT_FRAMES = 0x7ffffdfe,
    SV_BOT_BUTTON_ATTACK = 0x01,
    SV_BOT_BUTTON_EXTRA = 0x40,
    SV_BOT_FORWARD_MOVE = 127,
    SV_BOT_BACK_MOVE = -127
};

static netadr_t sv_rconRedirectAddress;

/* NOT_FROM_ORIGINAL_SOURCE: clears the split native representation of the
 * original server-static shutdown span owned by this translation unit. */
void coduomp_sv_clear_rcon_redirect_address(void)
{
    memset(&sv_rconRedirectAddress, 0, sizeof(sv_rconRedirectAddress));
}

/* NOT_FROM_ORIGINAL_SOURCE: shared status response line builder. */
static int coduomp_sv_build_status_client_lines(char *status,
                                                size_t statusSize)
{
    int statusLength;
    int clientNum;
    char line[SV_STATUS_CLIENT_LINE_BUFFER_SIZE];
    size_t lineLength;

    status[0] = '\0';
    statusLength = 0;
    for (clientNum = 0; clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
         ++clientNum) {
        client_t *client = &svs.clients[clientNum];

        if (client->state <= CODUO_CS_ZOMBIE) {
            continue;
        }

        (void)SV_GameClientNum(clientNum);

        Com_sprintf(line, 0x400, "%i %i \"%s\"\n",
                    SV_GetClientScore(client), client->ping, client->name);

        lineLength = strlen(line);
        if (lineLength + (size_t)statusLength > SV_STATUS_TEXT_APPEND_LIMIT ||
            lineLength + (size_t)statusLength >= statusSize) {
            break;
        }

        strcpy(status + statusLength, line);
        statusLength += (int)lineLength;
    }

    return statusLength;
}

/* NOT_FROM_ORIGINAL_SOURCE: shared fs_restrict keyword decoration. */
static void coduomp_sv_add_restricted_demo_keyword(char *info)
{
    char keywords[SV_STATUS_KEYWORD_BUFFER_SIZE];

    if (Cvar_VariableValue("fs_restrict") == 0.0f) {
        return;
    }

    Com_sprintf(keywords, sizeof(keywords), "demo %s",
                Info_ValueForKey(info, "sv_keywords"));
    Info_SetValueForKey(info, "sv_keywords", keywords);
}

/* NOT_FROM_ORIGINAL_SOURCE: shared `mod` flag calculation for status queries. */
static qboolean coduomp_sv_server_reports_modded(void)
{
    const char *fsGame = Cvar_VariableString("fs_game");
    const char *referencedPaks;
    int pakCount;
    int pakIndex;

    if (host_cvar_sv_pure_pointer_slot->integer == 0 ||
        (fsGame != NULL && fsGame[0] != '\0')) {
        return qtrue;
    }

    referencedPaks = Cvar_VariableString("sv_referencedPakNames");
    if (referencedPaks == NULL || referencedPaks[0] == '\0') {
        return qfalse;
    }

    Cmd_TokenizeString(referencedPaks);
    pakCount = Cmd_Argc();
    for (pakIndex = 0; pakIndex < pakCount; ++pakIndex) {
        if (FS_idPak(Cmd_Argv(pakIndex), "main",
                     host_cvar_fs_basegame_pointer_slot->string) ==
            qfalse) {
            return qtrue;
        }
    }

    return qfalse;
}

/* NOT_FROM_ORIGINAL_SOURCE: shared status/info prefix population. */
static void coduomp_sv_add_status_base_info(char *info)
{
    strcpy(info, Cvar_InfoString(CODUO_CVAR_SERVERINFO_SYNC_MASK));
    Info_SetValueForKey(info, "challenge", Cmd_Argv(1));
    coduomp_sv_add_restricted_demo_keyword(info);
}

void SVC_Status(netadr_t from)
{
    char info[SV_STATUS_INFO_BUFFER_SIZE];
    char status[CODUO_MAX_MSGLEN];
    const char *password;

    coduomp_sv_add_status_base_info(info);
    coduomp_sv_build_status_client_lines(status, sizeof(status));

    if (host_cvar_sv_disableClientConsole_pointer_slot->integer != 0) {
        Info_SetValueForKey(
            info, "con_disabled",
            va("%i", host_cvar_sv_disableClientConsole_pointer_slot->integer));
    }

    password = Cvar_VariableString("g_password");
    Info_SetValueForKey(info, "pswrd",
                        password == NULL || password[0] == '\0' ? "0" : "1");
    Info_SetValueForKey(
        info, "mod", va("%i", coduomp_sv_server_reports_modded()));

    NET_OutOfBandPrint(NS_SERVER, from, "statusResponse\n%s\n%s", info,
                       status);
}

void SVC_GameCompleteStatus(netadr_t from)
{
    char info[SV_STATUS_INFO_BUFFER_SIZE];
    char status[CODUO_MAX_MSGLEN];
    char line[SV_STATUS_KEYWORD_BUFFER_SIZE];
    int statusLength;
    int clientNum;
    int lineLength;

    coduomp_sv_add_status_base_info(info);

    status[0] = '\0';
    statusLength = 0;
    for (clientNum = 0; clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
         ++clientNum) {
        client_t *client = &svs.clients[clientNum];

        if (client->state <= CODUO_CS_ZOMBIE) {
            continue;
        }

        (void)SV_GameClientNum(clientNum);

        Com_sprintf(line, sizeof(line), "%i %i \"%s\"\n",
                    SV_GetClientScore(client), client->ping, client->name);
        lineLength = (int)strlen(line);
        if (statusLength + lineLength > SV_STATUS_TEXT_APPEND_LIMIT) {
            break;
        }

        strcpy(status + statusLength, line);
        statusLength += lineLength;
    }

    NET_OutOfBandPrint(NS_SERVER, from, "gameCompleteStatus\n%s\n%s", info,
                       status);
}

void SVC_Info(netadr_t from)
{
    char info[SV_INFO_BUFFER_SIZE];
    int clientNum;
    int privateClientCount;
    int connectedClientCount;
    int publicClientSlots;
    const char *value;

    privateClientCount = 0;
    for (clientNum = 0;
         clientNum < host_cvar_sv_privateClients_pointer_slot->integer;
         ++clientNum) {
        if (svs.clients[clientNum].state > CODUO_CS_ZOMBIE) {
            ++privateClientCount;
        }
    }

    connectedClientCount = privateClientCount;
    for (; clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
         ++clientNum) {
        if (svs.clients[clientNum].state > CODUO_CS_ZOMBIE) {
            ++connectedClientCount;
        }
    }

    info[0] = '\0';
    Info_SetValueForKey(info, "challenge", Cmd_Argv(1));
    Info_SetValueForKey(info, "protocol", va("%i", SV_PROTOCOL_VERSION));
    Info_SetValueForKey(info, "hostname",
                        host_cvar_sv_hostname_pointer_slot->string);
    Info_SetValueForKey(info, "mapname", host_cvar_mapname_pointer_slot->string);
    if (connectedClientCount != 0) {
        Info_SetValueForKey(info, "clients", va("%i", connectedClientCount));
    }

    publicClientSlots =
        host_cvar_sv_maxclients_pointer_slot->integer -
        (host_cvar_sv_privateClients_pointer_slot->integer - privateClientCount);
    if (publicClientSlots > 0) {
        Info_SetValueForKey(info, "sv_maxclients",
                            va("%i", publicClientSlots));
    }

    Info_SetValueForKey(info, "gametype",
                        host_cvar_g_gametype_pointer_slot->string);
    if (host_cvar_sv_pure_pointer_slot->integer != 0) {
        Info_SetValueForKey(info, "pure",
                            va("%i", host_cvar_sv_pure_pointer_slot->integer));
    }
    if (host_cvar_sv_minPing_pointer_slot->integer != 0) {
        Info_SetValueForKey(info, "minPing",
                            va("%i", host_cvar_sv_minPing_pointer_slot->integer));
    }
    if (host_cvar_sv_maxPing_pointer_slot->integer != 0) {
        Info_SetValueForKey(info, "maxPing",
                            va("%i", host_cvar_sv_maxPing_pointer_slot->integer));
    }

    value = Cvar_VariableString("fs_game");
    if (value != NULL && value[0] != '\0') {
        Info_SetValueForKey(info, "game", value);
    }
    if (host_cvar_sv_allowAnonymous_pointer_slot->integer != 0) {
        Info_SetValueForKey(
            info, "sv_allowAnonymous",
            va("%i", host_cvar_sv_allowAnonymous_pointer_slot->integer));
    }

    value = Cvar_VariableString("g_password");
    if (value != NULL && value[0] != '\0') {
        Info_SetValueForKey(info, "pswrd", "1");
    }
    value = Cvar_VariableString("scr_friendlyfire");
    if (value != NULL && atoi(value) != 0) {
        Info_SetValueForKey(info, "ff", value);
    }
    value = Cvar_VariableString("scr_killcam");
    if (value != NULL && atoi(value) != 0) {
        Info_SetValueForKey(info, "kc", value);
    }
    value = Cvar_VariableString("g_timeoutsallowed");
    if (value != NULL && atoi(value) != 0) {
        Info_SetValueForKey(info, "timeoutsAllowed", value);
    }
    value = Cvar_VariableString("scr_allow_jeeps");
    if (value != NULL && atoi(value) != 0) {
        Info_SetValueForKey(info, "jps", value);
    }
    value = Cvar_VariableString("scr_allow_tanks");
    if (value != NULL && atoi(value) != 0) {
        Info_SetValueForKey(info, "tnk", value);
    }

    Info_SetValueForKey(info, "hw", va("%i", qtrue));
    Info_SetValueForKey(info, "pb",
                        va("%i", host_cvar_sv_punkbuster_pointer_slot->integer));
    Info_SetValueForKey(
        info, "mod", va("%i", coduomp_sv_server_reports_modded()));

    NET_OutOfBandPrint(NS_SERVER, from, "infoResponse\n%s", info);
}

void SV_FlushRedirect(char *text)
{
    int32_t length;

    length = (int32_t)strlen(text);
    while (length > SV_RCON_OUTPUT_CHUNK_SIZE) {
        char saved;

        saved = text[SV_RCON_OUTPUT_CHUNK_SIZE];
        text[SV_RCON_OUTPUT_CHUNK_SIZE] = '\0';
        NET_OutOfBandPrint(NS_SERVER, sv_rconRedirectAddress, "print\n%s",
                           text);
        length -= SV_RCON_OUTPUT_CHUNK_SIZE;
        text += SV_RCON_OUTPUT_CHUNK_SIZE;
        *text = saved;
    }

    NET_OutOfBandPrint(NS_SERVER, sv_rconRedirectAddress, "print\n%s", text);
}

void SVC_RemoteCommand(netadr_t from)
{
    char redirectBuffer[SV_RCON_REDIRECT_BUFFER_SIZE];
    char commandBuffer[SV_RCON_COMMAND_BUFFER_SIZE];
    int commandOffset;
    int commandLimit;
    qboolean validPassword;
    int now;
    int arg;

    now = Com_Milliseconds();
    if (host_sv_rconThrottleTime != 0 &&
        now - host_sv_rconThrottleTime < SV_RCON_THROTTLE_MSEC) {
        return;
    }

    host_sv_rconThrottleTime = now;
    validPassword = qfalse;
    if (host_cvar_rconPassword_pointer_slot->string[0] != '\0' &&
        strcmp(Cmd_Argv(1), host_cvar_rconPassword_pointer_slot->string) == 0) {
        validPassword = qtrue;
        Com_Printf("Rcon from %s:\n%s\n", NET_AdrToString(from), Cmd_Argv(2));
    }
    else {
        Com_Printf("Bad rcon from %s:\n%s\n", NET_AdrToString(from),
                   Cmd_Argv(2));
    }

    sv_rconRedirectAddress = from;
    Com_BeginRedirect(redirectBuffer, SV_RCON_REDIRECT_BUFFER_SIZE,
                      SV_FlushRedirect);

    if (host_cvar_rconPassword_pointer_slot->string[0] == '\0') {
        Com_Printf("No rconpassword set on the server.\n");
    }
    else if (validPassword == qfalse) {
        Com_Printf("Bad rconpassword.\n");
    }
    else {
        commandOffset = 0;
        commandLimit = SV_RCON_COMMAND_COPY_LIMIT;
        for (arg = 2; arg < Cmd_Argc(); ++arg) {
            commandOffset = Com_AddToString(Cmd_Argv(arg), commandBuffer,
                                            commandOffset, commandLimit, qtrue);
            commandOffset = Com_AddToString(" ", commandBuffer, commandOffset,
                                            commandLimit, qfalse);
        }

        if (commandOffset < commandLimit) {
            commandBuffer[commandOffset] = '\0';
            Cmd_ExecuteString(commandBuffer);
            if (Q_stricmpn(commandBuffer, "pb_sv_", 6) == 0) {
                PB_CallServerSaCommandDrain();
            }
        }
    }

    Com_EndRedirect();
}

void SV_ConnectionlessPacket(netadr_t from, msg_t *msg)
{
    const char *commandText;
    const char *command;
    int clientNum;
    int pbClientNum;

    pbClientNum = -1;
    MSG_BeginReading(msg);
    (void)MSG_ReadLong(msg);
    /* ORIGINAL_BINARY_BUG: the incoming connectionless packet is added to
     * the OOB profile's send stream. See original-bugs/
     * coduomp-incoming-oob-profile-direction.md. */
    SV_Netchan_AddOOBProfilePacket(msg->cursize);

    /* ORIGINAL_BINARY_BUG: SV_PacketEvent proves only the four-byte marker;
     * these three-, seven-, and offset-seven reads have no local extent
     * gate. See original-bugs/
     * coduomp-connectionless-short-prefix-stale-bytes.md. */
    if (Q_stricmpn((const char *)msg->data + SV_OOB_MARKER_BYTES, "pb_",
                   SV_PB_PREFIX_LENGTH) == 0) {
        for (clientNum = 0;
             clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
             ++clientNum) {
            client_t *client = &svs.clients[clientNum];

            if (client->state != CODUO_CS_FREE &&
                NET_CompareBaseAdr(from,
                                   client->netchan.remoteAddress) != qfalse &&
                client->netchan.remoteAddress.port == from.port) {
                pbClientNum = clientNum;
                break;
            }
        }

        if (msg->data[SV_PB_MESSAGE_COMMAND_OFFSET] != 'C' &&
            msg->data[SV_PB_MESSAGE_COMMAND_OFFSET] != '1' &&
            msg->data[SV_PB_MESSAGE_COMMAND_OFFSET] != 'J') {
            PB_CallServerSbGlobal(
                SV_PB_SERVER_COMMAND, pbClientNum,
                (uint32_t)(msg->cursize - SV_OOB_MARKER_BYTES),
                (const char *)msg->data + SV_OOB_MARKER_BYTES);
        }
        return;
    }

    if (Q_strncmp("connect", (const char *)msg->data + SV_OOB_MARKER_BYTES,
                  (int)sizeof("connect") - 1) == 0) {
        /* ORIGINAL_BINARY_BUG: retail Huff_Decompress has no failure result
         * and continues command dispatch after an out-of-extent adaptive
         * decode. SECURITY_PATCH, NOT_FROM_ORIGINAL_SOURCE: a failed transform
         * leaves the compressed message unpublished, so discard it instead of
         * tokenizing the compressed/partial payload. See original-bugs/
         * coduomp-huffman-short-connect-input.md. */
        if (Huff_Decompress(msg, SV_CONNECT_COMPRESSED_PAYLOAD_OFFSET) ==
            qfalse) {
            return;
        }
    }

    commandText = MSG_ReadStringLine(msg);
    Cmd_TokenizeString(commandText);
    command = Cmd_Argv(0);

    if (host_cvar_sv_packet_info_pointer_slot->integer != 0) {
        Com_Printf("SV packet %s : %s\n", NET_AdrToString(from), command);
    }

    if (Q_stricmp(command, "getstatus") == 0) {
        SVC_Status(from);
    }
    else if (Q_stricmp(command, "getinfo") == 0) {
        SVC_Info(from);
    }
    else if (Q_stricmp(command, "getchallenge") == 0) {
        SV_GetChallenge(from);
    }
    else if (Q_stricmp(command, "connect") == 0) {
        PB_InvokeEventCallback(NET_IsLocalAddress(from) ? "localhost"
                                                        : NET_AdrToString(from),
                               msg->data);
        SVC_DirectConnect(from);
    }
    else if (Q_stricmp(command, "ipAuthorize") == 0) {
        SV_AuthorizeIpPacket(from);
    }
    else if (Q_stricmp(command, "rcon") == 0) {
        SVC_RemoteCommand(from);
    }
    else if (Q_stricmp(command, "disconnect") != 0) {
        Com_DPrintf("bad connectionless packet from %s:\n%s\n",
                    NET_AdrToString(from), commandText);
    }
}

void SV_PacketEvent(netadr_t from, msg_t *msg)
{
    int32_t marker;
    int32_t qport;
    client_t *client;
    int clientNum;

    if (msg->cursize > 3) {
        /* Stock VA 0x08094a07 loads the first four message bytes as one
         * dword. The network buffer has no alignment contract on native
         * hosts, so copy the same bits into an aligned scalar before testing. */
        memcpy(&marker, msg->data, sizeof(marker));
        if (marker == -1) {
            SV_ConnectionlessPacket(from, msg);
            return;
        }
    }

    ++dobj_skelCacheKey;
    if (dobj_skelCacheKey == 0) {
        ++dobj_skelCacheKey;
    }

    host_sv_frame_running_flag = 1;
    MSG_BeginReading(msg);
    (void)MSG_ReadLong(msg);
    qport = MSG_ReadShort(msg) & 0xffff;

    client = svs.clients;
    for (clientNum = 0; clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
         ++clientNum, ++client) {
        if (client->state == CODUO_CS_FREE ||
            NET_CompareBaseAdr(from,
                               client->netchan.remoteAddress) == qfalse ||
            client->netchan.qport != qport) {
            continue;
        }

        if (client->netchan.remoteAddress.port != from.port) {
            Com_Printf("SV_ReadPackets: fixing up a translated port\n");
            client->netchan.remoteAddress.port = from.port;
        }

        if (Netchan_Process(&client->netchan, msg) != 0) {
            client->serverId = MSG_ReadByte(msg);
            client->messageAcknowledge = MSG_ReadLong(msg);
            if (client->messageAcknowledge >= 0) {
                client->reliableAcknowledge = MSG_ReadLong(msg);
                if (client->reliableSequence -
                        client->reliableAcknowledge <
                    SV_RELIABLE_COMMAND_WINDOW) {
                    SV_Netchan_Decode(client, msg->data + msg->readcount,
                                      msg->cursize - msg->readcount);
                    if (client->state != CODUO_CS_ZOMBIE) {
                        client->lastPacketTime = svs.timeSecondary;
                        SV_ParseClientMessage(client, msg);
                    }
                }
                else {
                    client->reliableAcknowledge = client->reliableSequence;
                }
            }
        }

        host_sv_frame_running_flag = 0;
        Hunk_ClearTempMemory();
        return;
    }

    NET_OutOfBandPrint(NS_SERVER, from, "disconnect");
    host_sv_frame_running_flag = 0;
    Hunk_ClearTempMemory();
}

void SV_CalcPings(void)
{
    client_t *client;
    int clientNum;

    client = svs.clients;
    for (clientNum = 0; clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
         ++clientNum, ++client) {
        int pingTotal;
        int pingSamples;
        int frameIndex;

        if (client->state != CODUO_CS_ACTIVE || client->gentity == NULL) {
            client->ping = SV_MAX_PING;
            continue;
        }

        pingTotal = 0;
        pingSamples = 0;
        for (frameIndex = 0; frameIndex < CODUO_HOST_CLIENT_SNAPSHOT_FRAME_COUNT;
             ++frameIndex) {
            const clientSnapshot_t *frame;

            frame = &client->snapshotFrames[frameIndex];
            if (frame->messageAckedTime > 0) {
                ++pingSamples;
                pingTotal += frame->messageAckedTime - frame->messageSentTime;
            }
        }

        if (pingSamples == 0) {
            client->ping = SV_MAX_PING;
        }
        else {
            client->ping = pingTotal / pingSamples;
            if (client->ping > SV_MAX_PING) {
                client->ping = SV_MAX_PING;
            }
        }
    }
}

void SV_CheckTimeouts(void)
{
    int timeoutLimit;
    int zombieLimit;
    client_t *client;
    int clientNum;

    timeoutLimit = svs.timeSecondary -
                   host_cvar_sv_timeout_pointer_slot->integer *
                       SV_TIMEOUT_SECONDS_TO_MSEC;
    zombieLimit = svs.timeSecondary -
                  host_cvar_sv_zombietime_pointer_slot->integer *
                      SV_TIMEOUT_SECONDS_TO_MSEC;

    client = svs.clients;
    for (clientNum = 0; clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
         ++clientNum, ++client) {
        if (client->lastPacketTime > svs.timeSecondary) {
            client->lastPacketTime = svs.timeSecondary;
        }

        if (client->bIsTestClient != 0) {
            continue;
        }

        if (client->state == CODUO_CS_ZOMBIE &&
            client->lastPacketTime < zombieLimit) {
            Com_DPrintf("Going from CS_ZOMBIE to CS_FREE for %s\n",
                        client->name);
            client->state = CODUO_CS_FREE;
        }
        else if (client->state < CODUO_CS_CONNECTED ||
                 client->lastPacketTime >= timeoutLimit) {
            client->timeoutCount = 0;
        }
        else {
            ++client->timeoutCount;
            if (client->timeoutCount > SV_TIMEOUT_DROP_SCANS) {
                SV_DropClient(client, "EXE_TIMEDOUT");
                client->state = CODUO_CS_FREE;
            }
        }
    }
}

qboolean SV_CheckPaused(void)
{
    client_t *client;
    int clientNum;
    int connectedClients;

    if (host_cvar_cl_paused_pointer_slot->integer == 0) {
        return qfalse;
    }

    connectedClients = 0;
    client = svs.clients;
    for (clientNum = 0; clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
         ++clientNum, ++client) {
        if (client->state > CODUO_CS_ZOMBIE) {
            ++connectedClients;
        }
    }

    if (connectedClients > 1) {
        host_cvar_sv_paused_pointer_slot->integer = 0;
        return qfalse;
    }

    host_cvar_sv_paused_pointer_slot->integer = 1;
    return qtrue;
}

void SV_RunGameFrame(void)
{
    VM_Call(sv_gameVM, CODUO_GAME_UPDATE_CVARS);

    ++dobj_skelCacheKey;
    if (dobj_skelCacheKey == 0) {
        ++dobj_skelCacheKey;
    }

    host_sv_frame_running_flag = 1;
    VM_Call(sv_gameVM, CODUO_GAME_RUN_FRAME, svs.time);
    host_sv_frame_running_flag = 0;
    Hunk_ClearTempMemory();
}

void SV_BotUserMove(client_t *client)
{
    int32_t clientNum;
    const playerState_t *playerState;
    usercmd_t cmd;

    if (client->gentity == NULL) {
        return;
    }

    memset(&cmd, 0, sizeof(cmd));
    clientNum = (int32_t)(client - svs.clients);
    playerState = SV_GameClientNum(clientNum);
    cmd.weapon = (uint8_t)playerState->currentWeapon;

    if (SV_GetClientArchiveTime(clientNum) == 0) {
        /* Stock 0x80950b5.. keeps each rand() fild-exact (no float cast). */
        if (rand() / 2147483648.0f < 0.5f) {
            cmd.buttons |= SV_BOT_BUTTON_ATTACK;
        }
        if (rand() / 2147483648.0f < 0.5f) {
            cmd.buttons |= SV_BOT_BUTTON_EXTRA;
        }
        if (rand() / 2147483648.0f < 0.33f) {
            cmd.forwardmove = SV_BOT_FORWARD_MOVE;
        }
        else if (rand() / 2147483648.0f < 0.5f) {
            cmd.forwardmove = SV_BOT_BACK_MOVE;
        }
    }

    client->deltaMessage = client->netchan.outgoingSequence - 1;
    SV_ClientThink(client, &cmd);
}

void SV_RunBotFrame(void)
{
    client_t *client;
    int clientNum;

    ++dobj_skelCacheKey;
    if (dobj_skelCacheKey == 0) {
        ++dobj_skelCacheKey;
    }

    host_sv_frame_running_flag = 1;
    client = svs.clients;
    for (clientNum = 0; clientNum < host_cvar_sv_maxclients_pointer_slot->integer;
         ++clientNum, ++client) {
        if (client->state != CODUO_CS_FREE &&
            client->netchan.remoteAddress.type == CODUO_NA_BAD) {
            SV_BotUserMove(client);
        }
    }
    host_sv_frame_running_flag = 0;
    Hunk_ClearTempMemory();
}

static void coduomp_sv_restart_map_after_counter_wrap(const char *message)
{
    /* NOT_FROM_ORIGINAL_SOURCE: local factoring of SV_Frame's repeated map-restart block. */
    char mapname[SV_FRAME_MAPNAME_BUFFER_SIZE];

    Q_strncpyz(mapname, host_cvar_mapname_pointer_slot->string,
               SV_FRAME_MAPNAME_COPY_LIMIT);
    Com_Shutdown(message);
    Cbuf_AddText(va("map %s\n", mapname));
}

void SV_Frame(int msec)
{
    int frameMsec;
    int frameStartTime;
    int scaledFrameMsec;

    frameStartTime = 0;
    if (host_cvar_sv_killserver_pointer_slot->integer != 0) {
        Com_Shutdown("EXE_SERVERKILLED");
        Cvar_Set("sv_killserver", "0");
        return;
    }

    if (host_cvar_sv_running_pointer_slot->integer == 0 ||
        SV_CheckPaused() != qfalse) {
        return;
    }

    if (host_cvar_sv_fps_pointer_slot->integer < 1) {
        Cvar_Set("sv_fps", "10");
    }

    frameMsec = SV_FRAME_MSEC_BASE / host_cvar_sv_fps_pointer_slot->integer;
    host_sv_timeResidual += msec;
    if (host_sv_timeResidual < frameMsec) {
        return;
    }

    if (svs.timeSecondary > SV_FRAME_WRAP_LIMIT ||
        svs.time > SV_FRAME_WRAP_LIMIT) {
        coduomp_sv_restart_map_after_counter_wrap(
            "EXE_SERVERRESTARTTIMEWRAP");
        return;
    }
    if (svs.nextEntityStateSnapshot >=
        SV_FRAME_MAX_SNAPSHOT_COUNTER - svs.numEntityStateSnapshots) {
        coduomp_sv_restart_map_after_counter_wrap(
            "EXE_SERVERRESTARTMISC\x15numSnapshotEntities");
        return;
    }
    if (svs.nextCachedSnapshotEntities >=
        SV_FRAME_MAX_CACHED_SNAPSHOT_ENTITIES) {
        coduomp_sv_restart_map_after_counter_wrap(
            "EXE_SERVERRESTARTMISC\x15nextCachedSnapshotEntities");
        return;
    }
    if (svs.nextCachedSnapshotClients >=
        SV_FRAME_MAX_CACHED_SNAPSHOT_CLIENTS) {
        coduomp_sv_restart_map_after_counter_wrap(
            "EXE_SERVERRESTARTMISC\x15nextCachedSnapshotClients");
        return;
    }
    if (svs.nextArchivedSnapshotFrames >=
        SV_FRAME_MAX_ARCHIVED_SNAPSHOT_FRAMES) {
        coduomp_sv_restart_map_after_counter_wrap(
            "EXE_SERVERRESTARTMISC\x15nextArchivedSnapshotFrames");
        return;
    }
    if (svs.nextArchivedSnapshotBuffer >=
        SV_FRAME_MAX_ARCHIVED_SNAPSHOT_BUFFER) {
        coduomp_sv_restart_map_after_counter_wrap(
            "EXE_SERVERRESTARTMISC\x15nextArchivedSnapshotBuffer");
        return;
    }
    if (svs.nextCachedSnapshotFrames >=
        SV_FRAME_MAX_CACHED_SNAPSHOT_FRAMES) {
        coduomp_sv_restart_map_after_counter_wrap(
            "EXE_SERVERRESTARTMISC\x15nextCachedSnapshotFrames");
        return;
    }
    if (svs.nextClientSnapshot >=
        SV_FRAME_MAX_SNAPSHOT_COUNTER - svs.numClientSnapshots) {
        coduomp_sv_restart_map_after_counter_wrap(
            "EXE_SERVERRESTARTMISC\x15numSnapshotClients");
        return;
    }

    XAnimSetUser(XANIM_SECONDARY_POOL_PAYLOAD_SLOT);
    if ((cvar_modifiedFlags & SV_FRAME_SERVERINFO_CONFIG_MASK) != 0) {
        SV_SetConfigstring(CODUO_HOST_CONFIGSTRING_SERVERINFO_INDEX,
                           Cvar_InfoString(SV_FRAME_SERVERINFO_CONFIG_MASK));
        cvar_modifiedFlags &= ~SV_FRAME_SERVERINFO_CONFIG_MASK;
    }
    if ((cvar_modifiedFlags & SV_FRAME_SYSTEMINFO_CONFIG_MASK) != 0) {
        SV_SetConfigstring(CODUO_HOST_CONFIGSTRING_SYSTEMINFO_INDEX,
                           Cvar_InfoString_Big(SV_FRAME_SYSTEMINFO_CONFIG_MASK));
        cvar_modifiedFlags &= ~SV_FRAME_SYSTEMINFO_CONFIG_MASK;
    }
    if ((cvar_modifiedFlags & SV_FRAME_SCRIPT_SERVERINFO_CONFIG_MASK) != 0) {
        Cvar_SetConfigstringValues(CODUO_HOST_CONFIGSTRING_SYSTEMINFO_KV_BASE,
                                   CODUO_HOST_CONFIGSTRING_SYSTEMINFO_KV_COUNT,
                                   SV_FRAME_SCRIPT_SERVERINFO_CONFIG_MASK);
        cvar_modifiedFlags &= ~SV_FRAME_SCRIPT_SERVERINFO_CONFIG_MASK;
    }

    SV_RunBotFrame();
    if (host_cvar_com_speeds_pointer_slot->integer != 0) {
        frameStartTime = Sys_Milliseconds();
    }

    SV_CalcPings();
    scaledFrameMsec = Com_ModifyMsec(frameMsec);
    for (;;) {
        host_sv_timeResidual -= frameMsec;
        svs.timeSecondary += frameMsec;
        svs.time += scaledFrameMsec;
        SV_RunGameFrame();
        Scr_SetLoading(qfalse);
        if (frameMsec < 1 || host_sv_timeResidual < frameMsec) {
            break;
        }
        SV_ArchiveSnapshot();
    }

    if (host_cvar_com_speeds_pointer_slot->integer != 0) {
        com_timeGame = Sys_Milliseconds() - frameStartTime;
    }

    SV_CheckTimeouts();
    SV_SendClientMessages();
    if (host_cvar_timescale_pointer_slot->value > 0.0f) {
        SV_ArchiveSnapshot();
    }
    SV_MasterHeartbeat("COD-1");
}
