//
//  SuperTuxKart - a fun racing game with go-kart
//  Copyright (C) 2013-2015 SuperTuxKart-Team
//
//  This program is free software; you can redistribute it and/or
//  modify it under the terms of the GNU General Public License
//  as published by the Free Software Foundation; either version 3
//  of the License, or (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program; if not, write to the Free Software
//  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

#include "network/protocols/client_lobby.hpp"

#include "addons/addons_manager.hpp"
#include "audio/music_manager.hpp"
#include "audio/sfx_manager.hpp"
#include "config/user_config.hpp"
#include "config/player_manager.hpp"
#include "graphics/camera.hpp"
#include "guiengine/engine.hpp"
#include "guiengine/modaldialog.hpp"
#include "guiengine/message_queue.hpp"
#include "guiengine/screen_keyboard.hpp"
#include "input/device_manager.hpp"
#include "input/input_device.hpp"
#include "items/network_item_manager.hpp"
#include "items/powerup_manager.hpp"
#include "karts/abstract_kart.hpp"
#include "karts/controller/controller.hpp"
#include "karts/kart_properties.hpp"
#include "karts/kart_properties_manager.hpp"
#include "karts/official_karts.hpp"
#include "modes/linear_world.hpp"
#include "network/crypto.hpp"
#include "network/event.hpp"
#include "network/game_setup.hpp"
#include "network/network_config.hpp"
#include "network/network_player_profile.hpp"
#include "network/network_timer_synchronizer.hpp"
#include "network/peer_vote.hpp"
#include "network/protocols/connect_to_server.hpp"
#include "network/protocols/game_protocol.hpp"
#include "network/protocols/game_events_protocol.hpp"
#include "network/protocols/server_lobby.hpp"
#include "network/protocol_manager.hpp"
#include "network/race_event_manager.hpp"
#include "network/server.hpp"
#include "network/server_config.hpp"
#include "network/stk_host.hpp"
#include "network/stk_peer.hpp"
#include "race/grand_prix_manager.hpp"
#include "replay/replay_play.hpp"
#include "online/online_profile.hpp"
#include "online/xml_request.hpp"
#include "states_screens/dialogs/addons_pack.hpp"
#include "states_screens/dialogs/message_dialog.hpp"
#include "states_screens/online/networking_lobby.hpp"
#include "states_screens/online/network_kart_selection.hpp"
#include "states_screens/online/tracks_screen.hpp"
#include "tracks/track.hpp"
#include "tracks/track_manager.hpp"
#include "utils/log.hpp"
#include "utils/string_utils.hpp"
#include "utils/translation.hpp"

#include <algorithm>
#include <cstdlib>

#ifndef SERVER_ONLY
extern "C"
{
    #include <SheenBidi.h>
}
#endif

// ============================================================================
std::thread ClientLobby::m_background_download;
std::shared_ptr<Online::HTTPRequest> ClientLobby::m_download_request;
//-----------------------------------------------------------------------------
void ClientLobby::destroyBackgroundDownload()
{
    if (m_download_request)
    {
        m_download_request->cancel();
        m_download_request = nullptr;
    }
    if (m_background_download.joinable())
        m_background_download.join();
}

// ============================================================================
/** The protocol that manages starting a race with the server. It uses a 
 *  finite state machine:
\dot
digraph interaction {
"NONE" -> "LINKED" [label="ENet connection with server established"]
"LINKED" -> "REQUESTING_CONNECTION" [label="Request connection from server"]
"REQUESTING_CONNECTION" -> CONNECTED [label="Connection accepted by server"]
"REQUESTING_CONNECTION" -> "?? TO BE DONE ??" [label="Connection refused by server"]
"CONNECTED" -> "KART_SELECTION" [label="Server tells us to start kart selection"]
"KART_SELECTION" -> "SELECTING_KARTS" [label="Show kart selection screen"]
"SELECTING_KARTS" -> "RACING" [label="Server sends start race message"]
}
\enddot
Note that some states are actually managed outside of the client lobby. For
example to select race details after selecting a kart is managed by the GUI
engine.

*/

ClientLobby::ClientLobby(std::shared_ptr<Server> s)
           : LobbyProtocol()
{
    m_auto_started = false;
    m_waiting_for_game = false;
    m_server_auto_game_time = false;
    m_received_server_result = false;
    m_state.store(NONE);
    m_server = s;
    setHandleDisconnections(true);
    m_disconnected_msg[PDI_TIMEOUT] = _("Server connection timed out.");
    m_disconnected_msg[PDI_NORMAL] = _("Server has been shut down.");
    m_disconnected_msg[PDI_KICK] = _("You were kicked from the server.");
    m_disconnected_msg[PDI_KICK_HIGH_PING] =
        _("You were kicked: Ping too high.");
    m_first_connect = true;
    m_spectator = false;
    m_server_live_joinable = false;
    m_server_send_live_load_world = false;
    m_server_enabled_chat = true;
    m_server_enabled_track_voting = true;
    m_server_enabled_report_player = false;
}   // ClientLobby

//-----------------------------------------------------------------------------
ClientLobby::~ClientLobby()
{
    if (m_server->supportsEncryption())
    {
        auto request = std::make_shared<Online::XMLRequest>();
        NetworkConfig::get()->setServerDetails(request,
            "clear-user-joined-server");
        request->queue();
    }
}   // ClientLobby

//-----------------------------------------------------------------------------
void ClientLobby::setup()
{
    m_ranking_changes.clear();
    m_spectator = false;
    m_server_send_live_load_world = false;
    m_auto_back_to_lobby_time = std::numeric_limits<uint64_t>::max();
    m_start_live_game_time = std::numeric_limits<uint64_t>::max();
    m_received_server_result = false;
    if (!GUIEngine::isNoGraphics())
        TracksScreen::getInstance()->resetVote();
    LobbyProtocol::setup();
    // The client lobby is only created when connected to server
    m_state.store(LINKED);
}   // setup

//-----------------------------------------------------------------------------
/** Called from the gui when a client clicked on 'continue' on the race result
 *  screen. It notifies the server that this client has exited the screen and
 *  is back at the lobby.
 */
void ClientLobby::doneWithResults()
{
    NetworkString* done = getNetworkString(1);
    done->setSynchronous(true);
    done->addUInt8(LE_RACE_FINISHED_ACK);
    sendToServer(done, /*reliable*/true);
    delete done;
}   // doneWithResults

//-----------------------------------------------------------------------------
bool ClientLobby::notifyEvent(Event* event)
{
    assert(m_game_setup); // assert that the setup exists
    if (event->getType() != EVENT_TYPE_MESSAGE)
        return true;

    NetworkString &data = event->data();
    assert(data.size()); // assert that data isn't empty
    uint8_t message_type = data.getUInt8();
    Log::info("ClientLobby", "Synchronous message of type %d",
              message_type);
    switch(message_type)
    {
        case LE_START_SELECTION:       startSelection(event);      break;
        case LE_LOAD_WORLD:            addAllPlayers(event);       break;
        case LE_RACE_FINISHED:         raceFinished(event);        break;
        case LE_BACK_LOBBY:            backToLobby(event);         break;
        case LE_UPDATE_PLAYER_LIST:    updatePlayerList(event);    break;
        case LE_CHAT:                  handleChat(event);          break;
        case LE_CONNECTION_ACCEPTED:   connectionAccepted(event);  break;
        case LE_SERVER_INFO:           handleServerInfo(event);    break;
        case LE_PLAYER_DISCONNECTED :  disconnectedPlayer(event);  break;
        case LE_CONNECTION_REFUSED:    connectionRefused(event);   break;
        case LE_VOTE:                  receivePlayerVote(event);   break;
        case LE_SERVER_OWNERSHIP:      becomingServerOwner();      break;
        case LE_BAD_TEAM:              handleBadTeam();            break;
        case LE_BAD_CONNECTION:        handleBadConnection();      break;
        case LE_LIVE_JOIN_ACK:        liveJoinAcknowledged(event); break;
        case LE_KART_INFO:             handleKartInfo(event);      break;
        case LE_START_RACE:            startGame(event);           break;
        case LE_REPORT_PLAYER:         reportSuccess(event);       break;
        default:
            break;
    }   // switch
    return true;
}   // notifyEvent

//-----------------------------------------------------------------------------
bool ClientLobby::notifyEventAsynchronous(Event* event)
{
    assert(m_game_setup); // assert that the setup exists
    if (event->getType() == EVENT_TYPE_MESSAGE)
    {
        NetworkString &data = event->data();
        assert(data.size()); // assert that data isn't empty
        uint8_t message_type = data.getUInt8();

        Log::info("ClientLobby", "Asynchronous message of type %d",
                  message_type);
        switch(message_type)
        {
            default:                                                     break;
        }   // switch
    } // message
    else if (event->getType() == EVENT_TYPE_DISCONNECTED) 
    {
        // This means we left essentially.
        // We can't delete STKHost from this thread, since the main
        // thread might still test if STKHost exists and then call
        // the ProtocolManager, which might already have been deleted.
        // So only signal that STKHost should exit, which will be tested
        // from the main thread.
        STKHost::get()->disconnectAllPeers(false/*timeout_waiting*/);
        STKHost::get()->setErrorMessage(
            m_disconnected_msg.at(event->getPeerDisconnectInfo()));
        STKHost::get()->requestShutdown();
    } // disconnection
    return true;
}   // notifyEventAsynchronous

