// ClientConnection.cpp
// copyright 2001 Verant Interactive

//-----------------------------------------------------------------------

#include "FirstConnectionServer.h"
#include "ClientConnection.h"

#include "Archive/ByteStream.h"
#include "ChatServerConnection.h"
#include "CustomerServiceConnection.h"
#include "ConfigConnectionServer.h"
#include "ConnectionServer.h"
#include "GameConnection.h"
#include "SessionApiClient.h"
#include "UnicodeUtils.h"
#include "serverKeyShare/KeyShare.h"
#include "serverNetworkMessages/CentralConnectionServerMessages.h"
#include "serverNetworkMessages/ChatDisconnectAvatar.h"
#include "serverNetworkMessages/GameConnectionServerMessages.h"
#include "serverNetworkMessages/RandomName.h"
#include "serverNetworkMessages/RequestGameServerForLoginMessage.h"
#include "serverNetworkMessages/ValidateAccountMessage.h"
#include "serverNetworkMessages/ValidateCharacterForLoginMessage.h"
#include "serverNetworkMessages/VerifyAndLockName.h"
#include "serverUtility/AdminAccountManager.h"
#include "sharedFoundation/CalendarTime.h"
#include "sharedFoundation/Clock.h"
#include "sharedFoundation/Crc.h"
#include "sharedGame/PlatformFeatureBits.h"
#include "sharedLog/Log.h"
#include "sharedNetwork/Connection.h"
#include "sharedNetworkMessages/ClientCentralMessages.h"
#include "sharedNetworkMessages/ClientPermissionsMessage.h"
#include "sharedNetworkMessages/AppendCommentMessage.h"
#include "sharedNetworkMessages/CancelTicketMessage.h"
#include "sharedNetworkMessages/ChatEnterRoom.h"
#include "sharedNetworkMessages/ChatEnterRoomById.h"
#include "sharedNetworkMessages/ChatEnum.h"
#include "sharedNetworkMessages/ChatOnEnteredRoom.h"
#include "sharedNetworkMessages/ChatPersistentMessageToServer.h"
#include "sharedNetworkMessages/ChatQueryRoom.h"
#include "sharedNetworkMessages/ConnectPlayerMessage.h"
#include "sharedNetworkMessages/CreateTicketMessage.h"
#include "sharedNetworkMessages/GenericValueTypeMessage.h"
#include "sharedNetworkMessages/GetTicketsMessage.h"
#include "sharedNetworkMessages/NewTicketActivityMessage.h"
#include "sharedNetworkMessages/ErrorMessage.h"
#include "sharedNetworkMessages/HeartBeat.h"
#include "Session/LoginAPI/Client.h"
#include "UdpLibrary.h"

#include "sharedFoundation/CrcConstexpr.hpp"

#include <algorithm>

#include "webAPI.h"

using namespace StellaBellum;

//-----------------------------------------------------------------------

namespace ClientConnectionNamespace {
    unsigned long gs_receiveDelayMaxMs = 16384;
}

using namespace ClientConnectionNamespace;




//-----------------------------------------------------------------------


std::map <std::string, uint32> ClientConnection::sm_outgoingBytesMap_Working;  // working stats that will rotate after 1 minute
std::map <std::string, uint32> ClientConnection::sm_outgoingBytesMap_Stats;    // computed stats from the last minute
uint32                          ClientConnection::sm_outgoingBytesMap_Worktime = 0; // time we started filling in the working map



//-----------------------------------------------------------------------

ClientConnection::ClientConnection(UdpConnectionMT *u, TcpClient *t)
        : ServerConnection(u, t), m_accountName(""), m_canCreateRegularCharacter(false),
          m_canCreateJediCharacter(false), m_hasRequestedCharacterCreate(false), m_hasCreatedCharacter(false),
          m_pendingCharacterCreate(nullptr), m_canSkipTutorial(false), m_characterId(NetworkId::cms_invalid),
          m_characterName(), m_startPlayTime(0), m_lastActiveTime(0), m_activePlayTimeDuration(0), m_client(0),
          m_containerId(NetworkId::cms_invalid), m_featureBitsGame(0), m_featureBitsSubscription(0),
          m_hasBeenSentToGameServer(false), m_hasBeenValidated(false), m_hasSelectedCharacter(false), m_isSecure(false),
          m_isAdminAccount(false), m_hasCSLoggedAccountFeatureIds(false), m_suid(0), m_requestedSuid(0),
          m_usingAdminLogin(false), m_targetCoordinates(), m_targetScene(""), m_validatingCharacter(false),
          m_receiveHistoryBytes(0), m_receiveHistoryPackets(0), m_receiveHistoryMs(0), m_receiveLastTimeMs(0),
          m_sendLastTimeMs(0), m_sessionId(""), m_sessionValidated(false), m_connectionServerLag(0), m_gameServerLag(0),
          m_countSpamLimitResetTime(0), m_entitlementTotalTime(0), m_entitlementEntitledTime(0),
          m_entitlementTotalTimeSinceLastLogin(0), m_entitlementEntitledTimeSinceLastLogin(0), m_buddyPoints(0),
          m_sendToStarport(false), m_pendingChatEnterRoomRequests(), m_pendingChatQueryRoomRequests() {
    static const std::string loginTrace("TRACE_LOGIN");
    LOG(loginTrace, ("new ClientConnection"));

    setNoDataTimeout(600000);
}

//-----------------------------------------------------------------------

ClientConnection::~ClientConnection() {
    bool hasBeenKicked = false;

    if (ConnectionServer::getClientConnection(m_suid) == this) {
        ConnectionServer::removeConnectedCharacter(m_suid);
    }
    if (m_client) {
        hasBeenKicked = m_client->hasBeenKicked();
        delete m_client;
        m_client = nullptr;
    }

    // tell Session to stop recording play time for the character
    if (m_hasBeenValidated && m_sessionValidated && ConnectionServer::getSessionApiClient() && (m_lastActiveTime > 0) &&
        ConfigConnectionServer::getSessionRecordPlayTime()) {
        LOG("CustomerService", ("Login:%s calling SessionStopPlay() for %s/%s/%s (%s). Active play time: %s", ClientConnection::describeAccount(this).c_str(), this->getSessionId().c_str(), ConfigConnectionServer::getClusterName(), this->getCharacterName().c_str(), this->getCharacterId().getValueString().c_str(), this->getCurrentActivePlayTimeDuration().c_str()));

        // log total active play time for the session to the balance log
        LOG("GameBalance", ("balancelog:%s calling SessionStopPlay() for %s/%s/%s (%s). Active play time: %s", ClientConnection::describeAccount(this).c_str(), this->getSessionId().c_str(), ConfigConnectionServer::getClusterName(), this->getCharacterName().c_str(), this->getCharacterId().getValueString().c_str(), this->getActivePlayTimeDuration().c_str()));

        ConnectionServer::getSessionApiClient()->stopPlay(*this);
    }

    if (ConnectionServer::getSessionApiClient()) {
        ConnectionServer::getSessionApiClient()->dropClient(this, hasBeenKicked);
    }

    std::map < unsigned
    long, GameClientMessage * > ::const_iterator
    iter;
    for (iter = m_pendingChatEnterRoomRequests.begin(); iter != m_pendingChatEnterRoomRequests.end(); ++iter) {
        delete iter->second;
    }
    m_pendingChatEnterRoomRequests.clear();

    for (iter = m_pendingChatQueryRoomRequests.begin(); iter != m_pendingChatQueryRoomRequests.end(); ++iter) {
        delete iter->second;
    }
    m_pendingChatQueryRoomRequests.clear();

    delete m_pendingCharacterCreate;
    m_pendingCharacterCreate = nullptr;
}


//-----------------------------------------------------------------------

const NetworkId &ClientConnection::getCharacterId() const {
    return m_characterId;
}

//-----------------------------------------------------------------------

const std::string &ClientConnection::getCharacterName() const {
    return m_characterName;
}

//-----------------------------------------------------------------------

std::string ClientConnection::getPlayTimeDuration() const {
    int playTimeDuration = 0;

    if (m_startPlayTime > 0) {
        playTimeDuration = static_cast<int>(::time(nullptr) - m_startPlayTime);
    }

    return CalendarTime::convertSecondsToHMS(static_cast<unsigned int>(playTimeDuration));
}

//-----------------------------------------------------------------------

