/*H********************************************************************************/
/*!
    \File netgamedistserv.c

    \Description
        Server module to handle 2+ NetGameDist connections in a client/server
        architecture.

    \Copyright
        Copyright (c) 2007 Electronic Arts Inc.

    \Version 02/01/2007 (jbrookes) First Version
*/
/********************************************************************************H*/

/*** Include files ****************************************************************/

#include <string.h>

#include "DirtySDK/dirtysock.h"
#include "DirtySDK/dirtysock/dirtymem.h"
#include "DirtySDK/game/netgamepkt.h"
#include "DirtySDK/game/netgamedistserv.h"

/*** Defines **********************************************************************/

#define MONITOR_HIGHWATER               (0)
#define DISTSERV_MAX_MULTIPACKET_LENGTH (1100)
#define DISTSERV_MAX_FIXEDRATE_MULTIPLE (3)
#define DISTSERV_DEFAULT_FIXEDRATE      (33)

/*** Type Definitions *************************************************************/

//! client state
typedef struct NetGameDistServClientT
{
    NetGameDistRefT *pGameDist;         //!< client dist ref
    char strName[32];                   //!< client name
    uint8_t aLocalInput[NETGAME_DATAPKT_MAXSIZE]; //!< local input buffer (TODO - this will be a queue)
    int32_t iLocalInputSize;            //!< size of input data
    int32_t iPeekKey;                   //!< delta at the time of our peek.
    uint8_t bLocalInput;                //!< whether we have local input or not
    uint8_t iLocalInputType;            //!< type of input
    uint8_t bInitialized;               //!< true if client is initialized
    uint8_t bDisconnected;              //!< whether the client is disconnected or not
    int32_t iDisconnectReason;          //!< why the disconnect
    int32_t iCount;                     //!< total number of packets received
    int32_t iCRC;                       //!< latest client-side CRC received
    uint8_t uCRCValid;                  //!< whether we got a CRC response to the last request
    uint8_t bRemoteRcv;                 //!< last 'rrcv' value (is client ready to recv)
    uint8_t bRemoteSnd;                 //!< last 'rsnd' value (is client ready to send)
    uint8_t bReallyPeeked;              //!< indicate whether we peeked  this client before InputLocalMulti this frame (to address delayed input scenarios)
    uint32_t uNakSent;                  //!< previous nak sent stat
    uint32_t uPackLost;                 //!< previous packet loss stat
    char strDiscReason[GMDIST_ERROR_SIZE];
} NetGameDistServClientT;

//! client list
typedef struct NetGameDistServClientListT
{
    int32_t iNumClients;                //!< current number of clients in list
    int32_t iMaxClients;                //!< max number of clients in list
    NetGameDistServClientT Clients[1];  //!< variable-length array of clients
} NetGameDistServClientListT;

//! module state
struct NetGameDistServT
{
    //! module memory group
    int32_t iMemGroup;
    void *pMemGroupUserData;

    //! verbose debug output level
    int32_t iVerbosity;

    //! Last stats update time
    uint32_t uLastStatsUpdate;

    // debugging
    uint32_t aPeekTimes[32];
    uint32_t aRecvTimes[32];
    uint32_t aSendTimes[32];
    uint32_t aEscalate[32];
    int32_t iFixedRate;

    uint32_t uNbFramesNoData;

    uint32_t uCRCRate;                  //!< number of frames between challenges
    uint32_t uCRCResponseLimit;         //!< number of frames allowed to receive response
    uint32_t uCRCRemainingFrames;       //!< number of frames remaining until next challenge
    uint32_t uCRCResponseCountdown;     //!< number of frames remaining to accept CRC responses

    uint8_t uSendThreshold;

    #if MONITOR_HIGHWATER
    int32_t iHighWaterInputQueue;
    int32_t iHighWaterOutputQueue;
    uint8_t uHighWaterChanged;
    #endif

    int32_t iFirstClient;
    uint32_t uLastFlowUpdateTime;
    uint32_t uOutputMultiPacketCount;  //!< total output dist multi packet
    uint32_t uCurStatClientCount;      //!< current stat client count
    uint8_t bClientCountChanged;       //!< did the client count change between stat sampling interval?
    uint8_t bFlowEnabled;              //!< whether all clients are ready to receive.
    uint8_t bFlowEnabledChanged;       //!< did flow control change between stat sampling interval?
    uint8_t bNoInputMode;              //!< TRUE means: outbound multipackets are no longer generated because inbound packets have not been coming in for multiple frames
    uint8_t bNoInputModeChanged;       //!< did no input mode change between stat sampling interval?
    uint8_t uMultiPacketVersion;       //!< sparse multi-packet version number. Changes when player list changes
    uint8_t _pad[2];

    NetGameDistServLoggingCbT *pLoggingCb;  //!< logging callback
    void *pUserData;                        //!< logging userdata

    //! variable-length array of clients -- must come last!
    NetGameDistServClientListT ClientList;
};

/*** Variables ********************************************************************/

/*** Private Functions ************************************************************/

/*F********************************************************************************/
/*!
    \Function _NetGameDistServLogPrintf

    \Description
        Log printing for netgamedistserv module

    \Input *pDistServ   - module state
    \Input *pFormat     - printf format string
    \Input ...          - variable argument list

    \Version 06/26/2019 (eesponda)
*/
/********************************************************************************F*/
static void _NetGameDistServLogPrintf(NetGameDistServT *pDistServ, const char *pFormat, ...)
{
    char strText[2048];
    int32_t iOffset = 0;
    va_list Args;

    // format prefix
    iOffset += ds_snzprintf(strText+iOffset, sizeof(strText)-iOffset, "netgamedistserv: ");

    // format output
    va_start(Args, pFormat);
    iOffset += ds_vsnprintf(strText+iOffset, sizeof(strText)-iOffset, pFormat, Args);
    va_end(Args);

    // forward to callback if registered
    if ((pDistServ != NULL) && (pDistServ->pLoggingCb != NULL))
    {
        pDistServ->pLoggingCb(strText, pDistServ->pUserData);
    }
    else
    {
        NetPrintf(("%s", strText));
    }
}

/*F********************************************************************************/
/*!
    \Function _NetGameDistServLogPrintfVerbose

    \Description
        Log printing for netgamedistserv module at varying verbosity levels

    \Input *pDistServ   - module state
    \Input iCheckLevel  - the level we are checking our internal iVerbosity against
    \Input *pFormat     - printf format string
    \Input ...          - variable argument list

    \Version 06/26/2019 (eesponda)
*/
/********************************************************************************F*/
static void _NetGameDistServLogPrintfVerbose(NetGameDistServT *pDistServ, int32_t iCheckLevel, const char *pFormat, ...)
{
    char strText[2048];
    va_list Args;

    // no-op
    if (pDistServ->iVerbosity <= iCheckLevel)
    {
        return;
    }

    // format output
    va_start(Args, pFormat);
    ds_vsnprintf(strText, sizeof(strText), pFormat, Args);
    va_end(Args);

    // format to the logging function
    _NetGameDistServLogPrintf(pDistServ, "%s", strText);
}