//-----------------------------------------------------------------------------
void ClientLobby::addAllPlayers(Event* event)
{
    if (World::getWorld())
    {
        Log::warn("ClientLobby", "World already loaded.");
        return;
    }

    // In case the user opened a user info dialog
    GUIEngine::ModalDialog::dismiss();
    GUIEngine::ScreenKeyboard::dismiss();

    if (!checkDataSize(event, 1))
    {
        // If recieved invalid message for players leave now
        STKHost::get()->disconnectAllPeers(false/*timeout_waiting*/);
        STKHost::get()->requestShutdown();
        return;
    }
    // Timeout is too slow to synchronize, force it to stop and set current
    // time
    if (!STKHost::get()->getNetworkTimerSynchronizer()->isSynchronised())
    {
        if (ServerConfig::m_voting_timeout >= 10.0f)
        {
            core::stringw msg = _("Bad network connection is detected.");
            MessageQueue::add(MessageQueue::MT_ERROR, msg);
            Log::warn("ClientLobby", "Failed to synchronize timer before game "
                "start, maybe you enter the game too quick? (at least 5 "
                "seconds are required for synchronization.");
        }
        STKHost::get()->getNetworkTimerSynchronizer()->enableForceSetTimer();
    }

    NetworkString& data = event->data();
    uint32_t winner_peer_id = data.getUInt32();
    PeerVote winner_vote(data);

    m_game_setup->setRace(winner_vote);
    if (!GUIEngine::isNoGraphics())
        TracksScreen::getInstance()->setResult(winner_peer_id, winner_vote);

    std::shared_ptr<STKPeer> peer = event->getPeerSP();
    peer->cleanPlayerProfiles();
    m_server_send_live_load_world = data.getUInt8() == 1;

    bool is_specator = true;
    std::vector<std::shared_ptr<NetworkPlayerProfile> > players =
        decodePlayers(data, peer, &is_specator);
    setSpectator(is_specator);

    uint32_t random_seed = data.getUInt32();
    ItemManager::updateRandomSeed(random_seed);
    if (RaceManager::get()->isBattleMode())
    {
        int hit_capture_limit = data.getUInt32();
        float time_limit = data.getFloat();
        m_game_setup->setHitCaptureTime(hit_capture_limit, time_limit);
        uint16_t flag_return_timeout = data.getUInt16();
        RaceManager::get()->setFlagReturnTicks(flag_return_timeout);
        unsigned flag_deactivated_time = data.getUInt16();
        RaceManager::get()->setFlagDeactivatedTicks(flag_deactivated_time);
    }
    configRemoteKart(players, isSpectator() ? 1 :
        (int)NetworkConfig::get()->getNetworkPlayers().size());
    loadWorld();
    // Disable until render gui during loading is bug free
    //StateManager::get()->enterGameState();

    if (m_server_send_live_load_world)
    {
        World* w = World::getWorld();
        w->setLiveJoinWorld(true);
        Camera* cam = Camera::getCamera(0);
        for (unsigned i = 0; i < w->getNumKarts(); i++)
        {
            AbstractKart* k = w->getKart(i);
            // Change spectating target to first non-eliminated kart
            if (isSpectator() && cam && !k->isEliminated())
            {
                cam->setKart(k);
                cam = NULL;
            }
            // The final joining ticks will be set by server later
            if (k->getController()->isLocalPlayerController())
                k->setLiveJoinKart(std::numeric_limits<int>::max());
            else
                k->getNode()->setVisible(false);
        }
    }

    // Switch to assign mode in case a player hasn't chosen any karts
    input_manager->getDeviceManager()->setAssignMode(ASSIGN);
}   // addAllPlayers

//-----------------------------------------------------------------------------
/* Get list of players from server and see if we are spectating it. */
std::vector<std::shared_ptr<NetworkPlayerProfile> >
  ClientLobby::decodePlayers(const BareNetworkString& data,
                             std::shared_ptr<STKPeer> peer,
                             bool* is_spectator) const
{
    std::vector<std::shared_ptr<NetworkPlayerProfile> > players;
    unsigned player_count = data.getUInt8();
    for (unsigned i = 0; i < player_count; i++)
    {
        core::stringw player_name;
        data.decodeStringW(&player_name);
        uint32_t host_id = data.getUInt32();
        float kart_color = data.getFloat();
        uint32_t online_id = data.getUInt32();
        HandicapLevel handicap = (HandicapLevel)data.getUInt8();
        uint8_t local_id = data.getUInt8();
        KartTeam team = (KartTeam)data.getUInt8();
        std::string country_code;
        data.decodeString(&country_code);
        if (is_spectator && host_id == STKHost::get()->getMyHostId())
            *is_spectator = false;
        auto player = std::make_shared<NetworkPlayerProfile>(peer, player_name,
            host_id, kart_color, online_id, handicap, local_id, team, country_code);
        std::string kart_name;
        data.decodeString(&kart_name);
        player->setKartName(kart_name);
        players.push_back(player);
    }
    return players;
}   // decodePlayers

//-----------------------------------------------------------------------------
void ClientLobby::update(int ticks)
{
    switch (m_state.load())
    {
    case LINKED:
    {
        NetworkConfig::get()->clearServerCapabilities();
        std::string ua = StringUtils::getUserAgentString();
        if (NetworkConfig::get()->isNetworkAIInstance())
            ua = "AI";
        NetworkString* ns = getNetworkString();
        ns->addUInt8(LE_CONNECTION_REQUESTED)
            .addUInt32(ServerConfig::m_server_version).encodeString(ua)
            .addUInt16((uint16_t)stk_config->m_network_capabilities.size());
        for (const std::string& cap : stk_config->m_network_capabilities)
            ns->encodeString(cap);

        getKartsTracksNetworkString(ns);
        assert(!NetworkConfig::get()->isAddingNetworkPlayers());
        const uint8_t player_count =
            (uint8_t)NetworkConfig::get()->getNetworkPlayers().size();
        ns->addUInt8(player_count);

        bool encryption = false;
        // Make sure there is a player before calling getCurrentOnlineId
        PlayerManager::get()->enforceCurrentPlayer();
        uint32_t id = PlayerManager::getCurrentOnlineId();
        bool lan_ai = !m_server->supportsEncryption() &&
            NetworkConfig::get()->isNetworkAIInstance();
        if (lan_ai)
            id = 0;
        BareNetworkString* rest = new BareNetworkString();
        if (m_server->supportsEncryption() && id != 0)
        {
            ns->addUInt32(id);
            encryption = true;
        }
        else
        {
            ns->addUInt32(id).addUInt32(0);
            if (id != 0)
            {
                ns->encodeString(
                    PlayerManager::getCurrentOnlineProfile()->getUserName());
            }
        }

        rest->encodeString(ServerConfig::m_private_server_password)
            .addUInt8(player_count);
        for (unsigned i = 0;
             i < NetworkConfig::get()->getNetworkPlayers().size(); i++)
        {
            auto& p = NetworkConfig::get()->getNetworkPlayers()[i];
            PlayerProfile* player = std::get<1>(p);
            core::stringw name = player->getName();
            if (NetworkConfig::get()->isNetworkAIInstance())
            {
                // I18N: Shown in lobby to indicate it's a bot in LAN game
#ifdef SERVER_ONLY
                name = L"Bot";
#else
                name = _("Bot");
#endif
                if (i > 0)
                {
                    name += core::stringw(" ") + StringUtils::toWString(i);
                }
            }
            rest->encodeString(name).
                addFloat(player->getDefaultKartColor());
            // Per-player handicap
            rest->addUInt8(std::get<2>(p));
        }

        finalizeConnectionRequest(ns, rest, encryption);
        m_state.store(REQUESTING_CONNECTION);
    }
    break;
    case RACE_FINISHED:
        if (!RaceEventManager::get()->protocolStopped() ||
            !GameProtocol::emptyInstance())
            return;
        if (!m_received_server_result)
        {
            m_received_server_result = true;
            m_auto_back_to_lobby_time = StkTime::getMonoTimeMs() + 5000;
            // In case someone opened paused race dialog or menu in network game
            GUIEngine::ModalDialog::dismiss();
            GUIEngine::ScreenKeyboard::dismiss();
            if (StateManager::get()->getGameState() == GUIEngine::INGAME_MENU)
                StateManager::get()->enterGameState();
            World::getWorld()->enterRaceOverState();
        }
        if (NetworkConfig::get()->isAutoConnect() &&
            StkTime::getMonoTimeMs() > m_auto_back_to_lobby_time)
        {
            m_auto_back_to_lobby_time = std::numeric_limits<uint64_t>::max();
            doneWithResults();
        }
        break;
    case DONE:
        m_state.store(EXITING);
        requestTerminate();
        break;
    case REQUESTING_CONNECTION:
    case CONNECTED:
        if (m_start_live_game_time != std::numeric_limits<uint64_t>::max() &&
            STKHost::get()->getNetworkTimer() >= m_start_live_game_time)
        {
            finishLiveJoin();
        }
        if (NetworkConfig::get()->isAutoConnect() && !m_auto_started)
        {
            // Send a message to the server to start
            m_auto_started = true;
            NetworkString start(PROTOCOL_LOBBY_ROOM);
            start.addUInt8(LobbyProtocol::LE_REQUEST_BEGIN);
            STKHost::get()->sendToServer(&start, true);
        }
        if (m_background_download.joinable())
        {
            if (m_download_request && (m_download_request->isCancelled() ||
                m_download_request->isDone()))
            {
                m_background_download.join();
                if (m_download_request->isDone())
                    doInstallAddonsPack();
                m_download_request = nullptr;
            }
        }
    case SELECTING_ASSETS:
    case RACING:
    case EXITING:
    case NONE:
        break;
    }
}   // update