std::string ClientConnection::getActivePlayTimeDuration() const {
    int activePlayTimeDuration = static_cast<int>(m_activePlayTimeDuration);

    if (m_lastActiveTime > 0) {
        activePlayTimeDuration += static_cast<int>(::time(nullptr) - m_lastActiveTime);
    }

    return CalendarTime::convertSecondsToHMS(static_cast<unsigned int>(activePlayTimeDuration));
}

//-----------------------------------------------------------------------

std::string ClientConnection::getCurrentActivePlayTimeDuration() const {
    int activePlayTimeDuration = 0;

    if (m_lastActiveTime > 0) {
        activePlayTimeDuration = static_cast<int>(::time(nullptr) - m_lastActiveTime);
    }

    return CalendarTime::convertSecondsToHMS(static_cast<unsigned int>(activePlayTimeDuration));
}

//-----------------------------------------------------------------------

void ClientConnection::sendPlayTimeInfoToGameServer() const {
    if (m_client && m_client->getGameConnection()) {
        // update the game server with play time info
        GenericValueTypeMessage < std::pair < int32, std::pair < int32, unsigned
        long > > >
        const msgPlayTimeInfo(
        "UpdateSessionPlayTimeInfo", std::make_pair(static_cast<int32>(m_startPlayTime), std::make_pair(static_cast<int32>(m_lastActiveTime), m_activePlayTimeDuration)));

        std::vector <NetworkId> v;
        v.push_back(m_client->getNetworkId());
        GameClientMessage const gcm(v, true, msgPlayTimeInfo);
        m_client->getGameConnection()->send(gcm, true);
    }
}

// ----------------------------------------------------------------------

void ClientConnection::handleSelectCharacterMessage(const SelectCharacter &msg) {
    if (!m_sessionValidated) {

    }

    //Only accept this message from clients who have been validated and
    //haven't already selected.
    if (m_hasSelectedCharacter || m_validatingCharacter || !m_hasBeenValidated || !m_sessionValidated) {
        if (m_hasSelectedCharacter) {
            LOG("TraceCharacterSelection", ("%d cannot select a character because the client has already selected a character", getSUID()));
        }
        if (m_validatingCharacter) {
            LOG("TraceCharacterSelection", ("%d cannot select a character because the client has not yet received validation", getSUID()));
        }
        if (!m_hasBeenValidated) {
            LOG("TraceCharacterSelection", ("%d cannot select a character because the client has not been validated", getSUID()));
        }
        if (!m_sessionValidated) {
            LOG("TraceCharacterSelection", ("%d cannot select a character because the client has not been session validated", getSUID()));
        }

        return;
    }

    m_validatingCharacter = true;

    // The client is picking a character from the list the Login Server gave him.
    // But we don't trust him not to cheat, so we double-check that he really
    // owns the character he selected.

    ValidateCharacterForLoginMessage vclm(getSUID(), msg.getId());
    ConnectionServer::sendToCentralProcess(vclm);
    LOG("TraceCharacterSelection", ("%d selected %s for login. Sending a validation request to CentralServer to verify this client can use this character", getSUID(), msg.getId().getValueString().c_str()));

}

// ----------------------------------------------------------------------

/**
 * We got a message telling us what game server to use for the player (who
 * is in the process of logging in).
 * Connect the player with the game server.
 */
void ClientConnection::handleGameServerForLoginMessage(uint32 serverId) {
    DEBUG_WARNING(serverId == 0, ("Got handleGameServerForLoginMessage with serverId=0.\n"));
    IGNORE_RETURN(sendToGameServer(serverId));
}

//----------------------------------------------------------------------

/**
 * Examine the ID the client sends us.  If it looks OK, send it to Central
 * to be validated.  Central will reply with a list of permissions.
 * @see onIdValidated
 */
void ClientConnection::handleClientIdMessage(const ClientIdMsg &msg) {
    //Only check clients that have not been validated.
    if (m_hasBeenValidated) {
        return;
    }

    DEBUG_FATAL(m_hasSelectedCharacter, ("Trying to validate a client who already has a character selected.\n"));
    bool result = false;
    m_gameBitsToClear = msg.getGameBitsToClear();

    if (msg.getTokenSize() > 0) {
        Archive::ByteStream t(msg.getToken(), msg.getTokenSize());
        Archive::ReadIterator ri(t);
        KeyShare::Token token(ri);
        char sessionId[apiSessionIdWidth];

        if (!ConfigConnectionServer::getValidateStationKey()) {
            // get SUID from token
            result = ConnectionServer::decryptToken(token, m_suid, m_isSecure, m_accountName);
        } else {
            result = ConnectionServer::decryptToken(token, sessionId, m_requestedSuid);
        }

        static const std::string sessURL(ConfigConnectionServer::getSessionURL());

        if (result || strlen(sessionId) != 0) {
            if (ConfigConnectionServer::getValidateStationKey()) {
                bool cont = false;
                StationId apiSuid = 0;

                FATAL(sessURL.empty(), ("Session URL is empty in connection server."));

                const std::string clientIP(getRemoteAddress());
                const std::string sess(sessionId);

                FATAL(clientIP.empty(), ("Remote IP is empty"));

                DEBUG_WARNING(true, ("ConnectionServer::handleClientIdMessage - For ip %s suid is %lu requestedSUID is %lu and session is %s", clientIP.c_str(), m_suid, m_requestedSuid, sess.c_str()));

                webAPI api(sessURL);

                // add our data
                api.addJsonData<std::string>("session_key", sess);

                if (api.submit()) {
                    bool status = api.getNullableValue<bool>("status");

                    if (status) {
                        std::string apiUser = api.getString("user_name");
                        std::string apiIP = api.getString("ip");
                        int expired = api.getNullableValue<int>("expired");

                        if (!ConfigConnectionServer::getUseOldSuidGenerator()) {
                            apiSuid = api.getNullableValue<int>("user_id");
                        } else {
                            if (apiUser.length() > MAX_ACCOUNT_NAME_LENGTH) {
                                apiUser.resize(MAX_ACCOUNT_NAME_LENGTH);
                            }
                            apiSuid = std::hash < std::string > {}(apiUser.c_str());
                        }

                        if (apiIP == clientIP && expired == 0) {
                            m_suid = apiSuid;
                            cont = true;
                        }
                    }
                }

                if (!cont) {
                    LOG("ClientDisconnect", ("SUID %d (%d) passed a bad token to the connections erver. Disconnecting.", m_suid, apiSuid));
                    disconnect();
                    return;
                }
            }

            static const std::string loginTrace("TRACE_LOGIN");
            LOG(loginTrace, ("ClientConnection SUID = %d", m_suid));

            //check for duplicate login
            ClientConnection *oldConnection = ConnectionServer::getClientConnection(m_suid);
            if (oldConnection) {
                //There is already someone connected to this cluster with this suid.
                LOG("Network", ("SUID %d already logged in, disconnecting client.\n", m_suid));

                ConnectionServer::dropClient(oldConnection, "Already Connected");

                disconnect();
                return;
            }

            // verify version
            if (ConfigConnectionServer::getValidateClientVersion() &&
                msg.getVersion() != GameNetworkMessage::NetworkVersionId) {
                std::string strSessionId(sessionId, apiSessionIdWidth);
                strSessionId += '\0';

                const int bufferSize = 255 + apiSessionIdWidth;
                char *buffer = new char[bufferSize];
                snprintf(buffer, bufferSize -
                                 1, "network version mismatch: got (ip=[%s], sessionId=[%s], version=[%s]), required (version=[%s])", getRemoteAddress().c_str(), strSessionId.c_str(), msg.getVersion().c_str(), GameNetworkMessage::NetworkVersionId.c_str());
                buffer[bufferSize - 1] = '\0';

                ConnectionServer::dropClient(this, std::string(buffer));
                disconnect();

                delete[] buffer;

                return;
            }

            // test mode only
            if (!m_suid && !ConfigConnectionServer::getValidateStationKey()) {
                WARNING(true, ("Generating suid from username. This is not safe or secure."));

                m_suid = atoi(m_accountName.c_str());

                if (m_suid == 0) {
                    m_suid = std::hash < std::string > {}(m_accountName.c_str());
                }
            }

            onValidateClient(m_suid, m_accountName, m_isSecure, nullptr, ConfigConnectionServer::getDefaultGameFeatures(), ConfigConnectionServer::getDefaultSubscriptionFeatures(), 0, 0, 0, 0, ConfigConnectionServer::getFakeBuddyPoints());
        }
    } else {
        WARNING(true, ("SUID %d passed a bad token to the connections server (cache issue or hacker?). Disconnecting.", m_suid));

        // They sent us a token that was no good -- either a hack attempt, or
        // possibly it was just too old.
        LOG("ClientDisconnect", ("SUID %d passed a bad token to the connections server (cache issue or hacker?). Disconnecting.", m_suid));
        disconnect();
    }
}