/*F********************************************************************************/
/*!
    \Function _NetGameDistServSanityCheckIn

    \Description
        Perform sanity checking on input.

    \Input *pDistServ   - module state
    \Input iClient      - client to check
    \Input iRet         - result from NetGameDistInputPeek()

    \Version 01/01/2007 (jrainy)
*/
/********************************************************************************F*/
static void _NetGameDistServSanityCheckIn(NetGameDistServT *pDistServ, int32_t iClient, int32_t iRet)
{
    int32_t iDelay;
    uint32_t uCurTick;

    uCurTick = NetTick();

    // no need to perform sanity checking on input when flow is disabled
    if (pDistServ->bFlowEnabled)
    {
        iDelay = uCurTick - pDistServ->aRecvTimes[iClient];
        if (iRet > 0)
        {
            pDistServ->aRecvTimes[iClient] = uCurTick;

            if (iDelay < 200)
            {
                pDistServ->aEscalate[iClient] = 0;
            }
        }
        if ((iDelay > 200) && pDistServ->aRecvTimes[iClient] && (pDistServ->aEscalate[iClient] < 1))
        {
            _NetGameDistServLogPrintfVerbose(pDistServ, 0, "no input in 200 ms from client %d\n", iClient);
            pDistServ->aEscalate[iClient] = 1;
        }
        else if ((iDelay > 500) && pDistServ->aRecvTimes[iClient] && (pDistServ->aEscalate[iClient] < 2))
        {
            _NetGameDistServLogPrintfVerbose(pDistServ, 0, "no input in 500 ms from client %d\n", iClient);
            pDistServ->aEscalate[iClient] = 2;
        }
        else if ((iDelay > 1000) && pDistServ->aRecvTimes[iClient])
        {
            _NetGameDistServLogPrintfVerbose(pDistServ, 0, "no input in 1000 ms from client %d\n", iClient);
            pDistServ->aRecvTimes[iClient] = uCurTick;
            pDistServ->aEscalate[iClient] = 0;
        }

        iDelay = uCurTick - pDistServ->aPeekTimes[iClient];
        pDistServ->aPeekTimes[iClient] = uCurTick;
        if (iDelay > (pDistServ->iFixedRate * DISTSERV_MAX_FIXEDRATE_MULTIPLE))
        {
            _NetGameDistServLogPrintfVerbose(pDistServ, 0, "no peek in %d ms from client %d\n", iDelay, iClient);
        }
    }
}

/*F********************************************************************************/
/*!
    \Function _NetGameDistServSanityCheckOut

    \Description
        Perform sanity checking on output.

    \Input *pDistServ   - module state
    \Input iClient      - client to check
    \Input iRet         - result from NetGameDistInputLocalMulti()

    \Version 01/01/2007 (jrainy)
*/
/********************************************************************************F*/
static void _NetGameDistServSanityCheckOut(NetGameDistServT *pDistServ, int32_t iClient, int32_t iRet)
{
    int32_t iDelay;
    uint32_t uCurTick;

    uCurTick = NetTick();

    iDelay = uCurTick - pDistServ->aSendTimes[iClient];
    if (iRet == 1)
    {
        pDistServ->aSendTimes[iClient] = uCurTick;
    }
    if ((iDelay > (pDistServ->iFixedRate * DISTSERV_MAX_FIXEDRATE_MULTIPLE)) && pDistServ->aSendTimes[iClient])
    {
        _NetGameDistServLogPrintfVerbose(pDistServ, 0, "no send in %d ms to client %d\n", iDelay, iClient);
        pDistServ->aSendTimes[iClient] = uCurTick;
    }
}

/*F********************************************************************************/
/*!
    \Function _NetGameDistServFlowControl

    \Description
        Send flow control update from the server as appropriate

    \Input *pDistServ   - module state

    \Version 12/03/2007 (jrainy)
*/
/********************************************************************************F*/
static void _NetGameDistServFlowControl(NetGameDistServT *pDistServ)
{
    int32_t iClient;
    NetGameDistServClientT *pDistClient;
    uint8_t bPrevFlowEnabled;          //!< previous reported stat flow enabled boolean 

    bPrevFlowEnabled = pDistServ->bFlowEnabled;
    pDistServ->bFlowEnabled = TRUE;

    for (iClient = 0; iClient < pDistServ->ClientList.iMaxClients; iClient++)
    {
        pDistClient = &pDistServ->ClientList.Clients[iClient];

        if (pDistClient->bInitialized && !pDistClient->bDisconnected)
        {
            if (!pDistClient->bRemoteRcv)
            {
                pDistServ->bFlowEnabled = FALSE;
            }
        }
    }

    _NetGameDistServLogPrintf(pDistServ, "sending flow update lrcv %d\n", pDistServ->bFlowEnabled);
    for (iClient = 0; iClient < pDistServ->ClientList.iMaxClients; iClient++)
    {
        pDistClient = &pDistServ->ClientList.Clients[iClient];

        if (pDistClient->bInitialized && !pDistClient->bDisconnected)
        {
            NetGameDistControl(pDistClient->pGameDist, 'lrcv', pDistServ->bFlowEnabled, NULL);
        }
    }

    /* If flow control changed, flag the boolean used to remember that it happened at least once
       during the metrics sampling interval. */
    if (bPrevFlowEnabled != pDistServ->bFlowEnabled)
    {
        if (pDistServ->bFlowEnabledChanged == FALSE)
        {
            _NetGameDistServLogPrintfVerbose(pDistServ, 1, "metric sampling interval marked invalid for idpps and inputdrop because flow control changed (flow enabled = %d)\n", pDistServ->bFlowEnabled);
        }
        pDistServ->bFlowEnabledChanged = TRUE;
    }
}

/*F********************************************************************************/
/*!
    \Function _NetGameDistServDiscClient

    \Description
        Destroy game dist refs.

    \Input *pDistServ       - module state
    \Input *pDistClient     - client to disconnect from
    \Input iDistClient      - index of client
    \Input iReason          - disconnection reason

    \Version 04/26/2007 (jbrookes)
*/
/********************************************************************************F*/
static void _NetGameDistServDiscClient(NetGameDistServT *pDistServ, NetGameDistServClientT *pDistClient, int32_t iDistClient, int32_t iReason)
{
    // mark as disconnected
    if (!pDistClient->bDisconnected)
    {
        _NetGameDistServLogPrintf(pDistServ, "disconnecting from client %d\n", iDistClient);
        pDistClient->bDisconnected = TRUE;
        pDistClient->iDisconnectReason = iReason;
    }
    else if (pDistClient->pGameDist != NULL)
    {
        _NetGameDistServLogPrintf(pDistServ, "warning -- client %d has gamedist but is disconnected\n", iDistClient);
    }

    // destroy dist
    if (pDistClient->pGameDist != NULL)
    {
        NetGameDistGetErrorText(pDistClient->pGameDist, pDistClient->strDiscReason, sizeof(pDistClient->strDiscReason));

        _NetGameDistServLogPrintfVerbose(pDistServ, 0, "deleting dist ref for client %s/%d\n", pDistClient->strName, iDistClient);
        NetGameDistDestroy(pDistClient->pGameDist);
        pDistClient->pGameDist = NULL;
    }

    _NetGameDistServFlowControl(pDistServ);
}