//-----------------------------------------------------------------------------
void ClientLobby::finalizeConnectionRequest(NetworkString* header,
                                            BareNetworkString* rest,
                                            bool encrypt)
{
    if (encrypt)
    {
        auto crypto = Crypto::getClientCrypto();
        Crypto::resetClientAES();
        BareNetworkString* result = new BareNetworkString();
        if (!crypto->encryptConnectionRequest(*rest))
        {
            // Failed
            result->addUInt32(0);
            *result += BareNetworkString(rest->getData(), rest->getTotalSize());
            encrypt = false;
        }
        else
        {
            Log::info("ClientLobby", "Server will validate this online player.");
            result->addUInt32(rest->getTotalSize());
            *result += BareNetworkString(rest->getData(), rest->getTotalSize());
        }
        delete rest;
        *header += *result;
        delete result;
        sendToServer(header);
        delete header;
        if (encrypt)
        {
            STKHost::get()->getServerPeerForClient()
                ->setCrypto(std::move(crypto));
        }
    }
    else
    {
        *header += *rest;
        delete rest;
        sendToServer(header);
        delete header;
    }
}   // finalizeConnectionRequest

//-----------------------------------------------------------------------------
void ClientLobby::receivePlayerVote(Event* event)
{
    if (!checkDataSize(event, 4)) return;
    // Get the player name who voted
    NetworkString& data = event->data();
    uint32_t host_id = data.getUInt32();
    PeerVote vote(data);
    Log::debug("ClientLobby",
        "Vote from server: host %d, track %s, laps %d, reverse %d.",
        host_id, vote.m_track_name.c_str(), vote.m_num_laps, vote.m_reverse);

    Track* track = track_manager->getTrack(vote.m_track_name);
    if (!track)
    {
        Log::fatal("ClientLobby", "Missing track %s",
            vote.m_track_name.c_str());
    }
    addVote(host_id, vote);
    if (!GUIEngine::isNoGraphics())
    {
        TracksScreen::getInstance()->addVote(host_id, vote);
        TracksScreen::getInstance()->updatePlayerVotes();
    }
}   // receivePlayerVote

//-----------------------------------------------------------------------------
/*! \brief Called when a new player is disconnected
 *  \param event : Event providing the information.
 *
 *  Format of the data :
 *  Byte 0 
 *       --------------
 *  Size |    1       |
 *  Data | player id *|
 *       --------------
 */
void ClientLobby::disconnectedPlayer(Event* event)
{
    if (!checkDataSize(event, 1)) return;

    NetworkString &data = event->data();
    unsigned disconnected_player_count = data.getUInt8();
    uint32_t host_id = data.getUInt32();
    m_peers_votes.erase(host_id);
    // If in-game world exists the kart rewinder will know which player
    // disconnects
    bool in_game_world = World::getWorld() &&
        RaceEventManager::get() &&
        RaceEventManager::get()->isRunning() &&
        !RaceEventManager::get()->isRaceOver();

    if (!in_game_world)
        SFXManager::get()->quickSound("appear");
    for (unsigned i = 0; i < disconnected_player_count; i++)
    {
        std::string name;
        data.decodeString(&name);
        if (in_game_world)
            continue;
        core::stringw player_name = StringUtils::utf8ToWide(name);
        core::stringw msg = _("%s disconnected.", player_name);
        // Use the friend icon to avoid an error-like message
        MessageQueue::add(MessageQueue::MT_FRIEND, msg);
    }
    if (!GUIEngine::isNoGraphics())
    {
        TracksScreen::getInstance()->removeVote(host_id);
        TracksScreen::getInstance()->updatePlayerVotes();
    }
}   // disconnectedPlayer

//-----------------------------------------------------------------------------
/*! \brief Called when the server accepts the connection.
 *  \param event : Event providing the information.
 */
void ClientLobby::connectionAccepted(Event* event)
{
    // At least 8 bytes should remain now
    if (!checkDataSize(event, 8)) return;

    NetworkString &data = event->data();
    // Accepted
    // ========
    Log::info("ClientLobby", "The server accepted the connection.");

    static bool shown_msg = false;
    if (!shown_msg)
    {
        shown_msg = true;
        // I18N: Message shown in network lobby to tell user that
        // player name is clickable
        core::stringw msg = _("Press player name in the list for player"
            " management and ranking information.");
        MessageQueue::add(MessageQueue::MT_GENERIC, msg);
    }

    uint32_t host_id = data.getUInt32();
    STKHost::get()->setMyHostId(host_id);
    if (auto sl = LobbyProtocol::getByType<ServerLobby>(PT_CHILD))
        sl->setClientServerHostId(host_id);

    assert(!NetworkConfig::get()->isAddingNetworkPlayers());
    uint32_t server_version = data.getUInt32();
    NetworkConfig::get()->setJoinedServerVersion(server_version);
    assert(server_version != 0);
    m_auto_started = false;
    m_state.store(CONNECTED);

    unsigned list_caps = data.getUInt16();
    std::set<std::string> caps;
    for (unsigned i = 0; i < list_caps; i++)
    {
        std::string cap;
        data.decodeString(&cap);
        caps.insert(cap);
    }
    NetworkConfig::get()->setServerCapabilities(caps);

    float auto_start_timer = data.getFloat();
    int state_frequency_in_server = data.getUInt32();
    NetworkConfig::get()->setStateFrequency(state_frequency_in_server);
    if (!GUIEngine::isNoGraphics() &&
        auto_start_timer != std::numeric_limits<float>::max())
        NetworkingLobby::getInstance()->setStartingTimerTo(auto_start_timer);
    m_server_enabled_chat = data.getUInt8() == 1;
    if (NetworkConfig::get()->getServerCapabilities().find("report_player") !=
        NetworkConfig::get()->getServerCapabilities().end())
        m_server_enabled_report_player = data.getUInt8() == 1;
}   // connectionAccepted