//-----------------------------------------------------------------------

void
ClientConnection::onIdValidated(bool canLogin, bool canCreateRegularCharacter, bool canCreateJediCharacter, bool canSkipTutorial, std::vector <std::pair<NetworkId, std::string>> const &consumedRewardEvents, std::vector <std::pair<NetworkId, std::string>> const &claimedRewardItems) {
    //@todo start session with station.
    //@todo add more permissions to this message as needed.

    // resume character creation
    if (m_pendingCharacterCreate) {
        if (!m_pendingCharacterCreate->getUseNewbieTutorial() && !canSkipTutorial) {
            LOG("TraceCharacterCreation", ("%d failed character creation. The client is not allowed to skip the tutorial", getSUID()));
            // This is probably a hack attempt, because the Client was already told they couldn't skip the tutorial
            LOG("ClientDisconnect", ("Disconnecting %u because they tried to skip the tutorial without permission.\n", getSUID()));
            disconnect();
        } else if (m_pendingCharacterCreate->getJedi() && !canCreateJediCharacter) {
            LOG("TraceCharacterCreation", ("%d failed character creation. The request character type was Jedi, but this client cannot create a Jedi character", getSUID()));
            // This is probably a hack attempt, because the Client was already told they couldn't create a character
            LOG("ClientDisconnect", ("Disconnecting %u because they tried to create a Jedi character without permission.\n", getSUID()));
            disconnect();
        } else if (!m_pendingCharacterCreate->getJedi() && !canCreateRegularCharacter) {
            LOG("TraceCharacterCreation", ("%d failed character creation. The client is not allowed to create any characters", getSUID()));
            // This is probably a hack attempt, because the Client was already told they couldn't create a character
            LOG("ClientDisconnect", ("Disconnecting %u because they tried to create a regular character without permission.\n", getSUID()));
            disconnect();
        } else {
            ConnectionServer::sendToCentralProcess(*m_pendingCharacterCreate);
            LOG("TraceCharacterCreation", ("%d character creation request sent to CentralServer", getSUID()));

            m_hasRequestedCharacterCreate = true;
        }

        delete m_pendingCharacterCreate;
        m_pendingCharacterCreate = nullptr;

        return;
    }

    // Save lists of claimed rewards, which won't be used again until later in the login sequence
    m_consumedRewardEvents = consumedRewardEvents;
    m_claimedRewardItems = claimedRewardItems;


    int level = 0;
    if (AdminAccountManager::isAdminAccount(Unicode::toLower(getAccountName()), level) && (level !=
                                                                                           0)) // Note:  not checking IP, so that owners of god accounts can create characters to play from home without having to erase the characters they use for work
    {
        canLogin = true;
        canCreateRegularCharacter = true;
        canSkipTutorial = true;
        m_isAdminAccount = true;
    }

    ClientPermissionsMessage c(canLogin, canCreateRegularCharacter, canCreateJediCharacter, canSkipTutorial);
    send(c, true);

    DEBUG_REPORT_LOG(true, ("Permissions for %lu:\n", getSUID()));
    DEBUG_REPORT_LOG(canLogin, ("\tcanLogin\n"));
    DEBUG_REPORT_LOG(canCreateRegularCharacter, ("\tcanCreateRegularCharacter\n"));
    DEBUG_REPORT_LOG(canCreateJediCharacter, ("\tcanCreateJediCharacter\n"));
    DEBUG_REPORT_LOG(canSkipTutorial, ("\tcanSkipTutorial\n"));
    DEBUG_REPORT_LOG(!(canLogin || canCreateRegularCharacter || canCreateJediCharacter ||
                       canSkipTutorial), ("\tnone\n"));

    if (canLogin) {
        m_hasBeenValidated = true;
        m_canCreateRegularCharacter = canCreateRegularCharacter;
        m_canCreateJediCharacter = canCreateJediCharacter;
        m_canSkipTutorial = canSkipTutorial;
    } else {
        LOG("TRACE_LOGIN", ("%d does not have permissions to log in", getSUID()));
        LOG("ClientDisconnect", ("Client (SUID %u) does not have permissions to log in. Disconnecting.", getSUID()));
        disconnect();
    }
}

//-----------------------------------------------------------------------

void ClientConnection::onConnectionClosed() {
    ServerConnection::onConnectionClosed();
    static MessageConnectionCallback m("ClientConnectionClosed");
    emitMessage(m);

    LOG("TRACE_LOGIN", ("%d closed connection", getSUID()));
    if (m_client) {
        if (!m_client->hasBeenKicked()) {
            LOG("CustomerService", ("Login:%s Dropped Reason: Client Dropped Connection. Character: %s (%s). Play time: %s. Active play time: %s", describeAccount(this).c_str(), getCharacterName().c_str(), getCharacterId().getValueString().c_str(), getPlayTimeDuration().c_str(), getActivePlayTimeDuration().c_str()));
        }
        ChatServerConnection *chatConnection = m_client->getChatConnection();
        if (chatConnection) {
            ChatDisconnectAvatar m(m_characterId);
            chatConnection->send(m, true);
        }
        // We cannot do this here, as this connection will be deleted on
        // return from this function already.
        //m_client->kick();
    }
}

//-----------------------------------------------------------------------

void ClientConnection::onConnectionOpened() {
    ServerConnection::onConnectionOpened();
    static MessageConnectionCallback m("ClientConnectionOpened");
    emitMessage(m);
    setOverflowLimit(ConfigConnectionServer::getClientOverflowLimit());
}

//-----------------------------------------------------------------------

void ClientConnection::onConnectionOverflowing(const unsigned int bytesPending) {
    char errbuf[1024];
    snprintf(errbuf, sizeof(errbuf), "Connection overflow from server to client, %d bytes. Disconnected.", bytesPending);
    LOG("Network", ("Disconnect: Client connection overflowing. %d bytes pending", bytesPending));

    std::vector < std::pair < std::string, int > > ::const_iterator
    i;
    for (i = m_pendingPackets.begin(); i != m_pendingPackets.end(); ++i) {
        LOG("Network", ("Overflow packets this frame: [%s] %d bytes", i->first.c_str(), i->second));
    }

//	ErrorMessage err(name, desc, false);
//	send(err, true);
    WARNING(true, (errbuf));
    LOG("ClientDisconnect", ("About to drop client (character) %s because the connection is overflowing\n", m_characterName.c_str()));

    snprintf(errbuf, sizeof(errbuf) - 1, "Connection Overflow (bytes pending=%u)", bytesPending);
    errbuf[sizeof(errbuf) - 1] = '\0';
    ConnectionServer::dropClient(this, std::string(errbuf));
}

//-----------------------------------------------------------------------

bool ClientConnection::checkSpamLimit(unsigned int messageSize) {
    if (!ConfigConnectionServer::getSpamLimitEnabled()) {
        return true;
    }

    unsigned long curTimeMs = Clock::timeMs();
    if (m_receiveLastTimeMs) {
        ++m_receiveHistoryPackets;
        m_receiveHistoryBytes += messageSize;
        m_receiveHistoryMs += curTimeMs - m_receiveLastTimeMs;

        // rescale the history information if we've exceeded the reset time; this
        // must be done before the spam check below or else we may run into overflow
        // issues because m_receiveHistoryMs could be pretty large if we haven't
        // received anything from the client for a while
        while (m_receiveHistoryMs > ConfigConnectionServer::getSpamLimitResetTimeMs()) {
            ++m_countSpamLimitResetTime;

            unsigned int resetScale = ConfigConnectionServer::getSpamLimitResetScaleFactor();
            m_receiveHistoryMs /= resetScale;
            m_receiveHistoryBytes /= resetScale;
            m_receiveHistoryPackets /= resetScale;
        }

        // check for exceeding limits, but wait for at least
        // one reset cycle so that there has been enough
        // elapsed time, so we won't get a false positive
        if (m_countSpamLimitResetTime) {
            if (m_receiveHistoryBytes >=
                m_receiveHistoryMs * ConfigConnectionServer::getSpamLimitBytesPerSec() / 1000) {
                LOG("Network", ("Client %s disconnected for exceeding bytes/sec limit (bytes=%u, time=%lums)\n", getCharacterId().getValueString().c_str(), m_receiveHistoryBytes, m_receiveHistoryMs));
                return false;
            }
            if (m_receiveHistoryPackets >=
                m_receiveHistoryMs * ConfigConnectionServer::getSpamLimitPacketsPerSec() / 1000) {
                LOG("Network", ("Client %s disconnected for exceeding packets/sec limit (packets=%u, time=%lums)\n", getCharacterId().getValueString().c_str(), m_receiveHistoryPackets, m_receiveHistoryMs));
                return false;
            }
        }
    }
    m_receiveLastTimeMs = curTimeMs;
    return true;
}