/*F********************************************************************************/
/*!
    \Function _NetGameDistUpdateMulti

    \Description
        Update Multi configuration when a client is added or removed

    \Input *pDistServ   - module state

    \Version 02/13/2007 (jbrookes)
*/
/********************************************************************************F*/
static void _NetGameDistUpdateMulti(NetGameDistServT *pDistServ)
{
    NetGameDistServClientT *pDistClient;
    int32_t iClient;
    int32_t iLastClient = 0;
    uint32_t uMask = 0;

    for (iClient = 0; iClient < pDistServ->ClientList.iMaxClients; iClient++)
    {
        pDistClient = &pDistServ->ClientList.Clients[iClient];
        if (pDistClient->bInitialized)
        {
            iLastClient = iClient;

            // set the bits in the mask for all initialized players
            uMask |= (1 << iClient);
        }
    }

    // increment the multipacket version
    pDistServ->uMultiPacketVersion++;

    for (iClient = 0; iClient < pDistServ->ClientList.iMaxClients; iClient++)
    {
        pDistClient = &pDistServ->ClientList.Clients[iClient];
        if (pDistClient->pGameDist != NULL)
        {
            _NetGameDistServLogPrintf(pDistServ, "issuing NetGameDistMultiSetup %d %d\n", iClient, iLastClient + 1);
            NetGameDistMultiSetup(pDistClient->pGameDist, iClient, iLastClient + 1);

            _NetGameDistServLogPrintf(pDistServ, "issuing NetGameDistMetaSetup 1 0x%0x %d\n", uMask, pDistServ->uMultiPacketVersion);
            NetGameDistMetaSetup(pDistClient->pGameDist, 1, uMask, pDistServ->uMultiPacketVersion);
        }
    }
}

/*F********************************************************************************/
/*!
    \Function _NetGameDistServDrop

    \Description
        Default implementation of drop function.

    \Input *pLinkRef        - module state
    \Input *pExisting       - input packet at end of the input queue
    \Input *pIncoming       - newly received input packet
    \Input uTypeExisting    - type of the packet in the queue
    \Input uTypeIncoming    - type of the new packet

    \Output
        int8_t              - TRUE if the input packet can be dropped, FALSE otherwise

    \Version 02/27/2007 (jrainy)
*/
/********************************************************************************F*/
static int8_t _NetGameDistServDrop(void *pLinkRef, void *pExisting, void *pIncoming, uint8_t uTypeExisting, uint8_t uTypeIncoming)
{
    return(uTypeExisting == GMDIST_DATA_INPUT_DROPPABLE);
}

/*F********************************************************************************/
/*!
    \Function _NetGameDistServHandleCRCResponses

    \Description
        Handle the CRC responses. Check the current response status of all clients

    \Input *pDistServ   - module state

    \Notes
        Can only compare 64 different CRCs. If we have over 64 clients, the function
        will still function correctly, as long as there is more than 64 different
        CRCs being returned by the clients (highly unlikely). If there's too many
        different CRCs received, we'll pick the one with majority from the 64 first
        clients.

    \Version 03/23/2009 (jrainy)
*/
/********************************************************************************F*/
static void _NetGameDistServHandleCRCResponses(NetGameDistServT *pDistServ)
{
    NetGameDistServClientT *pDistClient;
    int32_t iClient,iIndex;
    uint8_t uWaiting = FALSE;
    uint8_t uTied = FALSE;
    int32_t iCRCs[64] = {0};
    int32_t iOccurences[64] = {0};
    int32_t iBestCRC = 0;
    int32_t iBestOccurences = 0;

    // for all the current clients
    for (iClient = 0; iClient < pDistServ->ClientList.iMaxClients; iClient++)
    {
        pDistClient = &pDistServ->ClientList.Clients[iClient];

        // skip uninitialized/disconnected clients
        if (!pDistClient->bInitialized || pDistClient->bDisconnected)
        {
            continue;
        }

        // mark us waiting if this client didn't send its CRC yet
        if (!pDistClient->uCRCValid)
        {
            uWaiting = TRUE;
        }
        else
        {
            // For all the CRC seen already
            for(iIndex = 0; iIndex < 64; iIndex++)
            {
                // count the repeats
                if (pDistClient->iCRC == iCRCs[iIndex])
                {
                    iOccurences[iIndex]++;

                    if (iOccurences[iIndex] > iBestOccurences)
                    {
                        iBestOccurences = iOccurences[iIndex];
                        iBestCRC = iCRCs[iIndex];
                    }

                    break;
                }
                else if (iOccurences[iIndex] == 0)
                {
                    // remember the newly seen CRCs
                    iCRCs[iIndex] = pDistClient->iCRC;
                    iOccurences[iIndex] = 1;

                    if (iOccurences[iIndex] > iBestOccurences)
                    {
                        iBestOccurences = iOccurences[iIndex];
                        iBestCRC = iCRCs[iIndex];
                    }

                    break;
                }
            }
        }
    }

    // if the wait is over, (received all responses, or number of frames)
    if (!uWaiting || (pDistServ->uCRCResponseCountdown == 0))
    {
        for(iIndex = 0; iIndex < 64; iIndex++)
        {
            if ((iOccurences[iIndex] == iBestOccurences) && (iCRCs[iIndex] != iBestCRC))
            {
                uTied = TRUE;
            }
        }

        for (iClient = 0; iClient < pDistServ->ClientList.iMaxClients; iClient++)
        {
            pDistClient = &pDistServ->ClientList.Clients[iClient];

            // skip disconnected clients
            if (!pDistClient->bInitialized || pDistClient->bDisconnected)
            {
                continue;
            }

            if (!pDistClient->uCRCValid || (pDistClient->iCRC != iBestCRC) || uTied)
            {
                _NetGameDistServLogPrintf(pDistServ, "client %d failed CRC challenge: %d %d %d %d\n", iClient, pDistClient->uCRCValid, pDistClient->iCRC, iBestCRC, uTied);

                if (uTied)
                {
                    _NetGameDistServDiscClient(pDistServ, pDistClient, iClient, GMDIST_DESYNCED_ALL_PLAYERS);
                }
                else
                {
                    _NetGameDistServDiscClient(pDistServ, pDistClient, iClient, GMDIST_DESYNCED);
                }
            }
            else
            {
                _NetGameDistServLogPrintfVerbose(pDistServ, 2, "client %d passed CRC challenge: %d\n", iClient, pDistClient->iCRC);
            }

            pDistClient->uCRCValid = FALSE;
            pDistClient->iCRC = 0;
        }

        // go back to challenge mode
        pDistServ->uCRCResponseCountdown = 0;
        pDistServ->uCRCRemainingFrames = pDistServ->uCRCRate;
    }
}