//-----------------------------------------------------------------------------
void ClientLobby::handleServerInfo(Event* event)
{
    // At least 6 bytes should remain now
    if (!checkDataSize(event, 6)) return;

    core::stringw str, total_lines;
    if (!m_first_connect)
    {
        total_lines = L"--------------------";
        total_lines += L"\n";
    }
    m_first_connect = false;

    NetworkString &data = event->data();
    // Add server info
    uint8_t u_data;
    data.decodeStringW(&str);
    str.removeChars(L"\n\r\t");

    NetworkingLobby::getInstance()->setHeader(str);

    u_data = data.getUInt8();
    const core::stringw& difficulty_name =
        RaceManager::get()->getDifficultyName((RaceManager::Difficulty)u_data);
    RaceManager::get()->setDifficulty((RaceManager::Difficulty)u_data);
    //I18N: In the networking lobby
    total_lines += _("Difficulty: %s", difficulty_name);
    total_lines += L"\n";

    unsigned max_player = data.getUInt8();
    //I18N: In the networking lobby
    total_lines += _("Max players: %d", (int)max_player);
    total_lines += L"\n";

    // Reserved for extra spectators
    u_data = data.getUInt8();
    u_data = data.getUInt8();
    auto game_mode = ServerConfig::getLocalGameMode(u_data);
    RaceManager::get()->setMinorMode(game_mode.first);
    // We use single mode in network even it's grand prix
    RaceManager::get()->setMajorMode(RaceManager::MAJOR_MODE_SINGLE);

    //I18N: In the networking lobby
    core::stringw mode_name = ServerConfig::getModeName(u_data);
    total_lines += _("Game mode: %s", mode_name);
    total_lines += L"\n";

    uint8_t extra_server_info = data.getUInt8();
    bool grand_prix_started = false;
    m_game_setup->resetExtraServerInfo();
    switch (extra_server_info)
    {
        case 0:
            break;
        case 1:
        {
            u_data = data.getUInt8();
            core::stringw tl = _("Time limit");
            core::stringw gl = _("Goals limit");
            core::stringw sgt = u_data == 0 ? tl : gl;
            m_game_setup->setSoccerGoalTarget(u_data != 0);
            //I18N: In the networking lobby
            total_lines += _("Soccer game type: %s", sgt);
            total_lines += L"\n";
            break;
        }
        case 2:
        {
            unsigned cur_gp_track = data.getUInt8();
            grand_prix_started = cur_gp_track != 0;
            unsigned total_gp_track = data.getUInt8();
            m_game_setup->setGrandPrixTrack(total_gp_track);
            total_lines += _("Grand prix progress: %d / %d", cur_gp_track,
                total_gp_track);
            total_lines += L"\n";
            break;
        }
    }
    // Auto start info
    unsigned min_players = data.getUInt8();
    float start_timeout = data.getFloat();
    if (!GUIEngine::isNoGraphics())
    {
        NetworkingLobby::getInstance()->initAutoStartTimer(grand_prix_started,
            min_players, start_timeout, max_player);
    }

    // MOTD
    core::stringw motd;
    data.decodeString16(&motd);
    if (!motd.empty())
        total_lines += motd;

    // Remove last newline added, network lobby will add it back later after
    // removing old server info (with chat)
    if (total_lines[total_lines.size() - 1] == L'\n')
        total_lines.erase(total_lines.size() - 1);

    if (!GUIEngine::isNoGraphics())
        NetworkingLobby::getInstance()->addMoreServerInfo(total_lines);

    bool server_config = data.getUInt8() == 1;
    if (!GUIEngine::isNoGraphics())
        NetworkingLobby::getInstance()->toggleServerConfigButton(server_config);
    m_server_live_joinable = data.getUInt8() == 1;
    NetworkConfig::get()->setTuxHitboxAddon(m_server_live_joinable);
}   // handleServerInfo

//-----------------------------------------------------------------------------
void ClientLobby::updatePlayerList(Event* event)
{
    if (!checkDataSize(event, 1)) return;
    NetworkString& data = event->data();
    bool waiting = data.getUInt8() == 1;
    if (m_waiting_for_game && !waiting)
    {
        // The waiting game finished
        SFXManager::get()->quickSound("wee");
    }

    m_waiting_for_game = waiting;
    unsigned player_count = data.getUInt8();
    core::stringw total_players;
    m_lobby_players.clear();
    bool client_server_owner = false;
    for (unsigned i = 0; i < player_count; i++)
    {
        LobbyPlayer lp = {};
        lp.m_host_id = data.getUInt32();
        lp.m_online_id = data.getUInt32();
        uint8_t local_id = data.getUInt8();
        lp.m_handicap = HANDICAP_NONE;
        lp.m_local_player_id = local_id;
        data.decodeStringW(&lp.m_user_name);
        total_players += lp.m_user_name;
        uint8_t boolean_combine = data.getUInt8();
        bool is_peer_waiting_for_game = (boolean_combine & 1) == 1;
        bool is_spectator = ((boolean_combine >> 1) & 1) == 1;
        bool is_peer_server_owner = ((boolean_combine >> 2) & 1) == 1;
        bool ready = ((boolean_combine >> 3) & 1) == 1;
        bool ai = ((boolean_combine >> 4) & 1) == 1;
        // icon to be used, see NetworkingLobby::loadedFromFile
        lp.m_icon_id = is_peer_server_owner ? 0 :
            lp.m_online_id != 0 /*if online account*/ ? 1 : 2;
        if (ai)
            lp.m_icon_id = 6;
        if (waiting && !is_peer_waiting_for_game)
            lp.m_icon_id = 3;
        if (is_spectator)
            lp.m_icon_id = 5;
        if (ready)
            lp.m_icon_id = 4;
        lp.m_handicap = (HandicapLevel)data.getUInt8();
        if (lp.m_handicap != HANDICAP_NONE)
        {
            lp.m_user_name = _("%s (handicapped)", lp.m_user_name);
        }
        KartTeam team = (KartTeam)data.getUInt8();
        if (is_spectator)
            lp.m_kart_team = KART_TEAM_NONE;
        else
            lp.m_kart_team = team;
        // No handicap for AI peer
        if (!ai && lp.m_host_id == STKHost::get()->getMyHostId())
        {
            if (is_peer_server_owner)
                client_server_owner = true;
            auto& local_players = NetworkConfig::get()->getNetworkPlayers();
            std::get<2>(local_players.at(local_id)) = lp.m_handicap;
        }
        data.decodeString(&lp.m_country_code);
        m_lobby_players.push_back(lp);
    }
    STKHost::get()->setAuthorisedToControl(client_server_owner);

    // Notification sound for new player
    if (!m_total_players.empty() &&
        total_players.size() > m_total_players.size())
        SFXManager::get()->quickSound("energy_bar_full");
    m_total_players = total_players;

    if (!GUIEngine::isNoGraphics())
        NetworkingLobby::getInstance()->updatePlayers();
}   // updatePlayerList

//-----------------------------------------------------------------------------
void ClientLobby::handleBadTeam()
{
    SFXManager::get()->quickSound("anvil");
    //I18N: Display when all players are in red or blue team, which the race
    //will not be allowed to start
    core::stringw msg = _("All players joined red or blue team.");
    MessageQueue::add(MessageQueue::MT_ERROR, msg);
}   // handleBadTeam

//-----------------------------------------------------------------------------
void ClientLobby::handleBadConnection()
{
    SFXManager::get()->quickSound("anvil");
    core::stringw msg = _("Bad network connection is detected.");
    MessageQueue::add(MessageQueue::MT_ERROR, msg);
}   // handleBadConnection

//-----------------------------------------------------------------------------
void ClientLobby::becomingServerOwner()
{
    if (STKHost::get()->isClientServer())
        return;

    SFXManager::get()->quickSound("wee");
    //I18N: Display when a player is allow to control the server
    core::stringw msg = _("You are now the owner of server.");
    MessageQueue::add(MessageQueue::MT_GENERIC, msg);
}   // becomingServerOwner

//-----------------------------------------------------------------------------
void ClientLobby::handleChat(Event* event)
{
    if (!UserConfigParams::m_lobby_chat)
        return;
    SFXManager::get()->quickSound("plopp");
    core::stringw message;
    event->data().decodeString16(&message);
    Log::info("ClientLobby", "%s", StringUtils::wideToUtf8(message).c_str());
    if (GUIEngine::isNoGraphics())
        return;
    if (message.size() > 0)
    {
        if (GUIEngine::getCurrentScreen() == NetworkingLobby::getInstance())
            NetworkingLobby::getInstance()->addMoreServerInfo(message);
        else if (UserConfigParams::m_race_chat)
            MessageQueue::add(MessageQueue::MT_GENERIC, message);
    }
}   // handleChat

//-----------------------------------------------------------------------------
/*! \brief Called when the server refuses the connection.
 *  \param event : Event providing the information.
 *
 *  Format of the data :
 *  Byte 0 
 *       ----------------
 *  Size |      1       |
 *  Data | refusal code |
 *       ----------------
 */
void ClientLobby::connectionRefused(Event* event)
{
    if (!checkDataSize(event, 1)) return;
    const NetworkString &data = event->data();
    switch ((RejectReason)data.getUInt8()) // the second byte
    {
    case RR_BUSY:
        STKHost::get()->setErrorMessage(
            _("Connection refused: Server is busy."));
        break;
    case RR_BANNED:
    {
        core::stringw msg =
            _("Connection refused: You are banned from the server.");
        core::stringw reason;
        data.decodeStringW(&reason);
        if (!reason.empty())
        {
            msg += L"\n";
            msg += reason;
        }
        STKHost::get()->setErrorMessage(msg);
        break;
    }
    case RR_INCORRECT_PASSWORD:
        m_server->setReconnectWhenQuitLobby(true);
        m_server->setIsPasswordProtected(true);
        STKHost::get()->setErrorMessage(
            _("Connection refused: Server password is incorrect."));
        break;
    case RR_INCOMPATIBLE_DATA:
        STKHost::get()->setErrorMessage(
            _("Connection refused: Game data is incompatible."));
        break;
    case RR_TOO_MANY_PLAYERS:
        STKHost::get()->setErrorMessage(
            _("Connection refused: Server is full."));
        break;
    case RR_INVALID_PLAYER:
        STKHost::get()->setErrorMessage(
            _("Connection refused: Invalid player connecting."));
        break;
    }
    STKHost::get()->disconnectAllPeers(false/*timeout_waiting*/);
    STKHost::get()->requestShutdown();
}   // connectionRefused

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

