// // 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 "audio/sfx_manager.hpp" #include "config/user_config.hpp" #include "config/player_manager.hpp" #include "graphics/camera.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 "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/race_event_manager.hpp" #include "network/server.hpp" #include "network/server_config.hpp" #include "network/stk_host.hpp" #include "network/stk_peer.hpp" #include "states_screens/online/networking_lobby.hpp" #include "states_screens/online/network_kart_selection.hpp" #include "states_screens/race_result_gui.hpp" #include "states_screens/online/tracks_screen.hpp" #include "tracks/track.hpp" #include "tracks/track_manager.hpp" #include "utils/log.hpp" // ============================================================================ /** 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(const TransportAddress& a, std::shared_ptr s) : LobbyProtocol(NULL) { 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_address = a; 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_BAD_CONNECTION] = _("Bad network connection is detected."); m_first_connect = true; m_spectator = false; m_server_live_joinable = false; } // ClientLobby //----------------------------------------------------------------------------- ClientLobby::~ClientLobby() { if (m_server->supportsEncryption()) { Online::XMLRequest* request = new Online::XMLRequest(true/*manager_memory*/); NetworkConfig::get()->setServerDetails(request, "clear-user-joined-server"); request->queue(); ConnectToServer::m_previous_unjoin = request->observeExistence(); } } // ClientLobby //----------------------------------------------------------------------------- void ClientLobby::setup() { m_spectator = false; m_auto_back_to_lobby_time = std::numeric_limits::max(); m_start_live_game_time = std::numeric_limits::max(); m_received_server_result = false; TracksScreen::getInstance()->resetVote(); LobbyProtocol::setup(); m_state.store(NONE); } // 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 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; default: return false; 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) { case LE_START_RACE: startGame(event); break; default: break; } // switch return true; } // 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(); return true; } // disconnection return false; } // 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); TracksScreen::getInstance()->setResult(winner_peer_id, winner_vote); std::shared_ptr peer = event->getPeerSP(); peer->cleanPlayerProfiles(); std::vector > 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(); PerPlayerDifficulty ppd = (PerPlayerDifficulty)data.getUInt8(); uint8_t local_id = data.getUInt8(); KartTeam team = (KartTeam)data.getUInt8(); auto player = std::make_shared(peer, player_name, host_id, kart_color, online_id, ppd, local_id, team); std::string kart_name; data.decodeString(&kart_name); player->setKartName(kart_name); players.push_back(player); } uint32_t random_seed = data.getUInt32(); ItemManager::updateRandomSeed(random_seed); if (race_manager->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(); race_manager->setFlagReturnTicks(flag_return_timeout); } configRemoteKart(players, isSpectator() ? 1 : (int)NetworkConfig::get()->getNetworkPlayers().size()); loadWorld(); // Disable until render gui during loading is bug free //StateManager::get()->enterGameState(); // Live join or spectate if state is CONNECTED if (m_state.load() == CONNECTED) { 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::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 //----------------------------------------------------------------------------- void ClientLobby::update(int ticks) { switch (m_state.load()) { case NONE: if (STKHost::get()->isConnectedTo(m_server_address)) { m_state.store(LINKED); } break; case LINKED: { NetworkString* ns = getNetworkString(); ns->addUInt8(LE_CONNECTION_REQUESTED) .addUInt32(ServerConfig::m_server_version) .encodeString(StringUtils::getUserAgentString()); auto all_k = kart_properties_manager->getAllAvailableKarts(); auto all_t = track_manager->getAllTrackIdentifiers(); if (all_k.size() >= 65536) all_k.resize(65535); 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); } assert(!NetworkConfig::get()->isAddingNetworkPlayers()); const uint8_t player_count = (uint8_t)NetworkConfig::get()->getNetworkPlayers().size(); ns->addUInt8(player_count); bool encryption = false; uint32_t id = PlayerManager::getCurrentOnlineId(); 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::getCurrentOnlineUserName()); } rest->encodeString(ServerConfig::m_private_server_password) .addUInt8(player_count); for (auto& p : NetworkConfig::get()->getNetworkPlayers()) { core::stringw name; PlayerProfile* player = std::get<1>(p); rest->encodeString(player->getName()). 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::getInstance()->protocolStopped() || !GameProtocol::emptyInstance()) return; if (!m_received_server_result) { m_received_server_result = true; m_auto_back_to_lobby_time = StkTime::getRealTimeMs() + 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::getRealTimeMs() > m_auto_back_to_lobby_time) { m_auto_back_to_lobby_time = std::numeric_limits::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::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); } case SELECTING_ASSETS: case RACING: case EXITING: 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); 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::getInstance() && RaceEventManager::getInstance()->isRunning() && !RaceEventManager::getInstance()->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); } 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); } STKHost::get()->setMyHostId(data.getUInt32()); 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); float auto_start_timer = data.getFloat(); int state_frequency_in_server = data.getUInt32(); NetworkConfig::get()->setStateFrequency(state_frequency_in_server); if (auto_start_timer != std::numeric_limits::max()) NetworkingLobby::getInstance()->setStartingTimerTo(auto_start_timer); } // connectionAccepted //----------------------------------------------------------------------------- void ClientLobby::handleServerInfo(Event* event) { // At least 6 bytes should remain now if (!checkDataSize(event, 6)) return; if (!m_first_connect) { NetworkingLobby::getInstance() ->addMoreServerInfo(L"--------------------"); } m_first_connect = false; NetworkString &data = event->data(); // Add server info core::stringw str, each_line; uint8_t u_data; data.decodeStringW(&str); //I18N: In the networking lobby each_line = _("Server name: %s", str); NetworkingLobby::getInstance()->addMoreServerInfo(each_line); u_data = data.getUInt8(); const core::stringw& difficulty_name = race_manager->getDifficultyName((RaceManager::Difficulty)u_data); race_manager->setDifficulty((RaceManager::Difficulty)u_data); //I18N: In the networking lobby each_line = _("Difficulty: %s", difficulty_name); NetworkingLobby::getInstance()->addMoreServerInfo(each_line); unsigned max_player = data.getUInt8(); //I18N: In the networking lobby each_line = _("Max players: %d", (int)max_player); NetworkingLobby::getInstance()->addMoreServerInfo(each_line); u_data = data.getUInt8(); auto game_mode = ServerConfig::getLocalGameMode(u_data); race_manager->setMinorMode(game_mode.first); // We use single mode in network even it's grand prix race_manager->setMajorMode(RaceManager::MAJOR_MODE_SINGLE); //I18N: In the networking lobby core::stringw mode_name = ServerConfig::getModeName(u_data); each_line = _("Game mode: %s", mode_name); NetworkingLobby::getInstance()->addMoreServerInfo(each_line); 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 each_line = _("Soccer game type: %s", sgt); NetworkingLobby::getInstance()->addMoreServerInfo(each_line); 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); each_line = _("Grand prix progress: %d / %d", cur_gp_track, total_gp_track); NetworkingLobby::getInstance()->addMoreServerInfo(each_line); break; } } // Auto start info unsigned min_players = data.getUInt8(); float start_timeout = data.getFloat(); NetworkingLobby::getInstance()->initAutoStartTimer(grand_prix_started, min_players, start_timeout, max_player); // MOTD core::stringw motd; data.decodeString16(&motd); const std::vector& motd_line = StringUtils::split(motd, '\n'); if (!motd_line.empty()) { for (const core::stringw& motd : motd_line) NetworkingLobby::getInstance()->addMoreServerInfo(motd); } bool server_config = data.getUInt8() == 1; NetworkingLobby::getInstance()->toggleServerConfigButton(server_config); m_server_live_joinable = data.getUInt8() == 1; } // 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(); 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_difficulty = PLAYER_DIFFICULTY_NORMAL; lp.m_local_player_id = local_id; data.decodeStringW(&lp.m_user_name); total_players += lp.m_user_name; uint8_t waiting_and_spectator = data.getUInt8(); bool is_peer_waiting_for_game = waiting_and_spectator == 1; bool is_spectator = waiting_and_spectator == 2; bool is_peer_server_owner = data.getUInt8() == 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 (waiting) { if (is_spectator) lp.m_icon_id = 5; else if (!is_peer_waiting_for_game) lp.m_icon_id = 3; } lp.m_difficulty = (PerPlayerDifficulty)data.getUInt8(); if (lp.m_difficulty == PLAYER_DIFFICULTY_HANDICAP) { lp.m_user_name = _("%s (handicapped)", lp.m_user_name); } lp.m_kart_team = (KartTeam)data.getUInt8(); bool ready = data.getUInt8() == 1; if (ready) lp.m_icon_id = 4; if (lp.m_host_id == STKHost::get()->getMyHostId()) { auto& local_players = NetworkConfig::get()->getNetworkPlayers(); std::get<2>(local_players.at(local_id)) = lp.m_difficulty; } m_lobby_players.push_back(lp); } // 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; 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() { STKHost::get()->setAuthorisedToControl(true); 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 (message.size() > 0) { if (GUIEngine::getCurrentScreen() == NetworkingLobby::getInstance()) NetworkingLobby::getInstance()->addMoreServerInfo(message); else 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: STKHost::get()->setErrorMessage( _("Connection refused: You are banned from the server.")); break; case RR_INCORRECT_PASSWORD: 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); joinStartGameThread(); m_start_game_thread = std::thread([start_time, this]() { const uint64_t cur_time = STKHost::get()->getNetworkTimer(); if (!(start_time > cur_time)) { Log::warn("ClientLobby", "Network timer is too slow to catch " "up, you must have a poor network."); STKHost::get()->setErrorMessage( m_disconnected_msg.at(PDI_BAD_CONNECTION)); STKHost::get()->requestShutdown(); return; } int sleep_time = (int)(start_time - cur_time); Log::info("ClientLobby", "Start game after %dms", sleep_time); std::this_thread::sleep_for(std::chrono::milliseconds(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; 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(); 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) { 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); TracksScreen::getInstance()->setQuitServer(); TracksScreen::getInstance()->setNetworkTracks(); TracksScreen::getInstance()->push(); } else { screen->push(); } 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(World::getWorld()); if (m_game_setup->isGrandPrix()) { int t = data.getUInt32(); lw->setFastestLapTicks(t); race_manager->configGrandPrixResultFromNetwork(data); } else if (race_manager->modeHasLaps()) { int t = data.getUInt32(); lw->setFastestLapTicks(t); } 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*/); } } } // stop race protocols RaceEventManager::getInstance()->stop(); RaceEventManager::getInstance()->getProtocol()->requestTerminate(); GameProtocol::lock()->requestTerminate(); 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::getInstance()) { RaceEventManager::getInstance()->stop(); auto gep = RaceEventManager::getInstance()->getProtocol(); // Game events protocol is main thread event only if (gep) gep->requestTerminate(); } auto gp = GameProtocol::lock(); if (gp) { auto lock = gp->acquireWorldDeletingMutex(); gp->requestTerminate(); RaceResultGUI::getInstance()->backToLobby(); } else RaceResultGUI::getInstance()->backToLobby(); NetworkString &data = event->data(); core::stringw msg; 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; default: break; } if (!msg.empty()) { SFXManager::get()->quickSound("anvil"); MessageQueue::add(MessageQueue::MT_ERROR, 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); // Live join or spectate if state is CONNECTED ns->setSynchronous(m_state.load() == CONNECTED); 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); 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(ItemManager::get()); assert(nim); nim->restoreCompleteState(data); w->restoreCompleteState(data); } // liveJoinAcknowledged //----------------------------------------------------------------------------- void ClientLobby::finishLiveJoin() { m_start_live_game_time = std::numeric_limits::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(); PerPlayerDifficulty ppd = (PerPlayerDifficulty)data.getUInt8(); uint8_t local_id = data.getUInt8(); std::string kart_name; data.decodeString(&kart_name); RemoteKartInfo& rki = race_manager->getKartInfo(kart_id); rki.setPlayerName(player_name); rki.setHostId(host_id); rki.setDefaultKartColor(kart_color); rki.setOnlineId(online_id); rki.setPerPlayerDifficulty(ppd); rki.setLocalPlayerId(local_id); rki.setKartName(kart_name); addLiveJoiningKart(kart_id, rki, live_join_util_ticks); core::stringw msg; if (race_manager->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 all_k = kart_properties_manager->getKartsInGroup("standard"); std::set 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) { 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::getCurrentOnlineUserName(); else name = player->getName(); chat->encodeString16(name + L": " + text); 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 && type == Input::IT_STICKMOTION) return; if (action == PA_LOOK_BACK) { if (cam->getMode() == Camera::CM_REVERSE) cam->setMode(Camera::CM_NORMAL); else cam->setMode(Camera::CM_REVERSE); return; } World::KartList karts = World::getWorld()->getKarts(); bool sort_kart_for_position = race_manager->getMinorMode() == RaceManager::MINOR_MODE_FREE_FOR_ALL || race_manager->getMinorMode() == RaceManager::MINOR_MODE_CAPTURE_THE_FLAG || race_manager->modeHasLaps(); if (sort_kart_for_position) { std::sort(karts.begin(), karts.end(), [] (const std::shared_ptr& a, const std::shared_ptr& b)->bool { return a->getPosition() < b->getPosition(); }); } const int num_karts = karts.size(); int current_idx = -1; if (cam->getKart()) { if (sort_kart_for_position) current_idx = cam->getKart()->getPosition() - 1; else current_idx = cam->getKart()->getWorldKartId(); } 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].get()); 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); // 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 " "or <%s> for the camera position.", left, right, back); MessageQueue::add(MessageQueue::MT_GENERIC, msg); } // addSpectateHelperMessage