/*F********************************************************************************/
/*!
    \Function _NetGameDistServPrepareInputs

    \Description
        Prepare the inputs for sending a multipacket

    \Input *pDistServ       - module state
    \Input **pInputs       - array of pointers to set to data buffers
    \Input *pInputSizes    - array of sizes to set
    \Input *pInputTypes    - array of types to set
    \Input *pInputUsed     - array of booleans, indicates whether a given player's input was used
    \Input *pInputDropped  - array of booleans, indicates whether a given player's input was dropped

    \Output
        uint8_t             - indicates whether any data was available and prepared

    \Version 09/22/2009 (jrainy)
*/
/********************************************************************************F*/
static uint8_t _NetGameDistServPrepareInputs(NetGameDistServT *pDistServ, void** pInputs, int32_t* pInputSizes, uint8_t* pInputTypes, uint8_t* pInputUsed, uint8_t* pInputDropped)
{
    int32_t iClient, iIndex;
    int32_t iRunningLength, iRunningLengthDroppable;
    int32_t iAllowed, iAllowedDroppable;
    uint8_t bDataAvailable;

    NetGameDistServClientT *pDistClient;

    for (iClient = 0; iClient < pDistServ->ClientList.iMaxClients; iClient++)
    {
        pInputSizes[iClient] = 0;
        pInputTypes[iClient] = GMDIST_DATA_NONE;
        pInputUsed[iClient] = FALSE;
    }

    iRunningLength = 0;
    iRunningLengthDroppable = 0;

    for (iIndex = 0, bDataAvailable = FALSE; iIndex < pDistServ->ClientList.iMaxClients; iIndex++)
    {
        iClient = (iIndex + pDistServ->iFirstClient) % pDistServ->ClientList.iMaxClients;
        pDistClient = &pDistServ->ClientList.Clients[iClient];

        if (pDistClient->bLocalInput)
        {
            if (pDistClient->iLocalInputType == GMDIST_DATA_INPUT_DROPPABLE)
            {
                iRunningLengthDroppable += pDistClient->iLocalInputSize;
            }
            else
            {
                if (iRunningLength + pDistClient->iLocalInputSize < DISTSERV_MAX_MULTIPACKET_LENGTH)
                {
                    iRunningLength += pDistClient->iLocalInputSize;
                }
            }
        }
    }

    iAllowed = iRunningLength;
    iAllowedDroppable = DISTSERV_MAX_MULTIPACKET_LENGTH - iAllowed;
    iRunningLength = 0;
    iRunningLengthDroppable = 0;

    for (iIndex = 0, bDataAvailable = FALSE; iIndex < pDistServ->ClientList.iMaxClients; iIndex++)
    {
        iClient = (iIndex + pDistServ->iFirstClient) % pDistServ->ClientList.iMaxClients;
        pDistClient = &pDistServ->ClientList.Clients[iClient];
        pInputDropped[iClient] = FALSE;

        pInputs[iClient] = pDistClient->aLocalInput;
        if (!pDistClient->bInitialized || pDistClient->bDisconnected)
        {
            pInputSizes[iClient] = 0;
            pInputTypes[iClient] = GMDIST_DATA_DISCONNECT;
        }
        else if (pDistClient->bLocalInput)
        {
            if (pDistClient->iLocalInputType == GMDIST_DATA_INPUT_DROPPABLE)
            {
                iRunningLengthDroppable += pDistClient->iLocalInputSize;
                if (iRunningLengthDroppable > iAllowedDroppable)
                {
                    pInputDropped[iClient] = TRUE;

                    _NetGameDistServLogPrintfVerbose(pDistServ, 1, "dropping packet for iClient %d, iIndex %d\n", iClient, iIndex);
                    continue;
                }
            }
            else
            {
                iRunningLength += pDistClient->iLocalInputSize;
                if (iRunningLength > iAllowed)
                {
                    _NetGameDistServLogPrintfVerbose(pDistServ, 1, "delaying packet for iClient %d, iIndex %d\n", iClient, iIndex);
                    continue;
                }
            }

            pInputSizes[iClient] = pDistClient->iLocalInputSize;
            pInputTypes[iClient] = pDistClient->iLocalInputType;
            bDataAvailable = TRUE;
        }

        pInputUsed[iClient] = TRUE;

        pDistClient->iCount += pDistClient->iPeekKey;
    }

    return(bDataAvailable);
}

/*** Public functions *************************************************************/


/*F********************************************************************************/
/*!
    \Function NetGameDistServCreate

    \Description
        Create the NetGameDistServ module.

    \Input iMaxClients      - maximum number of clients supported
    \Input iVerbosity       - debug verbose level

    \Output
        NetGameDistServT *  - module state, or NULL if create failed

    \Version 02/01/2007 (jbrookes)
*/
/********************************************************************************F*/
NetGameDistServT *NetGameDistServCreate(int32_t iMaxClients, int32_t iVerbosity)
{
    NetGameDistServT *pDistServ;
    int32_t iMemSize;
    int32_t iMemGroup;
    void *pMemGroupUserData;

    // Query current mem group data
    DirtyMemGroupQuery(&iMemGroup, &pMemGroupUserData);

    // calculate memory size based on number of clients
    iMemSize = sizeof(*pDistServ) + (sizeof(pDistServ->ClientList.Clients[0]) * (iMaxClients - 1));

    // allocate and init module state
    if ((pDistServ = DirtyMemAlloc(iMemSize, NETGAMEDISTSERV_MEMID, iMemGroup, pMemGroupUserData)) == NULL)
    {
        _NetGameDistServLogPrintf(NULL, "could not allocate module state\n");
        return(NULL);
    }
    ds_memclr(pDistServ, iMemSize);
    pDistServ->iMemGroup = iMemGroup;
    pDistServ->pMemGroupUserData = pMemGroupUserData;

    // init other module state
    pDistServ->ClientList.iMaxClients = iMaxClients;
    pDistServ->iVerbosity = iVerbosity;
    pDistServ->uSendThreshold = 4;
    pDistServ->uLastFlowUpdateTime = NetTick();
    pDistServ->uCRCResponseCountdown = 0;
    pDistServ->uCRCRemainingFrames = pDistServ->uCRCRate;
    pDistServ->iFixedRate = DISTSERV_DEFAULT_FIXEDRATE;

    // return module ref to caller
    return(pDistServ);
}

/*F********************************************************************************/
/*!
    \Function NetGameDistServDestroy

    \Description
        Destroy the NetGameDistServ module.

    \Input *pDistServ   - module state

    \Version 02/01/2007 (jbrookes)
*/
/********************************************************************************F*/
void NetGameDistServDestroy(NetGameDistServT *pDistServ)
{
    DirtyMemFree(pDistServ, NETGAMEDISTSERV_MEMID, pDistServ->iMemGroup, pDistServ->pMemGroupUserData);
}

/*F********************************************************************************/
/*!
    \Function NetGameDistServAddClient

    \Description
        Add a client to the client list

    \Input *pDistServ   - module state
    \Input iClient      - index of slot to add client to
    \Input *pLinkRef    - link ref to create dist with
    \Input *pClientName - name of client

    \Output
        int32_t         - negative=error, else success

    \Version 02/05/2007 (jbrookes)
*/
/********************************************************************************F*/
int32_t NetGameDistServAddClient(NetGameDistServT *pDistServ, int32_t iClient, NetGameLinkRefT *pLinkRef, const char *pClientName)
{
    // ref given client index
    NetGameDistServClientT *pDistClient = &pDistServ->ClientList.Clients[iClient];

    // make sure this slot isn't already taken
    if (pDistClient->bInitialized && !pDistClient->bDisconnected)
    {
        _NetGameDistServLogPrintf(pDistServ, "skipping add of client to slot %d when slot is not empty\n", iClient);
        return(-1);
    }

    // save info and mark as initialized
    ds_memclr(pDistClient, sizeof(*pDistClient));
    ds_strnzcpy(pDistClient->strName, pClientName, sizeof(pDistClient->strName));
    pDistClient->bInitialized = TRUE;

    // create dist ref for client
    _NetGameDistServLogPrintfVerbose(pDistServ, 0, "creating dist ref for client %s\n", pDistClient->strName);
    DirtyMemGroupEnter(pDistServ->iMemGroup, pDistServ->pMemGroupUserData);
    pDistClient->pGameDist = NetGameDistCreate(pLinkRef,
        (NetGameDistStatProc *)NetGameLinkStatus,
        (NetGameDistSendProc *)NetGameLinkSend,
        (NetGameDistRecvProc *)NetGameLinkRecv,
        GMDIST_DEFAULT_BUFFERSIZE_IN * 20,
        GMDIST_DEFAULT_BUFFERSIZE_OUT * 20);
    DirtyMemGroupLeave();

    NetGameDistSetServer(pDistClient->pGameDist, TRUE);
    NetGameDistSetProc(pDistClient->pGameDist, 'drop', (void *)_NetGameDistServDrop);

    // update client count
    pDistServ->ClientList.iNumClients++;

    // this call is required for dirtycast to announce "no longer receiving" upon join-in-progress
    // DirtyCast will reenter the "receiving" state when the joiner explicitly uses NetGameDistControl('lrcv')
    _NetGameDistServFlowControl(pDistServ);

    // update dist multi configuration
    _NetGameDistUpdateMulti(pDistServ);

    // return success
    return(0);
}