/*! \brief Called when the server broadcasts to start the race to all clients.
 *  \param event : Event providing the time the client should start game.
 */
void ClientLobby::startGame(Event* event)
{
    World::getWorld()->setPhase(WorldStatus::SERVER_READY_PHASE);
    uint64_t start_time = event->data().getUInt64();
    powerup_manager->setRandomSeed(start_time);

    unsigned check_structure_count = event->data().getUInt8();
    LinearWorld* lw = dynamic_cast<LinearWorld*>(World::getWorld());
    if (lw)
        lw->handleServerCheckStructureCount(check_structure_count);

    NetworkItemManager* nim = dynamic_cast<NetworkItemManager*>
        (Track::getCurrentTrack()->getItemManager());
    assert(nim);
    nim->restoreCompleteState(event->data());

    core::stringw err_msg = _("Failed to start the network game.");
    // Different stk process thread may have different stk host
    STKHost* stk_host = STKHost::get();
    joinStartGameThread();
    m_start_game_thread = std::thread([start_time, stk_host, this, err_msg]()
        {
            const uint64_t cur_time = stk_host->getNetworkTimer();
            if (!(start_time > cur_time))
            {
                Log::error("ClientLobby", "Network timer is too slow to catch "
                    "up, you must have a poor network.");
                stk_host->setErrorMessage(err_msg);
                stk_host->requestShutdown();
                return;
            }
            int sleep_time = (int)(start_time - cur_time);
            //Log::info("ClientLobby", "Start game after %dms", sleep_time);
            StkTime::sleep(sleep_time);
            //Log::info("ClientLobby", "Started at %lf", StkTime::getRealTime());
            m_state.store(RACING);
        });
}   // startGame

//-----------------------------------------------------------------------------
/*! \brief Called when the kart selection starts.
 *  \param event : Event providing the information (no additional information
 *                 in this case).
 */
void ClientLobby::startSelection(Event* event)
{
    SFXManager::get()->quickSound("wee");
    const NetworkString& data = event->data();
    startVotingPeriod(data.getFloat());
    bool skip_kart_screen = data.getUInt8() == 1;
    m_server_auto_game_time = data.getUInt8() == 1;
    m_server_enabled_track_voting = data.getUInt8() == 1;
    const unsigned kart_num = data.getUInt16();
    const unsigned track_num = data.getUInt16();
    m_available_karts.clear();
    m_available_tracks.clear();
    for (unsigned i = 0; i < kart_num; i++)
    {
        std::string kart;
        data.decodeString(&kart);
        m_available_karts.insert(kart);
    }
    for (unsigned i = 0; i < track_num; i++)
    {
        std::string track;
        data.decodeString(&track);
        m_available_tracks.insert(track);
    }

    // In case the user opened a user info dialog
    GUIEngine::ModalDialog::dismiss();
    GUIEngine::ScreenKeyboard::dismiss();

    if (!GUIEngine::isNoGraphics())
    {
        NetworkKartSelectionScreen* screen =
            NetworkKartSelectionScreen::getInstance();
        screen->setAvailableKartsFromServer(m_available_karts);
        screen->setLiveJoin(false);
    }
    // In case of auto-connect or continue a grand prix, use random karts
    // (or previous kart) from server and go to track selection
    if ((NetworkConfig::get()->isAutoConnect() || skip_kart_screen) &&
        m_server_enabled_track_voting)
    {
        input_manager->setMasterPlayerOnly(true);
        for (auto& p : NetworkConfig::get()->getNetworkPlayers())
        {
            StateManager::get()
                ->createActivePlayer(std::get<1>(p), std::get<0>(p));
        }
        input_manager->getDeviceManager()->setAssignMode(ASSIGN);
        if (!GUIEngine::isNoGraphics())
        {
            TracksScreen::getInstance()->setQuitServer();
            TracksScreen::getInstance()->setNetworkTracks();
            TracksScreen::getInstance()->push();
        }
    }
    else if (!GUIEngine::isNoGraphics())
    {
        NetworkKartSelectionScreen::getInstance()->push();
    }

    if (!GUIEngine::isNoGraphics())
    {
        TracksScreen *ts = TracksScreen::getInstance();
        ts->resetVote();
    }
    m_state.store(SELECTING_ASSETS);
    Log::info("ClientLobby", "Selection starts now");
}   // startSelection

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

/*! \brief Called when all karts have finished the race.
 *  \param event : Event providing the information.
 *
 *  Format of the data :
 *  Byte 0           1
 *       -------------------------------
 *  Size |     1     |     1     |     |
 *  Data | Kart 1 ID | kart id 2 | ... |
 *       -------------------------------
 */
void ClientLobby::raceFinished(Event* event)
{
    NetworkString &data = event->data();
    Log::info("ClientLobby", "Server notified that the race is finished.");
    LinearWorld* lw = dynamic_cast<LinearWorld*>(World::getWorld());
    if (m_game_setup->isGrandPrix())
    {
        int t = data.getUInt32();
        core::stringw kart_name;
        data.decodeStringW(&kart_name);
        lw->setFastestLapTicks(t);
        lw->setFastestKartName(kart_name);
        RaceManager::get()->configGrandPrixResultFromNetwork(data);
    }
    else if (RaceManager::get()->modeHasLaps())
    {
        int t = data.getUInt32();
        core::stringw kart_name;
        data.decodeStringW(&kart_name);
        lw->setFastestLapTicks(t);
        lw->setFastestKartName(kart_name);
    }

    if (lw)
    {
        // Eliminate all karts which have not finished the race, it can happen
        // if the last player leave the game instead of crossing the finish
        // line
        lw->updateRacePosition();
        for (unsigned i = 0; i < lw->getNumKarts(); i++)
        {
            AbstractKart* k = lw->getKart(i);
            if (!k->hasFinishedRace() && !k->isEliminated())
            {
                core::stringw player_name = k->getController()->getName();
                core::stringw msg = _("%s left the game.", player_name);
                MessageQueue::add(MessageQueue::MT_FRIEND, msg);
                World::getWorld()->eliminateKart(i,
                    false/*notify_of_elimination*/);
                k->finishedRace(World::getWorld()->getTime(),
                    true/*from_server*/);
            }
        }
    }

    m_ranking_changes.clear();
    // Ranking changes from server
    if (NetworkConfig::get()->getServerCapabilities().find("ranking_changes")
        != NetworkConfig::get()->getServerCapabilities().end())
    {
        bool has_ranking_changes = (data.getUInt8() & 1) != 0;
        if (has_ranking_changes)
        {
            unsigned count = data.getUInt8();
            for (unsigned i = 0; i < count; i++)
                m_ranking_changes.push_back(data.getFloat());
        }
    }

    // stop race protocols
    RaceEventManager::get()->stop();
    ProtocolManager::lock()->findAndTerminate(PROTOCOL_GAME_EVENTS);
    ProtocolManager::lock()->findAndTerminate(PROTOCOL_CONTROLLER_EVENTS);
    m_state.store(RACE_FINISHED);
}   // raceFinished

//-----------------------------------------------------------------------------
/** Called when the server informs the clients to exit the race result screen.
 *  It exits the race, and goes back to the lobby.
 */
void ClientLobby::backToLobby(Event *event)
{
    // In case the user opened a user info dialog
    GUIEngine::ModalDialog::dismiss();
    GUIEngine::ScreenKeyboard::dismiss();

    NetworkConfig::get()->clearActivePlayersForClient();
    setup();
    m_auto_started = false;
    m_state.store(CONNECTED);

    if (RaceEventManager::get())
    {
        RaceEventManager::get()->stop();
        ProtocolManager::lock()->findAndTerminate(PROTOCOL_GAME_EVENTS);
    }
    auto gp = GameProtocol::lock();
    if (gp)
    {
        auto lock = gp->acquireWorldDeletingMutex();
        ProtocolManager::lock()->findAndTerminate(PROTOCOL_CONTROLLER_EVENTS);
        exitGameState();
    }
    else
        exitGameState();

    NetworkString &data = event->data();
    core::stringw msg;
    MessageQueue::MessageType mt = MessageQueue::MT_ERROR;

    switch ((BackLobbyReason)data.getUInt8()) // the second byte
    {
    case BLR_NO_GAME_FOR_LIVE_JOIN:
        // I18N: Error message shown if live join or spectate failed in network
        msg = _("The game has ended, you can't live join or spectate anymore.");
        break;
    case BLR_NO_PLACE_FOR_LIVE_JOIN:
        // I18N: Error message shown if live join failed in network
        msg = _("No remaining place in the arena - live join disabled.");
        break;
    case BLR_ONE_PLAYER_IN_RANKED_MATCH:
        // I18N: Error message shown if only 1 player remains in network
        msg = _("Only 1 player remaining, returning to lobby.");
        break;
    case BLR_SERVER_ONWER_QUITED_THE_GAME:
        // I18N: Error message shown when all players will go back to lobby
        // when server owner quited the game
        if (!STKHost::get()->isClientServer())
            msg = _("Server owner quit the game.");
        break;
    case BLR_SPECTATING_NEXT_GAME:
        // I18N: Status shown to player when he will be spectating the next game
        msg = _("You will be spectating the next game.");
        mt = MessageQueue::MT_GENERIC;
        break;
    default:
        break;
    }
    if (!msg.empty())
    {
        if (mt == MessageQueue::MT_GENERIC)
            SFXManager::get()->quickSound("plopp");
        else
            SFXManager::get()->quickSound("anvil");
        MessageQueue::add(mt, msg);
    }
}   // backToLobby