//-----------------------------------------------------------------------

void ClientConnection::onReceive(const Archive::ByteStream &message) {
    try {
        if (!checkSpamLimit(message.getSize())) {
            ConnectionServer::dropClient(this, "Spam Detected");
            return;
        }

        unsigned long curTimeMs = Clock::timeMs();
        if (m_sendLastTimeMs +
            std::min(gs_receiveDelayMaxMs, static_cast<unsigned long>(Clock::frameTime() * 1000.0f)) < curTimeMs) {
            static HeartBeat h;
            send(h, false);
        }

        Archive::ReadIterator ri = message.begin();
        GameNetworkMessage m(ri);
        ri = message.begin();

        const uint32 messageType = m.getType();

        //Clients with a selected character get routed to a game server.
        //@todo check for filtering out bad messages.
        if (m_hasSelectedCharacter) {
            // if it is a chat message, send it directly to the chat server
            switch (messageType) {
                case constcrc("ChatAddFriend") :
                case constcrc("ChatAddModeratorToRoom") :
                case constcrc("ChatBanAvatarFromRoom") :
                case constcrc("ChatCreateRoom") :
                case constcrc("ChatDeletePersistentMessage") :
                case constcrc("ChatDeleteAllPersistentMessages") :
                case constcrc("ChatDestroyRoom") :
                case constcrc("ChatInstantMessageToCharacter")  :
                case constcrc("ChatInviteAvatarToRoom") :
                case constcrc("ChatKickAvatarFromRoom") :
                case constcrc("ChatRemoveAvatarFromRoom") :
                case constcrc("ChatRemoveFriend") :
                case constcrc("ChatRemoveModeratorFromRoom") :
                case constcrc("ChatRequestPersistentMessage") :
                case constcrc("ChatRequestRoomList") :
                case constcrc("ChatSendToRoom") :
                case constcrc("ChatUninviteFromRoom") :
                case constcrc("ChatUnbanAvatarFromRoom") :
                case constcrc("VerifyPlayerNameMessage") : {
                    DEBUG_REPORT_LOG(true, ("ConnServ: ClientConnection::onReceive()\n"));

                    NOT_NULL(m_client);
                    if (m_client) {
                        static std::vector <NetworkId> v;
                        v.clear();
                        v.push_back(m_client->getNetworkId());
                        GameClientMessage gcm(v, true, ri);
                        if (m_client->getChatConnection()) {
                            m_client->getChatConnection()->send(gcm, true);
                        } else {
                            // defer chat messages until a server is back online
                            Archive::ByteStream bs;
                            m.pack(bs);
                            m_client->deferChatMessage(bs);
                        }
                    } else {
                        ConnectionServer::dropClient(this, "m_client is nullptr while receiving a message!");
                        disconnect();
                    }
                    break;
                }
                    // ChatEnterRoom and ChatEnterRoomById needs to go to the game server to determine
                    // if the character is not allowed to enter the room because of game rule restrictions;
                    // only if that test pass do we forward the message on to the chat server to request
                    // to enter the room
                case constcrc("ChatEnterRoom") :
                case constcrc("ChatEnterRoomById") : {
                    NOT_NULL(m_client);

                    unsigned int sequence;
                    std::string roomName;

                    Archive::ReadIterator cri = message.begin();

                    if (messageType == constcrc("ChatEnterRoom")) {
                        ChatEnterRoom const cer(cri);
                        sequence = cer.getSequence();
                        roomName = cer.getRoomName();
                    } else {
                        ChatEnterRoomById const cerbi(cri);
                        sequence = cerbi.getSequence();
                        roomName = cerbi.getRoomName();
                    }

                    if (m_client && m_client->getGameConnection()) {
                        if (m_pendingChatEnterRoomRequests.count(sequence) == 0) {
                            GenericValueTypeMessage < std::pair < std::pair < NetworkId, std::string >, unsigned
                            int > >
                            const cervr(
                            "ChatEnterRoomValidationRequest", std::make_pair(std::make_pair(m_client->getNetworkId(), roomName), sequence));

                            m_client->getGameConnection()->send(cervr, true);

                            // queue up request until game server responds
                            static std::vector <NetworkId> v;
                            v.clear();
                            v.push_back(m_client->getNetworkId());
                            m_pendingChatEnterRoomRequests[sequence] = new GameClientMessage(v, true, ri);
                        }
                    } else {
                        // send back response to client saying game server not available

                        // the client only cares about sequence and result when it's a failure
                        ChatOnEnteredRoom fail(sequence, SWG_CHAT_ERR_NO_GAME_SERVER, 0, ChatAvatarId());
                        send(fail, true);
                    }

                    break;
                }
                    // ChatQueryRoom needs to go to the game server to determine if the character is
                    // not allowed to query the room because of game rule restrictions; only if that
                    // test pass do we forward the message on to the chat server for completion
                case constcrc("ChatQueryRoom") : {
                    NOT_NULL(m_client);

                    Archive::ReadIterator cri = message.begin();
                    ChatQueryRoom cqr(cri);

                    if (m_client && m_client->getGameConnection()) {
                        if (m_pendingChatQueryRoomRequests.count(cqr.getSequence()) == 0) {
                            GenericValueTypeMessage < std::pair < std::pair < NetworkId, std::string >, unsigned
                            int > >
                            const cqrvr(
                            "ChatQueryRoomValidationRequest", std::make_pair(std::make_pair(m_client->getNetworkId(), cqr.getRoomName()), cqr.getSequence()));

                            m_client->getGameConnection()->send(cqrvr, true);

                            // queue up request until game server responds
                            static std::vector <NetworkId> v;
                            v.clear();
                            v.push_back(m_client->getNetworkId());
                            m_pendingChatQueryRoomRequests[cqr.getSequence()] = new GameClientMessage(v, true, ri);
                        }
                    }
                    break;
                }
                    // ChatInviteGroupToRoom needs to go to the game server to get group information
                case constcrc("ChatInviteGroupToRoom") : {
                    NOT_NULL(m_client);
                    if (m_client) {
                        if (m_client->getGameConnection()) {
                            static std::vector <NetworkId> v;
                            v.clear();
                            v.push_back(m_client->getNetworkId());
                            GameClientMessage gcm(v, true, ri);
                            m_client->getGameConnection()->send(gcm, true);
                        } else {
                            // defer chat messages until a server is back online
                            Archive::ByteStream bs;
                            m.pack(bs);
                            m_client->deferChatMessage(bs);
                        }
                    } else {
                        ConnectionServer::dropClient(this, "m_client is nullptr while receiving a message!");
                        disconnect();
                    }
                    break;
                }
                    // ChatPersistentMessageToServer may need to be passed off to the game server for guild or citizens messages
                case constcrc("ChatPersistentMessageToServer") : {
                    NOT_NULL(m_client);
                    if (m_client) {
                        static std::vector <NetworkId> v;
                        v.clear();
                        v.push_back(m_client->getNetworkId());

                        Archive::ReadIterator cri = message.begin();
                        ChatPersistentMessageToServer chat(cri);
                        std::string const &toName = chat.getToCharacterName().name;
                        if (!_stricmp(toName.c_str(), "guild") || !_strnicmp(toName.c_str(), "guild ", 6) ||
                            !_stricmp(toName.c_str(), "citizens")) {
                            if (m_client->getGameConnection()) {
                                GameClientMessage gcm(v, true, ri);
                                m_client->getGameConnection()->send(gcm, true);
                            }
                        } else {
                            if (m_client->getChatConnection()) {
                                GameClientMessage gcm(v, true, ri);
                                m_client->getChatConnection()->send(gcm, true);
                            } else {
                                // defer chat messages until a server is back online
                                Archive::ByteStream bs;
                                m.pack(bs);
                                m_client->deferChatMessage(bs);
                            }
                        }
                    } else {
                        ConnectionServer::dropClient(this, "m_client is nullptr while receiving a message!");
                        disconnect();
                    }
                    break;
                }
                    // if it is a cs message, send it directly to the cs server
                case constcrc("ConnectPlayerMessage") :
                case constcrc("DisconnectPlayerMessage") :
                case constcrc("CreateTicketMessage") :
                case constcrc("AppendCommentMessage") :
                case constcrc("CancelTicketMessage") :
                case constcrc("GetTicketsMessage") :
                case constcrc("GetCommentsMessage") :
                case constcrc("SearchKnowledgeBaseMessage") :
                case constcrc("GetArticleMessage") :
                case constcrc("RequestCategoriesMessage") :
                case constcrc("NewTicketActivityMessage") : {
                    NOT_NULL(m_client);
                    if (m_client) {
                        CustomerServiceConnection *customerServiceConnection = m_client->getCustomerServiceConnection();

                        //DEBUG_REPORT_LOG(true, ("CONSRV::CS - suid: %i\n", getSUID()));

                        if (customerServiceConnection != nullptr) {
                            static std::vector <NetworkId> v;
                            v.clear();
                            v.push_back(m_client->getNetworkId());

                            // TODO: this shit could be made into a template
                            switch (messageType) {
                                case constcrc("ConnectPlayerMessage") : {
                                    ConnectPlayerMessage message(ri);
                                    message.setStationId(getSUID());

                                    GameClientMessage gcm(v, true, message);
                                    customerServiceConnection->send(gcm, true);
                                    break;
                                }
                                case constcrc("CreateTicketMessage") : {
                                    CreateTicketMessage message(ri);
                                    message.setStationId(getSUID());

                                    GameClientMessage gcm(v, true, message);
                                    customerServiceConnection->send(gcm, true);
                                    break;
                                }
                                case constcrc("AppendCommentMessage") : {
                                    AppendCommentMessage message(ri);
                                    message.setStationId(getSUID());

                                    GameClientMessage gcm(v, true, message);
                                    customerServiceConnection->send(gcm, true);
                                    break;
                                }
                                case constcrc("CancelTicketMessage") : {
                                    CancelTicketMessage message(ri);
                                    message.setStationId(getSUID());

                                    GameClientMessage gcm(v, true, message);
                                    customerServiceConnection->send(gcm, true);
                                    break;
                                }
                                case constcrc("GetTicketsMessage") : {
                                    GetTicketsMessage message(ri);
                                    message.setStationId(getSUID());

                                    GameClientMessage gcm(v, true, message);
                                    customerServiceConnection->send(gcm, true);
                                    break;
                                }
                                case constcrc("NewTicketActivityMessage") : {
                                    NewTicketActivityMessage message(ri);
                                    message.setStationId(getSUID());

                                    GameClientMessage gcm(v, true, message);
                                    customerServiceConnection->send(gcm, true);
                                    break;
                                }
                                default : {
                                    GameClientMessage gcm(v, true, ri);
                                    customerServiceConnection->send(gcm, true);
                                    break;
                                }
                            }
                        }
                    } else {
                        ConnectionServer::dropClient(this, "m_client is nullptr while receiving a message!");
                        disconnect();
                    }
                    break;
                }
                case constcrc("28afefcc187a11dc888b001") : // obfuscation for ClientInactivityMessage message
                {
                    GenericValueTypeMessage<bool> msg(ri);

                    if (m_hasBeenValidated && m_sessionValidated) {
                        // client went inactive
                        if (msg.getValue()) {
                            if (m_lastActiveTime > 0) {
                                // record the amount of active time
                                m_activePlayTimeDuration += static_cast<unsigned long>(::time(nullptr) -
                                                                                       m_lastActiveTime);

                                // tell Session to stop recording play time for the character
                                if (ConnectionServer::getSessionApiClient() &&
                                    ConfigConnectionServer::getSessionRecordPlayTime()) {
                                    LOG("CustomerService", ("Login:%s calling SessionStopPlay() for %s/%s/%s (%s). Active play time: %s", ClientConnection::describeAccount(this).c_str(), this->getSessionId().c_str(), ConfigConnectionServer::getClusterName(), this->getCharacterName().c_str(), this->getCharacterId().getValueString().c_str(), this->getCurrentActivePlayTimeDuration().c_str()));
                                    ConnectionServer::getSessionApiClient()->stopPlay(*this);
                                }

                                // client is no longer active; this needs to be set after the LOG() statement
                                // above because getCurrentActivePlayTimeDuration() uses m_lastActiveTime
                                m_lastActiveTime = 0;

                                // update the play time info on the game server
                                sendPlayTimeInfoToGameServer();

                                // drop inactive character
                                if (ConfigConnectionServer::getDisconnectOnInactive()) {
                                    LOG("ClientDisconnect", ("Disconnecting %u because the player was inactive for too long.", getSUID()));
                                    ConnectionServer::dropClient(this, "Client inactivity");
                                    disconnect();
                                } else if (ConfigConnectionServer::getDisconnectFreeTrialOnInactive() &&
                                           ((m_featureBitsSubscription & ClientSubscriptionFeature::Base) == 0)) {
                                    LOG("ClientDisconnect", ("Disconnecting (free trial) %u because the player was inactive for too long.", getSUID()));
                                    ConnectionServer::dropClient(this, "Client inactivity (free trial)");
                                    disconnect();
                                }
                            }
                        }
                            // client went active
                        else {
                            if (m_lastActiveTime == 0) {
                                // record the time client went active
                                m_lastActiveTime = ::time(nullptr);

                                // tell Session to start recording play time for the character
                                if (ConnectionServer::getSessionApiClient() &&
                                    ConfigConnectionServer::getSessionRecordPlayTime()) {
                                    LOG("CustomerService", ("Login:%s calling SessionStartPlay() for %s/%s/%s (%s)", ClientConnection::describeAccount(this).c_str(), this->getSessionId().c_str(), ConfigConnectionServer::getClusterName(), this->getCharacterName().c_str(), this->getCharacterId().getValueString().c_str()));
                                    ConnectionServer::getSessionApiClient()->startPlay(*this);
                                }

                                // update the play time info on the game server
                                sendPlayTimeInfoToGameServer();
                            }
                        }
                    }
                    break;
                }
                default : {
                    //Forward on to Game Server
                    DEBUG_REPORT_LOG((!m_client ||
                                      !m_client->getGameConnection()), ("Warn, received game message with no game connection.  This may happen for a short time after a GameServer crashes.  If it continues to happen, it indicates a bug.\n"));

                    if (m_client && m_client->getGameConnection()) {
                        static std::vector <NetworkId> v;
                        v.clear();
                        v.push_back(m_client->getNetworkId());
                        GameClientMessage gcm(v, true, ri);
                        m_client->getGameConnection()->send(gcm, true);
                    }

                    break;
                }
            }
        } else {
            switch (messageType) {
                case constcrc("ClientIdMsg") : {
                    DEBUG_REPORT_LOG(true, ("Recieved ClientIdMsg\n"));
                    ClientIdMsg k(ri);

                    handleClientIdMessage(k);
                    break;
                }
                case constcrc("SelectCharacter") : {
                    SelectCharacter s(ri);
                    DEBUG_REPORT_LOG(true, ("Recvd SelectCharacter message for %s.\n", s.getId().getValueString().c_str()));

                    handleSelectCharacterMessage(s);
                    break;
                }
                case constcrc("ClientCreateCharacter") : {
                    if (m_hasBeenValidated && !m_hasSelectedCharacter) //lint !e774 no this doesn't always eval to true
                    {
                        ClientCreateCharacter clientCreate(ri);
                        DEBUG_REPORT_LOG(true, ("Got ClientCreateCharacter message for %lu with name %s\n", m_suid, Unicode::wideToNarrow(clientCreate.getCharacterName()).c_str()));
                        LOG("TraceCharacterCreation", ("%d sent ClientCreateCharacter(charaterName=%s, templateName=%s, scaleFactor=%f, startingLocation=%s, hairTemplateName=%s, profession=%s, jedi=%d, useNewbieTutorial=%d, skillTemplate=%s, workingSkill=%s)", getSUID(), Unicode::wideToNarrow(clientCreate.getCharacterName()).c_str(), clientCreate.getTemplateName().c_str(), clientCreate.getScaleFactor(), clientCreate.getStartingLocation().c_str(), clientCreate.getHairTemplateName().c_str(), clientCreate.getProfession().c_str(), static_cast<int>(clientCreate.getJedi()), static_cast<int>(clientCreate.getUseNewbieTutorial()), clientCreate.getSkillTemplate().c_str(), clientCreate.getWorkingSkill().c_str()));

                        if (!clientCreate.getUseNewbieTutorial() && !m_canSkipTutorial) {
                            LOG("TraceCharacterCreation", ("%d failed character creation. The client is not allowed to skip the tutorial", getSUID()));
                            // This is probably a hack attempt, because the Client was already told they couldn't skip the tutorial
                            LOG("ClientDisconnect", ("Disconnecting %u because they tried to skip the tutorial without permission.\n", getSUID()));
                            disconnect();
                        } else if (clientCreate.getJedi() && !m_canCreateJediCharacter) {
                            LOG("TraceCharacterCreation", ("%d failed character creation. The request character type was Jedi, but this client cannot create a Jedi character", getSUID()));
                            // This is probably a hack attempt, because the Client was already told they couldn't create a character
                            LOG("ClientDisconnect", ("Disconnecting %u because they tried to create a Jedi character without permission.\n", getSUID()));
                            disconnect();
                        } else if (!clientCreate.getJedi() && !m_canCreateRegularCharacter) {
                            LOG("TraceCharacterCreation", ("%d failed character creation. The client is not allowed to create any characters", getSUID()));
                            // This is probably a hack attempt, because the Client was already told they couldn't create a character
                            LOG("ClientDisconnect", ("Disconnecting %u because they tried to create a regular character without permission.\n", getSUID()));
                            disconnect();
                        } else if (m_hasRequestedCharacterCreate) {
                            LOG("TraceCharacterCreation", ("%d failed character creation. The client has already requested character creation on this connection", getSUID()));
                            LOG("ClientDisconnect", ("Disconnecting %u because the client has already requested character creation on this connection.\n", getSUID()));
                            disconnect();
                        } else if (m_hasCreatedCharacter) {
                            LOG("TraceCharacterCreation", ("%d failed character creation. A character has been created on this or another galaxy for this account while this connection was up", getSUID()));
                            LOG("ClientDisconnect", ("Disconnecting %u because a character has been created on this or another galaxy for this account while this connection was up.\n", getSUID()));
                            disconnect();
                        } else if (clientCreate.getCharacterName().length() == 0) {
                            LOG("TraceCharacterCreation", ("%d failed character creation. The character's name is empty", getSUID()));
                            LOG("ClientDisconnect", ("Disconnecting %u because they tried to create a character with no name.\n", getSUID()));
                            disconnect();
                        } else {
                            Unicode::String biography(clientCreate.getBiography());
                            if (biography.length() > 1024) {
                                IGNORE_RETURN(biography.erase(1024));
                                DEBUG_REPORT_LOG(true, ("Biography shortened to 1024 characters.\n"));
                            }

                            if (m_isAdminAccount) {
                                ConnectionCreateCharacter connectionCreate(m_suid, clientCreate.getCharacterName(), clientCreate.getTemplateName(), clientCreate.getScaleFactor(), clientCreate.getStartingLocation(), clientCreate.getAppearanceData(), clientCreate.getHairTemplateName(), clientCreate.getHairAppearanceData(), clientCreate.getProfession(), clientCreate.getJedi(), biography, clientCreate.getUseNewbieTutorial(), clientCreate.getSkillTemplate(), clientCreate.getWorkingSkill(), m_isAdminAccount, false, m_featureBitsGame);

                                ConnectionServer::sendToCentralProcess(connectionCreate);
                                LOG("TraceCharacterCreation", ("%d character creation request sent to CentralServer", getSUID()));
                            } else {
                                // for regular players, do one final check with the LoginServer
                                // to make sure the character can be created (i.e. that character
                                // limits have not been exceeded)
                                delete m_pendingCharacterCreate;
                                m_pendingCharacterCreate = new ConnectionCreateCharacter(m_suid, clientCreate.getCharacterName(), clientCreate.getTemplateName(), clientCreate.getScaleFactor(), clientCreate.getStartingLocation(), clientCreate.getAppearanceData(), clientCreate.getHairTemplateName(), clientCreate.getHairAppearanceData(), clientCreate.getProfession(), clientCreate.getJedi(), biography, clientCreate.getUseNewbieTutorial(), clientCreate.getSkillTemplate(), clientCreate.getWorkingSkill(), m_isAdminAccount, false, m_featureBitsGame);

                                LOG("TraceCharacterCreation", ("%d character creation request awaiting final verification from LoginServer", getSUID()));

                                ValidateAccountMessage vcm(m_suid, 0, m_featureBitsSubscription);
                                ConnectionServer::sendToCentralProcess(vcm);
                            }

                            m_hasRequestedCharacterCreate = true;
                        }
                    }
                    break;
                }
                case constcrc("ClientRandomNameRequest") : {
                    ClientRandomNameRequest clientRandomName(ri);

                    RandomNameRequest randomNameRequest(m_suid, clientRandomName.getCreatureTemplate());
                    ConnectionServer::sendToCentralProcess(randomNameRequest);
                    LOG("TraceCharacterCreation", ("%d requested a random name. Request sent to CentralServer", getSUID()));
                    break;
                }
                case constcrc("ClientVerifyAndLockNameRequest") : {
                    ClientVerifyAndLockNameRequest clientVerifyAndLockNameRequest(ri);

                    VerifyAndLockNameRequest verifyAndLockNameRequest(m_suid, NetworkId::cms_invalid, clientVerifyAndLockNameRequest.getTemplateName(), clientVerifyAndLockNameRequest.getCharacterName(), m_featureBitsGame);
                    ConnectionServer::sendToCentralProcess(verifyAndLockNameRequest);
                    LOG("TraceCharacterCreation", ("%d requested a verify and lock of name: %s. Request sent to CentralServer", getSUID(), Unicode::wideToNarrow(verifyAndLockNameRequest.getCharacterName()).c_str()));
                    break;
                }
                case constcrc("LagRequest") : {
                    // TODO: why is this commented out?
                    // handleLagRequest();
                    break;
                }
            }
        }
    } catch (const Archive::ReadException &readException) {
        WARNING(true, ("Archive read error (%s) from client. Disconnecting client", readException.what()));
        LOG("ClientDisconnect", ("Archive read error (%s) from client. Disconnecting client", readException.what()));
        disconnect();
    }
}