/*F********************************************************************************/
/*!
    \Function NetGameDistServDelClient

    \Description
        Delete a client from the client list

    \Input *pDistServ   - module state
    \Input iClient      - index of slot to delete client from

    \Output
        int32_t         - negative=error, else success

    \Version 02/05/2007 (jbrookes)
*/
/********************************************************************************F*/
int32_t NetGameDistServDelClient(NetGameDistServT *pDistServ, int32_t iClient)
{
    // ref given client index
    NetGameDistServClientT *pDistClient;

    // make sure this is a valid index
    if (iClient >= pDistServ->ClientList.iMaxClients)
    {
        _NetGameDistServLogPrintf(pDistServ, "skipping delete of client %d not in dist list\n", iClient);
        return(-1);
    }

    // ref client
    pDistClient = &pDistServ->ClientList.Clients[iClient];

    // disconnect from client
    _NetGameDistServDiscClient(pDistServ, pDistClient, iClient, GMDIST_DELETED);

    // clear slot in client list
    ds_memclr(pDistClient, sizeof(*pDistClient));

    // update client count
    pDistServ->ClientList.iNumClients--;

    // this used to be done only while game was not started
    // with meta-information, we can now do it at any time
    _NetGameDistUpdateMulti(pDistServ);

    // return success
    return(0);
}

/*F********************************************************************************/
/*!
    \Function NetGameDistServDiscClient

    \Description
        Mark the specified client as disconnect. This will prevent sends and update

    \Input *pDistServ   - module state
    \Input iClient      - index of slot holding client to update

    \Output
        int32_t         - negative=error, else success

    \Version 03/15/2007 (jrainy)
*/
/********************************************************************************F*/
int32_t NetGameDistServDiscClient(NetGameDistServT *pDistServ, int32_t iClient)
{
    // make sure this is a valid index
    if (iClient >= pDistServ->ClientList.iMaxClients)
    {
        _NetGameDistServLogPrintf(pDistServ, "skipping disc of client %d not in dist list\n", iClient);
        return(-1);
    }

    // destroy connection refs and mark as disconnected
    _NetGameDistServDiscClient(pDistServ, &pDistServ->ClientList.Clients[iClient], iClient, GMDIST_DISCONNECTED);
    return(TRUE);
}

/*F********************************************************************************/
/*!
    \Function NetGameDistServUpdateClient

    \Description
        Update the Dist ref for the specified client.

    \Input *pDistServ   - module state
    \Input iClient      - index of slot holding client to update

    \Output
        int32_t         - negative=disconnected, else zero

    \Version 02/05/2007 (jbrookes)
*/
/********************************************************************************F*/
int32_t NetGameDistServUpdateClient(NetGameDistServT *pDistServ, int32_t iClient)
{
    NetGameDistServClientT *pDistClient = &pDistServ->ClientList.Clients[iClient];
    int32_t iDistErr=0;

    // don't update uninitialized slots
    if (!pDistClient->bInitialized)
    {
        return(0);
    }

    // don't update disconnected clients
    if (pDistClient->bDisconnected)
    {
        return(pDistClient->iDisconnectReason ? pDistClient->iDisconnectReason : -1);
    }

    // update client's dist ref
    NetGameDistUpdate(pDistClient->pGameDist);

    // check for error
    iDistErr = NetGameDistGetError(pDistClient->pGameDist);
    if (iDistErr != 0)
    {
        _NetGameDistServLogPrintf(pDistServ, "NetGameDistGetError() returned %d\n", iDistErr);
        _NetGameDistServDiscClient(pDistServ, pDistClient, iClient, iDistErr);
        return(iDistErr);
    }

    // return status
    return(0);
}