//-----------------------------------------------------------------------------
/** Callback when the world is loaded. The client will inform the server
 *  that the players on this host are ready to start the race. It is called by
 *  the RaceManager after the world is loaded.
 */
void ClientLobby::finishedLoadingWorld()
{
    NetworkString* ns = getNetworkString(1);
    ns->setSynchronous(m_server_send_live_load_world);
    ns->addUInt8(LE_CLIENT_LOADED_WORLD);
    sendToServer(ns, true);
    delete ns;
}   // finishedLoadingWorld

//-----------------------------------------------------------------------------
void ClientLobby::liveJoinAcknowledged(Event* event)
{
    World* w = World::getWorld();
    if (!w)
        return;

    const NetworkString& data = event->data();
    m_start_live_game_time = data.getUInt64();
    powerup_manager->setRandomSeed(m_start_live_game_time);

    unsigned check_structure_count = event->data().getUInt8();
    LinearWorld* lw = dynamic_cast<LinearWorld*>(World::getWorld());
    if (lw)
        lw->handleServerCheckStructureCount(check_structure_count);

    m_start_live_game_time = data.getUInt64();
    m_last_live_join_util_ticks = data.getUInt32();
    for (unsigned i = 0; i < w->getNumKarts(); i++)
    {
        AbstractKart* k = w->getKart(i);
        if (k->getController()->isLocalPlayerController())
            k->setLiveJoinKart(m_last_live_join_util_ticks);
    }

    NetworkItemManager* nim = dynamic_cast<NetworkItemManager*>
        (Track::getCurrentTrack()->getItemManager());
    assert(nim);
    nim->restoreCompleteState(data);
    w->restoreCompleteState(data);

    if (RaceManager::get()->supportsLiveJoining() && data.size() > 0)
    {
        // Get and update the players list 1 more time in case the was
        // player connection or disconnection
        std::vector<std::shared_ptr<NetworkPlayerProfile> > players =
            decodePlayers(data);
        w->resetElimination();
        for (unsigned i = 0; i < players.size(); i++)
        {
            AbstractKart* k = w->getKart(i);
            if (k->getController()->isLocalPlayerController())
                continue;
            k->reset();
            // Only need to change non local player karts
            RemoteKartInfo& rki = RaceManager::get()->getKartInfo(i);
            rki.copyFrom(players[i], players[i]->getLocalPlayerId());
            if (rki.isReserved())
            {
                World::getWorld()->eliminateKart(i,
                    false/*notify_of_elimination*/);
                k->setPosition(
                    World::getWorld()->getCurrentNumKarts() + 1);
                k->finishedRace(World::getWorld()->getTime(),
                    true/*from_server*/);
            }
            else
            {
                // Will be reset once again after live join is finished
                addLiveJoiningKart(i, rki, 0/*live_join_util_ticks*/);
                k->getNode()->setVisible(false);
            }
        }
    }
}   // liveJoinAcknowledged

//-----------------------------------------------------------------------------
void ClientLobby::finishLiveJoin()
{
    m_start_live_game_time = std::numeric_limits<uint64_t>::max();
    World* w = World::getWorld();
    if (!w)
        return;
    Log::info("ClientLobby", "Live join started at %lf",
        StkTime::getRealTime());

    w->setLiveJoinWorld(false);
    w->endLiveJoinWorld(m_last_live_join_util_ticks);
    for (unsigned i = 0; i < w->getNumKarts(); i++)
    {
        AbstractKart* k = w->getKart(i);
        if (!k->getController()->isLocalPlayerController() &&
            !k->isEliminated())
            k->getNode()->setVisible(true);
    }
    m_state.store(RACING);
}   // finishLiveJoin

//-----------------------------------------------------------------------------
void ClientLobby::requestKartInfo(uint8_t kart_id)
{
    NetworkString* ns = getNetworkString(1);
    ns->setSynchronous(true);
    ns->addUInt8(LE_KART_INFO).addUInt8(kart_id);
    sendToServer(ns, true/*reliable*/);
    delete ns;
}   // requestKartInfo

//-----------------------------------------------------------------------------
void ClientLobby::handleKartInfo(Event* event)
{
    World* w = World::getWorld();
    if (!w)
        return;

    const NetworkString& data = event->data();
    int live_join_util_ticks = data.getUInt32();
    uint8_t kart_id = data.getUInt8();
    core::stringw player_name;
    data.decodeStringW(&player_name);
    uint32_t host_id = data.getUInt32();
    float kart_color = data.getFloat();
    uint32_t online_id = data.getUInt32();
    HandicapLevel h = (HandicapLevel)data.getUInt8();
    uint8_t local_id = data.getUInt8();
    std::string kart_name;
    data.decodeString(&kart_name);
    std::string country_code;
    data.decodeString(&country_code);

    RemoteKartInfo& rki = RaceManager::get()->getKartInfo(kart_id);
    rki.setPlayerName(player_name);
    rki.setHostId(host_id);
    rki.setDefaultKartColor(kart_color);
    rki.setOnlineId(online_id);
    rki.setHandicap(h);
    rki.setLocalPlayerId(local_id);
    rki.setKartName(kart_name);
    rki.setCountryCode(country_code);
    addLiveJoiningKart(kart_id, rki, live_join_util_ticks);

    core::stringw msg;
    if (RaceManager::get()->teamEnabled())
    {
        if (w->getKartTeam(kart_id) == KART_TEAM_RED)
        {
            // I18N: Show when player join red team of the started game in
            // network
            msg = _("%s joined the red team.", player_name);
        }
        else
        {
            // I18N: Show when player join blue team of the started game in
            // network
            msg = _("%s joined the blue team.", player_name);
        }
    }
    else
    {
        // I18N: Show when player join the started game in network
        msg = _("%s joined the game.", player_name);
    }
    SFXManager::get()->quickSound("energy_bar_full");
    MessageQueue::add(MessageQueue::MT_FRIEND, msg);
}   // handleKartInfo

//-----------------------------------------------------------------------------
void ClientLobby::startLiveJoinKartSelection()
{
    NetworkKartSelectionScreen::getInstance()->setLiveJoin(true);
    std::vector<int> all_k =
        kart_properties_manager->getKartsInGroup("standard");
    std::set<std::string> karts;
    for (int kart : all_k)
    {
        const KartProperties* kp = kart_properties_manager->getKartById(kart);
        if (!kp->isAddon())
            karts.insert(kp->getIdent());
    }
    NetworkKartSelectionScreen::getInstance()
        ->setAvailableKartsFromServer(karts);
    NetworkKartSelectionScreen::getInstance()->push();
}   // startLiveJoinKartSelection

// ----------------------------------------------------------------------------
void ClientLobby::sendChat(irr::core::stringw text, KartTeam team)
{
    text = text.trim().removeChars(L"\n\r");
    if (text.size() > 0)
    {
        NetworkString* chat = getNetworkString();
        chat->addUInt8(LobbyProtocol::LE_CHAT);

        core::stringw name;
        PlayerProfile* player = PlayerManager::getCurrentPlayer();
        if (PlayerManager::getCurrentOnlineState() ==
            PlayerProfile::OS_SIGNED_IN)
            name = PlayerManager::getCurrentOnlineProfile()->getUserName();
        else
            name = player->getName();
        // Make message look better by aligning to left or right side depends
        // on name and text
        // (LTR: RTL text always right; RTL : LTR text always left)
#ifndef SERVER_ONLY
        if (!name.empty() && !text.empty())
        {
            SBCodepointSequence codepoint_sequence;
            codepoint_sequence.stringEncoding = sizeof(wchar_t) == 2 ?
                SBStringEncodingUTF16 : SBStringEncodingUTF32;
            codepoint_sequence.stringBuffer = (void*)name.c_str();
            codepoint_sequence.stringLength = name.size();
            SBAlgorithmRef bidi_algorithm =
                SBAlgorithmCreate(&codepoint_sequence);
            SBParagraphRef first_paragraph =
                SBAlgorithmCreateParagraph(bidi_algorithm, 0, (int32_t)-1,
                SBLevelDefaultLTR);
            SBLevel name_level = SBParagraphGetBaseLevel(first_paragraph);
            SBParagraphRelease(first_paragraph);
            SBAlgorithmRelease(bidi_algorithm);

            codepoint_sequence.stringBuffer = (void*)text.c_str();
            codepoint_sequence.stringLength = text.size();
            bidi_algorithm =
                SBAlgorithmCreate(&codepoint_sequence);
            first_paragraph =
                SBAlgorithmCreateParagraph(bidi_algorithm, 0, (int32_t)-1,
                SBLevelDefaultLTR);
            SBLevel text_level = SBParagraphGetBaseLevel(first_paragraph);
            SBParagraphRelease(first_paragraph);
            SBAlgorithmRelease(bidi_algorithm);
            if (name_level % 2 == 0 && text_level % 2 != 0)
                name = core::stringw(L"\u200F") + name;
            else if (name_level % 2 != 0 && text_level % 2 == 0)
                name = core::stringw(L"\u200E") + name;
        }
#endif
        chat->encodeString16(name + L": " + text, 1000/*max_len*/);

        if (team != KART_TEAM_NONE)
            chat->addUInt8(team);

        STKHost::get()->sendToServer(chat, true);
        delete chat;
    }
}   // sendChat