//-----------------------------------------------------------------------

void ClientConnection::handleLagRequest() {
    // client is requesting a lag ping (reliable trace)
    GameNetworkMessage response("ConnectionServerLagResponse");
    send(response, true);

    if (m_hasSelectedCharacter && m_client && m_client->getGameConnection()) {
        // send to game server
        GameNetworkMessage request("LagRequest");
        std::vector <NetworkId> v;
        v.push_back(m_characterId);
        GameClientMessage gcm(v, true, request);
        m_client->getGameConnection()->send(gcm, true);
    } else {
        // send game response immediately
        GameNetworkMessage gameResponse("GameServerLagResponse");
        send(gameResponse, true);
    }

}

//-----------------------------------------------------------------------

/** character has selected a character and calls this function
* to associate the connection with the new client they created with that
* character.  We no longer need the character map.
*/
void ClientConnection::setClient(Client *newClient) {
    //This fatal is here to try to catch the reconnect bug.
    WARNING_STRICT_FATAL(m_client, ("Attempting to set the client on a connection that already has one. Client %s\n", getCharacterName().c_str()));
    // jrandall - I've removed the fatal because it is blocking some people from getting some work
    // done. I'm on a high priority fix at the moment. If this warning starts appearing,
    // set a break point or something.
    //DEBUG_FATAL(client, ("Attempting to set the client on a connection that already has one.\n"));
    m_client = newClient;

    // todo put this in:    characterMap.clear();
}