/*F********************************************************************************/
/*!
    \Function NetGameDistServUpdate

    \Description
        Update the DistServ module.  This function is expected to be called at the
        desired output rate.

    \Input *pDistServ   - module state

    \Version 02/12/2007 (jrainy)
*/
/********************************************************************************F*/
void NetGameDistServUpdate(NetGameDistServT *pDistServ)
{
    NetGameDistServClientT *pDistClient;
    int32_t iClient, iResult;
    int32_t aInputSizes[GMDIST_MAX_CLIENTS];
    uint8_t aInputTypes[GMDIST_MAX_CLIENTS];
    uint8_t aInputUsed[GMDIST_MAX_CLIENTS];
    uint8_t aInputDropped[GMDIST_MAX_CLIENTS];
    NetGameDistStatT aStats[GMDIST_MAX_CLIENTS];
    NetGameLinkStatT Stat;
    uint8_t bDataAvailable;
    uint8_t bRemoteRcv, bRemoteSnd;
    uint8_t uCRCevent = FALSE;
    uint32_t uSendThreshold;
    void *pInputs[GMDIST_MAX_CLIENTS];

    int32_t iPeekLength[GMDIST_MAX_CLIENTS];
    int32_t iPeekResult[GMDIST_MAX_CLIENTS] = {0};
    uint8_t iType[GMDIST_MAX_CLIENTS];
    static char aLocalInput[GMDIST_MAX_CLIENTS][NETGAME_DATAPKT_MAXSIZE];
    uint8_t uNewFlow[GMDIST_MAX_CLIENTS];
    uint32_t uNumClientsInitConn = 0;
    uint32_t uNow = NetTick();
    uint8_t bPrevNoInputMode;          //!< previous no input mode

    // every 100 sends, send stats to clients
    if ((pDistServ->uLastStatsUpdate == 0) || (NetTickDiff(uNow, pDistServ->uLastStatsUpdate) > 2000))
    {
        pDistServ->uLastStatsUpdate = uNow;

        // gather stats for all connected clients
        for (iClient = 0; iClient < pDistServ->ClientList.iMaxClients; iClient++)
        {
            pDistClient = &pDistServ->ClientList.Clients[iClient];
            if (pDistClient->bInitialized && !pDistClient->bDisconnected)
            {
                NetGameDistStatus(pDistClient->pGameDist, 'stat', 0, &Stat, sizeof(NetGameLinkStatT));
                aStats[iClient].late = SocketHtons(Stat.late);
                aStats[iClient].bps = SocketHtons(Stat.outbps);
                aStats[iClient].pps = (uint8_t)Stat.outpps;
                aStats[iClient].slen = (uint8_t)NetGameDistStatus(pDistClient->pGameDist, 'slen', 0, NULL, 0);
                aStats[iClient].naksent = (uint8_t)(Stat.lnaksent - pDistClient->uNakSent);
                aStats[iClient].plost = (uint8_t)(Stat.lpacklost - pDistClient->uPackLost);
                pDistClient->uNakSent = Stat.lnaksent;
                pDistClient->uPackLost = Stat.lpacklost;
            }
            else
            {
                ds_memclr(&aStats[iClient], sizeof(aStats[0]));
            }
        }

        // broadcast updated stat info to all connected clients
        for (iClient = 0; iClient < pDistServ->ClientList.iMaxClients; iClient++)
        {
            pDistClient = &pDistServ->ClientList.Clients[iClient];
            if(pDistClient->bInitialized && !pDistClient->bDisconnected)
            {
                NetGameDistSendStats(pDistClient->pGameDist, aStats);
            }
        }
    }

    #if MONITOR_HIGHWATER
    for (iClient = 0; iClient < pDistServ->ClientList.iMaxClients; iClient++)
    {
        int32_t iRet;

        pDistClient = &pDistServ->ClientList.Clients[iClient];
        if(pDistClient->bInitialized && !pDistClient->bDisconnected)
        {
            iRet = GMDIST_Modulo(NetGameDistStatus(pDistClient->pGameDist, '?inp', 0, NULL, 0) - NetGameDistStatus(pDistClient->pGameDist, '?cmp', 0, NULL, 0), NetGameDistStatus(NULL, 'pwin', 0, NULL, 0));
            if (iRet > pDistServ->iHighWaterInputQueue)
            {
                pDistServ->iHighWaterInputQueue = iRet;
                pDistServ->uHighWaterChanged = TRUE;
            }

            iRet = GMDIST_Modulo(NetGameDistStatus(pDistClient->pGameDist, '?out', 0, NULL, 0) - NetGameDistStatus(pDistClient->pGameDist, '?snd', 0, NULL, 0), NetGameDistStatus(NULL, 'pwin', 0, NULL, 0));
            if (iRet > pDistServ->iHighWaterOutputQueue)
            {
                pDistServ->iHighWaterOutputQueue = iRet;
                pDistServ->uHighWaterChanged = TRUE;
            }
        }
    }
    #endif

    for (iClient = 0; iClient < pDistServ->ClientList.iMaxClients; iClient++)
    {
        pDistClient = &pDistServ->ClientList.Clients[iClient];

        if (pDistClient->bInitialized && !pDistClient->bDisconnected)
        {
            if (!pDistClient->bLocalInput)
            {
                int32_t iLength = sizeof(pDistClient->aLocalInput);
                // check for incoming data on this dist
                iResult = NetGameDistInputPeek(pDistClient->pGameDist, &pDistClient->iLocalInputType, pDistClient->aLocalInput, &iLength);
                pDistClient->bReallyPeeked = TRUE;
                _NetGameDistServSanityCheckIn(pDistServ, iClient, iResult);

                if (iResult > 0)
                {
                    pDistClient->iLocalInputSize = iLength;
                    pDistClient->bLocalInput = TRUE;
                    pDistClient->iPeekKey = iResult;
                }
                else if (iResult < 0)
                {
                    _NetGameDistServLogPrintf(pDistServ, "peek error for client %d\n", iClient);
                    _NetGameDistServDiscClient(pDistServ, pDistClient, iClient, GMDIST_PEEK_ERROR);
                    continue;
                }
            }

            bRemoteRcv = NetGameDistStatus(pDistClient->pGameDist,'rrcv', 0, NULL, 0);
            bRemoteSnd = NetGameDistStatus(pDistClient->pGameDist,'rsnd', 0, NULL, 0);

            if ((bRemoteRcv != pDistClient->bRemoteRcv) || (bRemoteSnd != pDistClient->bRemoteSnd))
            {
                pDistClient->bRemoteRcv = bRemoteRcv;
                pDistClient->bRemoteSnd = bRemoteSnd;

                _NetGameDistServLogPrintf(pDistServ, "client %d got GAME_PACKET_INPUT_FLOW (send %d, recv %d)\n", iClient, pDistClient->bRemoteSnd, pDistClient->bRemoteRcv);
                _NetGameDistServFlowControl(pDistServ);
                pDistServ->uLastFlowUpdateTime = NetTick();
            }
        }
    }

    bDataAvailable = _NetGameDistServPrepareInputs(pDistServ, pInputs, aInputSizes, aInputTypes, aInputUsed, aInputDropped);

    if (!bDataAvailable)
    {
        pDistServ->uNbFramesNoData++;
    }
    else
    {
        pDistServ->uNbFramesNoData = 0;
    }

    uSendThreshold = pDistServ->uSendThreshold;

    if (!pDistServ->bFlowEnabled)
    {
        uSendThreshold = 1;
    }

    // don't send if there was no data available for a while
    if ((uSendThreshold == 0) || (pDistServ->uNbFramesNoData < uSendThreshold))
    {
        if (pDistServ->uCRCRemainingFrames)
        {
            if (pDistServ->bFlowEnabled)
            {
                pDistServ->uCRCRemainingFrames--;
            }
            if (pDistServ->uCRCRemainingFrames == 0)
            {
                int32_t iClientCount = 0;

                // if we happen to send to only one client, below,
                for (iClient = 0; iClient < pDistServ->ClientList.iMaxClients; iClient++)
                {
                    pDistClient = &pDistServ->ClientList.Clients[iClient];
                    if (pDistClient->bInitialized && (!pDistClient->bDisconnected))
                    {
                        iClientCount++;
                    }
                }

                if (iClientCount <= 1)
                {
                    // don't send CRC request to single players
                    pDistServ->uCRCRemainingFrames = pDistServ->uCRCRate;
                    pDistServ->uCRCResponseCountdown = 0;
                }
                else
                {
                    for (iClient = 0; iClient < pDistServ->ClientList.iMaxClients; iClient++)
                    {
                        aInputTypes[iClient] |= GMDIST_DATA_CRC_REQUEST;
                        _NetGameDistServLogPrintfVerbose(pDistServ, 2, "setting GMDIST_DATA_CRC_REQUEST in aInputTypes[%d]\n", iClient);
                    }

                    pDistServ->uCRCResponseCountdown = pDistServ->uCRCResponseLimit;
                }
            }
        }

        bPrevNoInputMode = pDistServ->bNoInputMode;
        pDistServ->bNoInputMode = FALSE;

        // scan through client list, and queue all inputs for sending
        for (iClient = 0; iClient < pDistServ->ClientList.iMaxClients; iClient++)
        {
            uNewFlow[iClient] = FALSE;

            pDistClient = &pDistServ->ClientList.Clients[iClient];

            // skip disconnected clients
            if (!pDistClient->bInitialized || pDistClient->bDisconnected)
            {
                continue;
            }

            uNumClientsInitConn++;

            if (pDistServ->uCRCResponseCountdown)
            {
                if (NetGameDistStatus(pDistClient->pGameDist, 'rcrc', 0, NULL, 0))
                {
                    pDistClient->iCRC = NetGameDistStatus(pDistClient->pGameDist, 'rcrc', 1, NULL, 0);
                    pDistClient->uCRCValid = TRUE;

                    _NetGameDistServLogPrintfVerbose(pDistServ, 2, "got CRC response %d back from client %d\n", pDistClient->iCRC, iClient);

                    uCRCevent = TRUE;
                }
            }

            if (!pDistClient->bReallyPeeked)
            {
                iPeekLength[iClient] = sizeof(aLocalInput[iClient]);
                iPeekResult[iClient] = NetGameDistInputPeek(pDistClient->pGameDist,  &(iType[iClient]), &(aLocalInput[iClient]), &(iPeekLength[iClient]));

                if (iPeekResult[iClient] > 0)
                {
                    uNewFlow[iClient] = TRUE;
                }
            }

            pDistClient->bReallyPeeked = FALSE;

            // queue input into client
			// FIFA_BEGIN | DIVERGENCE | mmisalucha | 2019-12-24 | Input latency tracking
            if ((iResult = NetGameDistInputLocalMulti(pDistClient->pGameDist, aInputTypes, pInputs, aInputSizes, pDistClient->iPeekKey, NULL)) == 1)
			// FIFA_END
            {
                pDistServ->uOutputMultiPacketCount++;
                _NetGameDistServLogPrintfVerbose(pDistServ, 2, "queued input into client %s\n", pDistClient->strName);
            }
            else
            {
                _NetGameDistServLogPrintf(pDistServ, "NetGameDistInputLocal() failed for client %s (err=%d)\n", pDistClient->strName, iResult);

                if (iResult == GMDIST_INVALID)
                {
                    _NetGameDistServDiscClient(pDistServ, pDistClient, iClient, GMDIST_INPUTLOCAL_FAILED_INVALID);
                }
                else if (iResult == GMDIST_OVERFLOW_MULTI)
                {
                    _NetGameDistServDiscClient(pDistServ, pDistClient, iClient, GMDIST_INPUTLOCAL_FAILED_MULTI);
                }
                else if (iResult == GMDIST_OVERFLOW_WINDOW)
                {
                    _NetGameDistServDiscClient(pDistServ, pDistClient, iClient, GMDIST_INPUTLOCAL_FAILED_WINDOW);
                }
                else
                {
                    _NetGameDistServDiscClient(pDistServ, pDistClient, iClient, GMDIST_INPUTLOCAL_FAILED);
                }
                continue;
            }

            if (aInputUsed[iClient] || aInputDropped[iClient])
            {
                pDistClient->bLocalInput = FALSE;
                pDistClient->iPeekKey = 0;
            }

            // sanity check output
            _NetGameDistServSanityCheckOut(pDistServ, iClient, iResult);

            // advance the state
			// FIFA_BEGIN | DIVERGENCE | mmisalucha | 2019-12-24 | Input latency tracking
            if ((iResult = NetGameDistInputQueryMulti(pDistClient->pGameDist, NULL, NULL, NULL, NULL)) > 0)
			// FIFA_END
            {
                _NetGameDistServLogPrintfVerbose(pDistServ, 2, "sent input %d\n", iResult);
            }
            else if (iResult)
            {
                _NetGameDistServLogPrintf(pDistServ, "NetGameDistInputQueryMulti() failed (err=%d)\n", iResult);
                _NetGameDistServDiscClient(pDistServ, pDistClient, iClient, GMDIST_INPUTQUERY_FAILED);
                continue;
            }

            if (uNewFlow[iClient] && !aInputUsed[iClient])
            {
                _NetGameDistServLogPrintf(pDistServ, "we're getting behind on large packet delaying for client %d\n", iClient);
                _NetGameDistServDiscClient(pDistServ, pDistClient, iClient, GMDIST_INPUTQUERY_FAILED);
                continue;
            }
        }

        /* If the client count changed, flag the boolean used to remember that it happened at least once
           during the metrics sampling interval. */
        if (pDistServ->uCurStatClientCount != uNumClientsInitConn)
        {
            if (pDistServ->bClientCountChanged == FALSE)
            {
                _NetGameDistServLogPrintfVerbose(pDistServ, 1, "metric sampling interval marked invalid for idpps, odmpps and inputdrop because client count changed (client count = %d)\n", pDistServ->uCurStatClientCount);
            }
            pDistServ->bClientCountChanged = TRUE;
            pDistServ->uCurStatClientCount = uNumClientsInitConn;
        }

        for (iClient = 0; iClient < pDistServ->ClientList.iMaxClients; iClient++)
        {
            pDistClient = &pDistServ->ClientList.Clients[iClient];

            if (uNewFlow[iClient])
            {
                pDistClient->iLocalInputType = iType[iClient];
                pDistClient->iLocalInputSize = iPeekLength[iClient];
                pDistClient->iPeekKey = iPeekResult[iClient];
                pDistClient->bLocalInput = TRUE;

                ds_memcpy_s(pDistClient->aLocalInput, sizeof(pDistClient->aLocalInput), aLocalInput[iClient], sizeof(aLocalInput[iClient]));

                _NetGameDistServSanityCheckIn(pDistServ, iClient, iPeekResult[iClient]);
            }
        }

        // if something occured, or we have a active countdown
        if (pDistServ->uCRCResponseCountdown || uCRCevent)
        {
            // countdown
            if (pDistServ->bFlowEnabled && pDistServ->uCRCResponseCountdown)
            {
                pDistServ->uCRCResponseCountdown--;
            }

            // if something occured or we just reached zero.
            if (uCRCevent || (pDistServ->uCRCResponseCountdown == 0))
            {
                _NetGameDistServHandleCRCResponses(pDistServ);
            }
        }
    }
    else
    {
        bPrevNoInputMode = pDistServ->bNoInputMode;
        pDistServ->bNoInputMode = TRUE;
    }

    /* If 'no input mode' changed, flag the boolean used to remember that it happened at least once
       during the metrics sampling interval. */
    if (bPrevNoInputMode != pDistServ->bNoInputMode)
    {
        if (pDistServ->bNoInputModeChanged == FALSE)
        {
            _NetGameDistServLogPrintfVerbose(pDistServ, 1, "metric sampling interval marked invalid for odmpps because no input mode changed (no input mode = %d)\n", pDistServ->bNoInputMode);
        }
        pDistServ->bNoInputModeChanged = TRUE;
    }

    // $$ jrainy -- fixed up the below code to work without collapsing but introduced a very slight
    // side-effect if we have a list with a big empty area (Alice, [Empty], [Empty], [Empty], [Empty], Bob)
    // Bob will be able to get his oversized packets through more easily than Alice.
    // $$ TODO: A better change would be to increment iFirstclient to the next allocated/initialized
    // spot instead of by just 1. (minor for now)
    if (pDistServ->ClientList.iMaxClients != 0)
    {
        pDistServ->iFirstClient = (pDistServ->iFirstClient + 1) % pDistServ->ClientList.iMaxClients;
    }
}