// ----------------------------------------------------------------------------
void ClientLobby::changeSpectateTarget(PlayerAction action, int value,
                                       Input::InputType type) const
{
    Camera* cam = Camera::getActiveCamera();
    if (!cam)
        return;

    // Only 1 local player will be able to change target, and this will replace
    // the end camera with normal
    if (cam->getType() != Camera::CM_TYPE_NORMAL)
        Camera::changeCamera(0, Camera::CM_TYPE_NORMAL);

    // Update if the camera again beacuse when race finished cam will be
    // changed above and invalid
    cam = Camera::getActiveCamera();
    if (!cam)
        return;

    // Copied from EventHandler::processGUIAction
    const bool pressed_down = value > Input::MAX_VALUE * 2 / 3;

    if (!pressed_down)
        return;

    if (action == PA_PAUSE_RACE)
    {
        StateManager::get()->escapePressed();
        return;
    }
    if (action == PA_LOOK_BACK)
    {
        if (cam->getMode() == Camera::CM_NORMAL)
            cam->setMode(Camera::CM_REVERSE);
        else
            cam->setMode(Camera::CM_NORMAL);
        return;
    }
    if (action == PA_ACCEL)
    {
        cam->setNextSpectatorMode();
        return;
    }

    WorldWithRank* wwr = dynamic_cast<WorldWithRank*>(World::getWorld());
    if (!wwr)
        return;
    std::vector<AbstractKart*> karts;
    for (unsigned i = 0; i < wwr->getNumKarts(); i++)
        karts.push_back(wwr->getKartAtDrawingPosition(i + 1));

    const int num_karts = (int)karts.size();
    int current_idx = -1;
    if (cam->getKart())
    {
        auto it = std::find(karts.begin(), karts.end(), cam->getKart());
        if (it != karts.end())
            current_idx = (int)std::distance(karts.begin(), it);
    }
    if (current_idx < 0 || current_idx >= num_karts)
        return;

    bool up = false;
    if (action == PA_STEER_LEFT)
        up = false;
    else if (action == PA_STEER_RIGHT)
        up = true;
    else
        return;
    for (int i = 0; i < num_karts; i++)
    {
        current_idx = up ? current_idx + 1 : current_idx - 1;
        // Handle looping
        if (current_idx == -1)
            current_idx = num_karts - 1;
        else if (current_idx == num_karts)
            current_idx = 0;

        if (!karts[current_idx]->isEliminated())
        {
            cam->setKart(karts[current_idx]);
            break;
        }
    }
}   // changeSpectateTarget

// ----------------------------------------------------------------------------
void ClientLobby::addSpectateHelperMessage() const
{
    auto& local_players = NetworkConfig::get()->getNetworkPlayers();
    if (local_players.empty())
        return;
    InputDevice* id = std::get<0>(local_players[0]);
    if (!id)
        return;
    DeviceConfig* dc = id->getConfiguration();
    if (!dc)
        return;
    core::stringw left = dc->getBindingAsString(PA_STEER_LEFT);
    core::stringw right = dc->getBindingAsString(PA_STEER_RIGHT);
    core::stringw back = dc->getBindingAsString(PA_LOOK_BACK);
    core::stringw accel = dc->getBindingAsString(PA_ACCEL);

    // I18N: Message shown in game to tell the player it's possible to change
    // the camera target in spectate mode of network
    core::stringw msg = _("Press <%s> or <%s> to change the targeted player, "
        "<%s> or <%s> for the camera position.", left, right, back, accel);
    MessageQueue::add(MessageQueue::MT_GENERIC, msg);
}   // addSpectateHelperMessage

// ----------------------------------------------------------------------------
void ClientLobby::reportSuccess(Event* event)
{
    bool succeeded = false;
    core::stringw reporting_name;
    succeeded = event->data().getUInt8() == 1;
    if (succeeded)
        event->data().decodeStringW(&reporting_name);
    if (succeeded && !reporting_name.empty())
    {
        // I18N: Tell player he has successfully report this named player
        core::stringw msg = _("Successfully reported %s.", reporting_name);
        MessageQueue::add(MessageQueue::MT_GENERIC, msg);
    }
}   // reportSuccess

// ----------------------------------------------------------------------------
void ClientLobby::handleClientCommand(const std::string& cmd)
{
#ifndef SERVER_ONLY
    auto argv = StringUtils::split(cmd, ' ');
    if (argv.size() == 0)
        return;
    if (argv[0] == "addondownloadprogress")
    {
        float progress = 0.0f;
        if (m_download_request)
            progress = m_download_request->getProgress();
        core::stringw msg = L"Background download progress: ";
        msg += progress;
        NetworkingLobby::getInstance()->addMoreServerInfo(msg);
    }
    else if (argv[0] == "stopaddondownload")
    {
        if (m_download_request)
        {
            m_download_request->cancel();
            m_download_request = nullptr;
        }
        if (m_background_download.joinable())
            m_background_download.join();
    }
    else if (argv[0] == "installaddon" && argv.size() == 2)
        AddonsPack::install(argv[1]);
    else if (argv[0] == "uninstalladdon" && argv.size() == 2)
        AddonsPack::uninstall(argv[1]);
    else if (argv[0] == "music" && argv.size() == 2)
    {
        int vol = atoi(argv[1].c_str());
        if (vol > 10)
            vol = 10;
        else if (vol < 0)
            vol = 0;
        if (vol == 0 && UserConfigParams::m_music)
        {
            UserConfigParams::m_music = false;
            music_manager->stopMusic();
        }
        else if (vol != 0 && !UserConfigParams::m_music)
        {
            UserConfigParams::m_music = true;
            music_manager->startMusic();
            music_manager->setMasterMusicVolume((float)vol / 10);
        }
        else
            music_manager->setMasterMusicVolume((float)vol / 10);
    }
    else if (argv[0] == "addonrevision" && argv.size() == 2)
    {
        Addon* addon = addons_manager->getAddon(Addon::createAddonId(argv[1]));
        if (!addon)
            return;
        core::stringw ret = addon->getName();
        ret += core::stringw(L": revision ") +
            core::stringw(addon->getInstalledRevision()) +
            L"/" + core::stringw(addon->getRevision());
        NetworkingLobby::getInstance()->addMoreServerInfo(ret);
    }
    else if (argv[0] == "liststkaddon")
    {
        if (argv.size() > 3)
        {
            NetworkingLobby::getInstance()->addMoreServerInfo(
                L"Usage: /liststkaddon [option][addon prefix letter(s) to find]");
            NetworkingLobby::getInstance()->addMoreServerInfo(
                L"available options: -kart, -track, -arena.");
        }
        else
        {
            std::string msg = "";
            for (unsigned i = 0; i < addons_manager->getNumAddons(); i++)
            {
                // addon_ (6 letters)
                const Addon& addon = addons_manager->getAddon(i);
                const std::string& addon_id = addon.getId();
                if (addon.testStatus(Addon::AS_APPROVED))
                {
                    std::string type = "";
                    std::string text = "";
                    if(argv.size() > 1)
                    {
                        if(argv[1].compare("-track") == 0 ||
                           argv[1].compare("-arena") == 0 ||
                           argv[1].compare("-kart" ) == 0)
                            type = argv[1].substr(1);
                        if((argv.size() == 2 && type.empty()) || argv.size() == 3)
                            text = argv[argv.size()-1];
                    }

                    if(!type.empty() && addon.getType()!=type)
                        continue; // only show given type
                    if(!text.empty() && addon_id.find(text, 6) == std::string::npos)
                        continue; // only show matched

                    msg += addon_id.substr(6);
                    msg += ", ";
                }
            }
            if (msg.empty())
                NetworkingLobby::getInstance()->addMoreServerInfo(L"Addon not found");
            else
            {
                msg = msg.substr(0, msg.size() - 2);
                NetworkingLobby::getInstance()->addMoreServerInfo(
                    StringUtils::utf8ToWide
                    (std::string("STK addon: ") + msg));
            }
        }
    }
    else if (argv[0] == "listlocaladdon")
    {
        if (argv.size() > 3)
        {
            NetworkingLobby::getInstance()->addMoreServerInfo(
                L"Usage: /listlocaladdon [option][addon prefix letter(s) to find]");
            NetworkingLobby::getInstance()->addMoreServerInfo(
                L"available options: -kart, -track/-arena, -skin.");
        }
        else
        {
            std::string type = "";
            std::string text = "";
            if(argv.size() > 1)
            {
                if(argv[1].compare("-track") == 0 ||
                   argv[1].compare("-arena") == 0 ||
                   argv[1].compare("-kart" ) == 0 ||
                   argv[1].compare("-skin" ) == 0)
                    type = argv[1].substr(1);
                if((argv.size() == 2 && type.empty()) || argv.size() == 3)
                    text = argv[argv.size()-1];
            }

            std::set<std::string> total_addons;
            if(type.empty() || // not specify addon type
               (!type.empty() && type.compare("kart") == 0)) // list kart addon
            {
                for (unsigned i = 0;
                     i < kart_properties_manager->getNumberOfKarts(); i++)
                {
                    const KartProperties* kp =
                        kart_properties_manager->getKartById(i);
                    if (kp->isAddon())
                        total_addons.insert(kp->getIdent());
                }
            }
            if(type.empty() || // not specify addon type
               (!type.empty() && (type.compare("track") == 0 || type.compare("arena") == 0)))
            {
                for (unsigned i = 0; i < track_manager->getNumberOfTracks(); i++)
                {
                    const Track* track = track_manager->getTrack(i);
                    if (track->isAddon())
                        total_addons.insert(track->getIdent());
                }
            }
            if(type.empty() || // not specify addon type
               (!type.empty() && type.compare("skin") == 0))
            {
                std::set<std::string> addon_skin_files;
                std::string skin_folder = file_manager->getAddonsFile("skins/");
                file_manager->listFiles(addon_skin_files/*out*/, skin_folder,
                                        false/*make full path*/);
                for (auto& skin : addon_skin_files)
                {
                    if (skin == "." || skin == "..")
                        continue;
                    std::string stkskin = skin_folder + skin + "/stkskin.xml";
                    if (file_manager->fileExists(stkskin))
                        total_addons.insert(Addon::createAddonId(skin));
                }
            }
            std::string msg = "";
            for (auto& addon : total_addons)
            {
                // addon_ (6 letters)
                if (!text.empty() && addon.find(text, 6) == std::string::npos)
                    continue;

                msg += addon.substr(6);
                msg += ", ";
            }
            if (msg.empty())
                NetworkingLobby::getInstance()->addMoreServerInfo(L"Addon not found");
            else
            {
                msg = msg.substr(0, msg.size() - 2);
                NetworkingLobby::getInstance()->addMoreServerInfo(
                    StringUtils::utf8ToWide(
                    std::string("Local addon: ") + msg));
            }
        }
    }
    else
    {
        // Send for server command
        NetworkString* cmd_ns = getNetworkString(1);
        const std::string& language = UserConfigParams::m_language;
        cmd_ns->addUInt8(LE_COMMAND).encodeString(language).encodeString(cmd);
        sendToServer(cmd_ns, /*reliable*/true);
        delete cmd_ns;
    }
#endif
}   // handleClientCommand