//-----------------------------------------------------------------------

void ClientConnection::send(const GameNetworkMessage &message, const bool reliable) {
    m_sendLastTimeMs = Clock::timeMs();


    if (sm_outgoingBytesMap_Worktime == 0) {
        sm_outgoingBytesMap_Worktime = m_sendLastTimeMs;
    } else if ((m_sendLastTimeMs - sm_outgoingBytesMap_Worktime) > 60000)   // 60 seconds
    {
        sm_outgoingBytesMap_Stats = sm_outgoingBytesMap_Working;
        std::map<std::string, uint32>::iterator iter;
        for (iter = sm_outgoingBytesMap_Working.begin(); iter != sm_outgoingBytesMap_Working.end(); ++iter) {
            iter->second = 0;
        }
        sm_outgoingBytesMap_Worktime = m_sendLastTimeMs;
    }
    sm_outgoingBytesMap_Working[message.getCmdName()] += message.getByteStream().getSize();


    ServerConnection::send(message, reliable);
}

//-----------------------------------------------------------------------

std::map <std::string, uint32> &ClientConnection::getPacketBytesPerMinStats() {
    uint32 now = Clock::timeMs();
    if (sm_outgoingBytesMap_Worktime == 0) {
        sm_outgoingBytesMap_Worktime = now;
    } else if ((now - sm_outgoingBytesMap_Worktime) > 60000)   // 60 seconds
    {
        sm_outgoingBytesMap_Stats = sm_outgoingBytesMap_Working;
        std::map<std::string, uint32>::iterator iter;
        for (iter = sm_outgoingBytesMap_Working.begin(); iter != sm_outgoingBytesMap_Working.end(); ++iter) {
            iter->second = 0;
        }
        sm_outgoingBytesMap_Worktime = now;
    }

    return sm_outgoingBytesMap_Stats;
}