/*F********************************************************************************/
/*!
    \Function NetGameDistServHighWaterChanged

    \Description
        Return whether the highwater mark changed, and the current highwater values.

    \Input *pDistServ               - module state
    \Input pHighWaterInputQueue     - input queue
    \Input pHighWaterOutputQueue    - output queue

    \Output
        uint8_t                     - whether the highwater mark changed since last call

    \Version 06/05/2008 (jrainy)
*/
/********************************************************************************F*/
uint8_t NetGameDistServHighWaterChanged(NetGameDistServT *pDistServ, int32_t* pHighWaterInputQueue, int32_t* pHighWaterOutputQueue)
{
    #if MONITOR_HIGHWATER
    uint8_t bChanged = pDistServ->uHighWaterChanged;

    if (pHighWaterInputQueue != NULL)
    {
        *pHighWaterInputQueue = pDistServ->iHighWaterInputQueue;
    }
    if (pHighWaterOutputQueue != NULL)
    {
        *pHighWaterOutputQueue = pDistServ->iHighWaterOutputQueue;
    }

    pDistServ->uHighWaterChanged = FALSE;
    return(bChanged);
    #else
    return(FALSE);
    #endif
}

/*F********************************************************************************/
/*!
    \Function NetGameDistServExplainError

    \Description
        return the lastest error reported by netgamedist, for client iClient

    \Input *pDistServ           - module state
    \Input iClient              - client to get the error for.

    \Output
        char *                  - the latest netgamedist error

    \Version 08/15/2008 (jrainy)
*/
/********************************************************************************F*/
char* NetGameDistServExplainError(NetGameDistServT *pDistServ, int32_t iClient)
{
    return(pDistServ->ClientList.Clients[iClient].strDiscReason);
}