// ----------------------------------------------------------------------------
void ClientLobby::getKartsTracksNetworkString(BareNetworkString* ns)
{
    std::vector<std::string> all_k;
    for (unsigned i = 0; i < kart_properties_manager->getNumberOfKarts(); i++)
    {
        const KartProperties* kp = kart_properties_manager->getKartById(i);
        if (kp->isAddon())
            all_k.push_back(kp->getIdent());
    }
    std::set<std::string> oks = OfficialKarts::getOfficialKarts();
    if (all_k.size() >= 65536 - (unsigned)oks.size())
        all_k.resize(65535 - (unsigned)oks.size());
    for (const std::string& k : oks)
        all_k.push_back(k);

    auto all_t = track_manager->getAllTrackIdentifiers();
    if (all_t.size() >= 65536)
        all_t.resize(65535);
    ns->addUInt16((uint16_t)all_k.size()).addUInt16((uint16_t)all_t.size());
    for (const std::string& kart : all_k)
    {
        ns->encodeString(kart);
    }
    for (const std::string& track : all_t)
    {
        ns->encodeString(track);
    }
}   // getKartsTracksNetworkString

// ----------------------------------------------------------------------------
void ClientLobby::updateAssetsToServer()
{
    NetworkString* ns = getNetworkString(1);
    ns->addUInt8(LE_ASSETS_UPDATE);
    getKartsTracksNetworkString(ns);
    sendToServer(ns, /*reliable*/true);
    delete ns;
}   // updateAssetsToServer

// ----------------------------------------------------------------------------
void ClientLobby::downloadAddonsPack(std::shared_ptr<Online::HTTPRequest> r)
{
    if (startedDownloadAddonsPack())
        return;
    m_download_request = r;
    m_background_download = std::thread([r](){ r->executeNow(); });
}   // downloadAddonsPack

// ----------------------------------------------------------------------------
/** Called when the asynchronous download of the addon finished.
 */
void ClientLobby::doInstallAddonsPack()
{
#ifndef SERVER_ONLY
    core::stringw msg;
    if (!m_download_request->isCancelled() &&
        m_download_request->hadDownloadError())
    {
        // Reset the download buttons so user can redownload if needed
        // I18N: Shown when there is download error for assets download
        // in the first run
        msg = _("Failed to download assets, check your storage space or "
            "internet connection and try again later.");
    }

    if (!msg.empty())
    {
        new MessageDialog(msg);
    }
    else
    {
        std::set<std::string> result;
        std::string tmp_extract = file_manager->getAddonsFile("tmp_extract");
        file_manager->listFiles(result, tmp_extract);
        tmp_extract += "/";
        bool addon_kart_installed = false;
        bool addon_track_installed = false;
        bool track_installed = false;
        for (auto& r : result)
        {
            if (r == ".." || r == ".")
                continue;
            std::string addon_id = Addon::createAddonId(r);
            // We assume the addons pack the user downloaded use the latest
            // revision from the stk-addons (if exists)
            if (file_manager->fileExists(tmp_extract + r + "/stkskin.xml"))
            {
                std::string skins = file_manager->getAddonsFile("skins");
                file_manager->checkAndCreateDirectoryP(skins);
                if (file_manager->isDirectory(skins + "/" + r))
                    file_manager->removeDirectory(skins + "/" + r);
                file_manager->moveDirectoryInto(tmp_extract + r, skins);
                // Skin is not supported in stk-addons atm
            }
            else if (file_manager->fileExists(tmp_extract + r + "/kart.xml"))
            {
                std::string karts = file_manager->getAddonsFile("karts");
                file_manager->checkAndCreateDirectoryP(karts);
                if (file_manager->isDirectory(karts + "/" + r))
                {
                    const KartProperties* prop =
                        kart_properties_manager->getKart(addon_id);
                    // If the model already exist, first remove the old kart
                    if (prop)
                        kart_properties_manager->removeKart(addon_id);
                    file_manager->removeDirectory(karts + "/" + r);
                }
                if (file_manager->moveDirectoryInto(tmp_extract + r, karts))
                {
                    kart_properties_manager->loadKart(karts + "/" + r);
                    Addon* addon = addons_manager->getAddon(addon_id);
                    if (addon)
                    {
                        addon_kart_installed = true;
                        addon->setInstalled(true);
                    }
                }
            }
            else if (file_manager->fileExists(tmp_extract + r + "/track.xml"))
            {
                track_installed = true;
                std::string tracks = file_manager->getAddonsFile("tracks");
                file_manager->checkAndCreateDirectoryP(tracks);
                if (file_manager->isDirectory(tracks + "/" + r))
                    file_manager->removeDirectory(tracks + "/" + r);
                if (file_manager->moveDirectoryInto(tmp_extract + r, tracks))
                {
                    Addon* addon = addons_manager->getAddon(addon_id);
                    if (addon)
                    {
                        addon_track_installed = true;
                        addon->setInstalled(true);
                    }
                }
            }
        }
        if (addon_kart_installed || addon_track_installed)
            addons_manager->saveInstalled();
        if (track_installed)
        {
            track_manager->loadTrackList();
            // Update the replay file list to use latest track pointer
            ReplayPlay::get()->loadAllReplayFile();
            delete grand_prix_manager;
            grand_prix_manager = new GrandPrixManager();
            grand_prix_manager->checkConsistency();
        }
        updateAssetsToServer();
    }
#endif
}   // doInstall