//-----------------------------------------------------------------------

void ClientConnection::handleChatEnterRoomValidationResponse(unsigned int sequence, unsigned int result) {
    std::map < unsigned
    long, GameClientMessage * > ::iterator
    iterFind = m_pendingChatEnterRoomRequests.find(sequence);
    if (iterFind != m_pendingChatEnterRoomRequests.end()) {
        if (result == CHATRESULT_SUCCESS) {
            if (m_client && m_client->getChatConnection()) {
                // game server says it's ok to enter the chat room,
                // so forward the request on to the chat server
                m_client->getChatConnection()->send(*(iterFind->second), true);
            } else {
                // send back response to client saying chat server not available

                // the client only cares about sequence and result when it's a failure
                ChatOnEnteredRoom fail(sequence, SWG_CHAT_ERR_CHAT_SERVER_UNAVAILABLE, 0, ChatAvatarId());
                send(fail, true);
            }
        } else {
            // send back response to client saying game server denied enter room request

            // the client only cares about sequence and result when it's a failure
            ChatOnEnteredRoom fail(sequence, result, 0, ChatAvatarId());
            send(fail, true);
        }

        delete iterFind->second;
        m_pendingChatEnterRoomRequests.erase(iterFind);
    }
}

//-----------------------------------------------------------------------

void ClientConnection::handleChatQueryRoomValidationResponse(unsigned int sequence, bool success) {
    std::map < unsigned
    long, GameClientMessage * > ::iterator
    iterFind = m_pendingChatQueryRoomRequests.find(sequence);
    if (iterFind != m_pendingChatQueryRoomRequests.end()) {
        if (success) {
            if (m_client && m_client->getChatConnection()) {
                // game server says it's ok to query the chat room,
                // so forward the request on to the chat server
                m_client->getChatConnection()->send(*(iterFind->second), true);
            }
        }

        delete iterFind->second;
        m_pendingChatQueryRoomRequests.erase(iterFind);
    }
}

//-----------------------------------------------------------------------

void ClientConnection::sendByteStream(const Archive::ByteStream &bs, bool reliable) {
    Connection::send(bs, reliable);
}

//-----------------------------------------------------------------------

/**
 * Send the client to an arbitratry game server based on the current scene
 */
const bool ClientConnection::sendToGameServer() {
    GameConnection *c = const_cast<GameConnection *>(ConnectionServer::getGameConnection(m_targetScene));
    return sendToGameServer(c);
}

// ----------------------------------------------------------------------

/**
 * Send the client to a particular game server, sepcified by process id.
 */

const bool ClientConnection::sendToGameServer(uint32 gameServerId) {
    GameConnection *c = const_cast<GameConnection *>(ConnectionServer::getGameConnection(gameServerId));
    return sendToGameServer(c);
}

// ----------------------------------------------------------------------

bool ClientConnection::sendToGameServer(GameConnection *c) {
    bool result = false;

    if (c && m_hasSelectedCharacter && m_hasBeenValidated) {
        //create a new client
        ConnectionServer::addNewClient(this, m_characterId, c, m_targetScene, m_sendToStarport);

        LoggedInMessage m(m_suid);
        ConnectionServer::sendToCentralProcess(m);
        result = true;
        m_hasBeenSentToGameServer = true;
    }
    return result;
}

//-----------------------------------------------------------------------

/**
 * Called when DBProcess responds to our ValidateCharacterForLoginMessage.
 * Now we know whether the character is valid and where in the world it
 * is located.
 */
void
ClientConnection::onCharacterValidated(bool isValid, const NetworkId &character, const std::string &characterName, const NetworkId &container, const std::string &scene, const Vector &coordinates) {
    if (!m_validatingCharacter) {
        LOG("TraceCharacterSelection", ("%d received a validation response, but is not in the process of validation", getSUID()));
        DEBUG_REPORT_LOG(true, ("Got unexpected onCharacterValidated() for account %lu.\n", getSUID()));
        return;
    }
    m_validatingCharacter = false;

    if (isValid) {
        LOG("TraceCharacterSelection", ("%d received a validation response. The character (%s: %s) at (%s,%f,%f,%f,%s) selected is valid", getSUID(), character.getValueString().c_str(), characterName.c_str(), scene.c_str(), coordinates.x, coordinates.y, coordinates.z, container.getValueString().c_str()));
        LOG("CustomerService", ("Login:%s received a validation response. The character (%s: %s) at (%s,%f,%f,%f,%s) selected is valid", describeAccount(this).c_str(), character.getValueString().c_str(), characterName.c_str(), scene.c_str(), coordinates.x, coordinates.y, coordinates.z, container.getValueString().c_str()));
        m_targetScene = scene;
        m_targetCoordinates = coordinates;
        m_characterId = character;
        m_containerId = container;
        m_characterName = characterName;

        m_hasSelectedCharacter = true;

        // ask CentralServer to suggest a game server for this character
        // (Central will forward the request to a Planet Server)

        RequestGameServerForLoginMessage requestmsg(getSUID(), m_characterId, m_containerId, m_targetScene, m_targetCoordinates, false);
        if (ConnectionServer::getCentralConnection()) {
            ConnectionServer::getCentralConnection()->send(requestmsg, true);
        } else {
            LOG("ClientDisconnect", ("Can't handle login of character %s because there is no connection to Central.\n", m_characterId.getValueString().c_str()));
            ErrorMessage err("Validation Failed", "The connection to the central server is down.  Please try again later.");
            send(err, true);

            disconnect();
        }
    } else {
        LOG("TraceCharacterSelection", ("%d validation failed, disconnecting client", getSUID()));
        ErrorMessage err("Validation Failed", "Your character was denied login by the database.");
        send(err, true);

        LOG("ClientDisconnect", ("Denying login for account %u.\n", getSUID()));
        disconnect();
    }
}

//------------------------------------------------------------------------------------------