/*F********************************************************************************/
/*!
    \Function NetGameDistServControl

    \Description
     Control behavior of module.

    \Input *pDistServ   - pointer to module state
    \Input iControl     - status selector
    \Input iValue       - control value
    \Input iValue2      - control value
    \Input *pValue      - control value

    \Notes
        iControl can be one of the following:

        \verbatim
            'crcc' - set the CRC challenge send rate in number of frames (0 to disable)
            'crcr' - set the CRC max response times in number of frames (0 makes it infinite)
            'mpty' - set the number(iValue) of empty frames needed to pause traffic
            'rate' - set the fixed rate the server has been configured to run at via iValue
            'rsta' - reset stat variables (flow control/no input mode bool)
        \endverbatim

    \Version 03/17/2009 (jrainy)
*/
/********************************************************************************F*/
int32_t NetGameDistServControl(NetGameDistServT *pDistServ, int32_t iControl, int32_t iValue, int32_t iValue2, void *pValue)
{
    if (iControl == 'crcc')
    {
        if ((uint32_t)iValue != pDistServ->uCRCRate)
        {
            //set the rate
            pDistServ->uCRCRate = iValue;

            //start counting down
            pDistServ->uCRCRemainingFrames = iValue;
            _NetGameDistServLogPrintf(pDistServ, "setting CRC rate to %d\n", iValue);

        }
        return(0);
    }
    if (iControl == 'crcr')
    {
        pDistServ->uCRCResponseLimit = iValue;
        return(0);
    }
    if (iControl == 'mpty')
    {
        pDistServ->uSendThreshold = iValue;
        return(0);
    }
    if (iControl == 'rate')
    {
        pDistServ->iFixedRate = iValue;
        return(0);
    }
    if (iControl == 'rsta')
    {
        pDistServ->bClientCountChanged = FALSE;
        pDistServ->bFlowEnabledChanged = FALSE;
        pDistServ->bNoInputModeChanged = FALSE;

        return(0);
    }

    return(-1);
}

/*F********************************************************************************/
/*!
    \Function NetGameDistServStatus2

    \Description
        Get status information.

    \Input *pDistServ   - pointer to module state
    \Input iSelect      - status selector
    \Input iValue       - selector specific
    \Input *pBuf        - [out] storage for selector-specific output
    \Input iBufSize     - size of output buffer

    \Output
        int32_t         - selector specific

    \Notes
        iSelect can be one of the following:

        \verbatim
            'clnu' - returns the current client count used for stat (note this not the same as iNumClients in client list)
            'drop' - returns total dropped input packet count in pValue (uint32_t) for client index in iValue, the value is invalid if GameServerDistStatus() return -1;
            'ftim' - flow time. Time (ms) since the last flow control packet was received.
            'icnt' - returns total input dist packet count in pValue (uint32_t) for client index in iValue, the value is invalid if GameServerDistStatus() return -1;
            'ninp' - whether we are currently in the mode where no inputs are sent due to stall in input arrival
            'ocnt' - reutrns output dist multi packet total in pValue (uint32_t), the value is invalid if GameServerDistStatus() return -1;
        \endverbatim

    \Version 09/18/2019 (tcho)
*/
/********************************************************************************F*/
int32_t NetGameDistServStatus2(NetGameDistServT *pDistServ, int32_t iSelect, int32_t iValue, void *pBuf, int32_t iBufSize)
{
    // total number of clients counted in the update loop
    if (iSelect == 'clnu')
    {
        return(pDistServ->uCurStatClientCount);
    }

    // total input packet dropped count for a client or total input dist packet count per client
    if ((iSelect == 'drop') || (iSelect == 'icnt'))
    {
        uint32_t uInputDropCount;
        uint32_t uInputCount;
        NetGameDistServClientT *pDistClient;

        if ((iValue < 0) || (iValue >= pDistServ->ClientList.iMaxClients))
        {
            // invalid index
            return(-1);
        }

        if ((pBuf == NULL) || (iBufSize < (int32_t)sizeof(uint32_t)))
        {
            // invalid output parameter
            return(-2);
        }

        pDistClient = &pDistServ->ClientList.Clients[iValue];
        
        // only report a value if the client is initialized and connected
        if (pDistClient->bInitialized && !pDistClient->bDisconnected)
        {
            if (iSelect == 'drop')
            {
                uInputDropCount = NetGameDistStatus(pDistClient->pGameDist, 'drop', 0, NULL, 0);
                ds_memcpy(pBuf, &uInputDropCount, sizeof(uint32_t));
            }
            
            if (iSelect == 'icnt')
            {
                uInputCount = NetGameDistStatus(pDistClient->pGameDist, 'icnt', 0, NULL, 0);
                ds_memcpy(pBuf, &uInputCount, sizeof(uint32_t));
            }

            if ((pDistServ->bClientCountChanged == TRUE) || (pDistServ->bFlowEnabledChanged == TRUE) || (pDistServ->bFlowEnabled == FALSE))
            {
                /* value copied in pBuf is valid but sampling interval should be ignored because conditions are not met
                   to properly calculate per-client rate of drops or per-client rate of inbound inputs. */
                return(1);
            }

            return(0);
        }
        else
        {
            ds_memclr(pBuf, iBufSize);
            return(-3);
        }
    }

    if (iSelect == 'ftim')
    {
        return(NetTickDiff(NetTick(), pDistServ->uLastFlowUpdateTime));
    }

    if (iSelect == 'ninp')
    {
        return(pDistServ->bNoInputMode);
    }

    // total output multi dist count per server
    if (iSelect == 'ocnt')
    {
        if ((iValue < 0) || (iValue >= pDistServ->ClientList.iMaxClients))
        {
            // invalid index
            return(-1);
        }

        if ((pBuf == NULL) || (iBufSize < (int32_t)sizeof(uint32_t)))
        {
            // invalid output parameter
            return(-2);
        }

        ds_memcpy(pBuf, &pDistServ->uOutputMultiPacketCount, sizeof(uint32_t));

        if ((pDistServ->uCurStatClientCount == 0) || (pDistServ->bClientCountChanged == TRUE) || (pDistServ->bNoInputModeChanged == TRUE) || (pDistServ->bNoInputMode == TRUE))
        {
            /* value copied in pBuf is valid but sampling interval should be ignored because conditions are not met
               to properly calculate rate of outbound multipackets */
            return(1);
        }

        return(0);
    }

    return(-1);
}

/*F********************************************************************************/
/*!
    \Function NetGameDistServStatus

    \Description
        Get status information.

    \Input *pDistServ   - pointer to module state
    \Input iSelect      - status selector
    \Input *pBuf        - [out] storage for selector-specific output
    \Input iBufSize     - size of output buffer

    \Output
        int32_t         - selector specific

    \Notes
        falls through to NetGameDistServStatus2()

    \Version 24/09/2012 (jrainy) first version
*/
/********************************************************************************F*/
int32_t NetGameDistServStatus(NetGameDistServT *pDistServ, int32_t iSelect, void *pBuf, int32_t iBufSize)
{
    return(NetGameDistServStatus2(pDistServ, iSelect, 0, pBuf, iBufSize));
}

/*F********************************************************************************/
/*!
    \Function NetGameDistServSetLoggingCallback

    \Description
        Set the logging callback

    \Input *pDistServ   - pointer to module state
    \Input *pLoggingCb  - logging callback
    \Input *pUserData   - logging userdata

    \Version 06/26/2019 (eesponda)
*/
/********************************************************************************F*/
void NetGameDistServSetLoggingCallback(NetGameDistServT *pDistServ, NetGameDistServLoggingCbT *pLoggingCb, void *pUserData)
{
    pDistServ->pLoggingCb = pLoggingCb;
    pDistServ->pUserData = pUserData;
}