void
ClientConnection::onValidateClient(StationId suid, const std::string &username, bool secure, const char *id, const uint32 gameFeatures, const uint32 subscriptionFeatures, unsigned int entitlementTotalTime, unsigned int entitlementEntitledTime, unsigned int entitlementTotalTimeSinceLastLogin, unsigned int entitlementEntitledTimeSinceLastLogin, int buddyPoints) {
    UNREF(id);
    m_sessionValidated = true;
    m_suid = suid;
    m_accountName = username;
    m_featureBitsGame = gameFeatures;
    m_featureBitsSubscription = subscriptionFeatures;
    m_isSecure = secure;
    m_entitlementTotalTime = entitlementTotalTime;
    m_entitlementEntitledTime = entitlementEntitledTime;
    m_entitlementTotalTimeSinceLastLogin = entitlementTotalTimeSinceLastLogin;
    m_entitlementEntitledTimeSinceLastLogin = entitlementEntitledTimeSinceLastLogin;
    m_buddyPoints = buddyPoints;

    if (id) {
        m_sessionId = id;
    }

    if (m_requestedSuid != 0 && suid != m_requestedSuid) {
        //verify internal, secure, is on the god list
        bool loginOK = false;
        if (!secure) {
            LOG("CustomerService", ("AdminLogin:  User %s (account %li) attempted to log into account %li, but was not using a SecureID token", username.c_str(), suid, m_requestedSuid));
        } else {
            if (!AdminAccountManager::isInternalIp(getRemoteAddress())) {
                LOG("CustomerService", ("AdminLogin:  User %s (account %li) attempted to log into account %li, but was not logging in from an internal IP", username.c_str(), suid, m_requestedSuid));
            } else {
                int adminLevel = 0;
                if (!AdminAccountManager::isAdminAccount(Unicode::toLower(username), adminLevel) || adminLevel < 10) {
                    LOG("CustomerService", ("AdminLogin:  User %s (account %li) attempted to log into account %li, but did not have sufficient permissions", username.c_str(), suid, m_requestedSuid));
                } else {
                    LOG("CustomerService", ("AdminLogin:  User %s (account %li) logged into account %li", username.c_str(), m_suid, m_requestedSuid));
                    DEBUG_REPORT_LOG(true, ("AdminLogin:  User %s (account %li) logged into account %li\n", username.c_str(), m_suid, m_requestedSuid));
                    m_suid = m_requestedSuid;
                    m_usingAdminLogin = true;
                    loginOK = true;
                }
            }
        }
        if (!loginOK) {
            disconnect();
            return;
        }
    }

    m_featureBitsGame &= ~ConfigConnectionServer::getDisabledFeatureBits();

    //Configoption to enable JTL features for beta players so that our code can pretend everything uses the JTL Retail bit
    if (ConfigConnectionServer::getSetJtlRetailIfBetaIsSet()) {
        if (ClientGameFeature::SpaceExpansionBeta & m_featureBitsGame) {
            m_featureBitsGame |= ClientGameFeature::SpaceExpansionRetail;
        }
    }

    //Configoption to enable Obiwan features for beta players so that our code can pretend everything uses the Obiwan Retail bit
    if (ConfigConnectionServer::getSetTrialsOfObiwanRetailIfBetaIsSet()) {
        if (ClientGameFeature::TrialsOfObiwanBeta & m_featureBitsGame) {
            //-- add retail bit only if player does not have the preorder bit
            if ((m_featureBitsGame & ClientGameFeature::TrialsOfObiwanPreorder) == 0) {
                m_featureBitsGame |= ClientGameFeature::TrialsOfObiwanRetail;
            }
        } else {
            // Clear bits from players who might have them for real, but aren't in the beta
            m_featureBitsGame &= ~ClientGameFeature::TrialsOfObiwanRetail;
            m_featureBitsGame &= ~ClientGameFeature::TrialsOfObiwanPreorder;
        }
    }

    //-- Obiwan Preorders get the Retail bit as well... All rewards etc...
    if (ClientGameFeature::TrialsOfObiwanPreorder & m_featureBitsGame) {
        m_featureBitsGame |= ClientGameFeature::TrialsOfObiwanRetail;
    }

    // Restrictions for "new free trial" account
    if (((m_featureBitsSubscription & ClientSubscriptionFeature::FreeTrial2) != 0) &&
        ((m_featureBitsSubscription & ClientSubscriptionFeature::Base) == 0)) {
        // "new free trial" account don't have access to RoW until they convert
        m_featureBitsGame &= ~ClientGameFeature::Episode3ExpansionRetail;
        m_featureBitsGame &= ~ClientGameFeature::Episode3PreorderDownload;

        // ClientGameFeature::FreeTrial2 indicates this is a converted "new free trial"
        // account, and since this account hasn't converted yet, we remove this bit
        m_featureBitsGame &= ~ClientGameFeature::FreeTrial2;
    }

    // Clear feature bits that only apply if the account is paying (i.e. the sub base bit is set)
    if ((m_featureBitsSubscription & ClientSubscriptionFeature::Base) == 0) {
        m_featureBitsGame &= ~ClientGameFeature::HousePackupReward;
        m_featureBitsGame &= ~ClientGameFeature::BuddyProgramReward;
    }

    ValidateAccountMessage vcm(m_suid, 0, m_featureBitsSubscription);
    ConnectionServer::sendToCentralProcess(vcm);
    ConnectionServer::addConnectedClient(m_suid, this);

    uint32 const requiredSubscriptionBits = ConfigConnectionServer::getRequiredSubscriptionBits();
    if (requiredSubscriptionBits != 0) {
        if ((subscriptionFeatures & requiredSubscriptionBits) != requiredSubscriptionBits) {
            LOG("ClientDisconnect", ("Suid %d (%s) by session denial reason 'Invalid Subscription Bits'.", suid, username.c_str()));
            LOG("CustomerService", ("Login: %s by session denial reason 'Invalid Subscription Bits'.", describeAccount(this).c_str()));
            disconnect();
            return;
        }
    }

    uint32 const requiredGameBits = ConfigConnectionServer::getRequiredGameBits();
    if (requiredGameBits != 0) {
        if ((gameFeatures & requiredGameBits) != requiredGameBits) {
            LOG("ClientDisconnect", ("Suid %d (%s) by session denial reason 'Invalid Game Bits'.", suid, username.c_str()));
            LOG("CustomerService", ("Login: %s by session denial reason 'Invalid Game Bits'.", describeAccount(this).c_str()));
            disconnect();
            return;
        }
    }

    // tell client the server-side game and subscription feature bits and which ConnectionServer we are and the current server Epoch time
    GenericValueTypeMessage < std::pair < std::pair < unsigned
    long, unsigned
    long > , std::pair < int, int32 > > >
    const msgFeatureBits(
    "AccountFeatureBits", std::make_pair(std::make_pair(gameFeatures, subscriptionFeatures), std::make_pair(ConfigConnectionServer::getConnectionServerNumber(), static_cast<int32>(::time(nullptr)))));
    send(msgFeatureBits, true);

    std::string const gameFeaturesDescription = ClientGameFeature::getDescription(gameFeatures);
    std::string const subscriptionFeaturesDescription = ClientSubscriptionFeature::getDescription(subscriptionFeatures);

    LOG("CustomerService", ("Login:%s at IP: %s:%hu has connected with game code 0x%x (%s) and sub code 0x%x (%s) (entitlement total: %u/%u, since last login: %u/%u)", describeAccount(this).c_str(), getRemoteAddress().c_str(), getRemotePort(), gameFeatures, gameFeaturesDescription.c_str(), subscriptionFeatures, subscriptionFeaturesDescription.c_str(), m_entitlementEntitledTime, m_entitlementTotalTime, m_entitlementEntitledTimeSinceLastLogin, m_entitlementTotalTimeSinceLastLogin));

    // ask CentralServer to tell all other ConnectionServers on this galaxy to drop duplicate connections for this account
    // and ask CentralServer (via LoginServer) to tell all ConnectionServers on other galaxies to drop duplicate connections for this account
    if (!m_usingAdminLogin && !m_isSecure) {
        GenericValueTypeMessage <std::pair<uint32, std::string>> const dropDuplicateConnections("ConnSrvDropDupeConns", std::make_pair(m_suid, m_sessionId));
        ConnectionServer::sendToCentralProcess(dropDuplicateConnections);
    }
}

//-----------------------------------------------------------------------

std::string ClientConnection::describeAccount(const ClientConnection *c) {
    std::string result = "";
    if (c) {
        char idbuf[512] = {"\0"};
        const std::string &sessionId = c->getSessionId();
        if (sessionId.empty()) {
            snprintf(idbuf, sizeof(idbuf), " (%lu)", c->m_suid);
        } else {
            snprintf(idbuf, sizeof(idbuf), " (%lu, %s)", c->m_suid, sessionId.c_str());
        }

        result = c->m_accountName;
        result += idbuf;
    }
    return result;
}

// ----------------------------------------------------------------------

std::vector <std::pair<NetworkId, std::string>> const &ClientConnection::getConsumedRewardEvents() const {
    return m_consumedRewardEvents;
}

// ----------------------------------------------------------------------

std::vector <std::pair<NetworkId, std::string>> const &ClientConnection::getClaimedRewardItems() const {
    return m_claimedRewardItems;
}

// ----------------------------------------------------------------------

bool ClientConnection::isUsingAdminLogin() const {
    return m_usingAdminLogin;
}

// ----------------------------------------------------------------------

int ClientConnection::getBuddyPoints() const {
    return m_buddyPoints;
}

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