// // 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/server_lobby.hpp" #include "addons/addon.hpp" #include "config/user_config.hpp" #include "items/network_item_manager.hpp" #include "items/powerup_manager.hpp" #include "karts/abstract_kart.hpp" #include "karts/controller/player_controller.hpp" #include "karts/kart_properties.hpp" #include "karts/kart_properties_manager.hpp" #include "karts/official_karts.hpp" #include "modes/capture_the_flag.hpp" #include "modes/linear_world.hpp" #include "network/crypto.hpp" #include "network/event.hpp" #include "network/game_setup.hpp" #include "network/network.hpp" #include "network/network_config.hpp" #include "network/network_player_profile.hpp" #include "network/peer_vote.hpp" #include "network/protocol_manager.hpp" #include "network/protocols/connect_to_peer.hpp" #include "network/protocols/game_protocol.hpp" #include "network/protocols/game_events_protocol.hpp" #include "network/race_event_manager.hpp" #include "network/server_config.hpp" #include "network/socket_address.hpp" #include "network/stk_host.hpp" #include "network/stk_ipv6.hpp" #include "network/stk_peer.hpp" #include "online/online_profile.hpp" #include "online/request_manager.hpp" #include "online/xml_request.hpp" #include "race/race_manager.hpp" #include "tracks/check_manager.hpp" #include "tracks/track.hpp" #include "tracks/track_manager.hpp" #include "utils/log.hpp" #include "utils/random_generator.hpp" #include "utils/string_utils.hpp" #include "utils/time.hpp" #include "utils/translation.hpp" #include #include #include #include // ======================================================================== class SubmitRankingRequest : public Online::XMLRequest { public: SubmitRankingRequest(uint32_t online_id, double scores, double max_scores, unsigned num_races, double raw_scores, double rating_deviation, uint64_t disconnects, const std::string& country_code) : XMLRequest(Online::RequestManager::HTTP_MAX_PRIORITY) { addParameter("id", online_id); addParameter("scores", scores); addParameter("max-scores", max_scores); addParameter("num-races-done", num_races); addParameter("raw-scores", raw_scores); addParameter("rating-deviation", rating_deviation); addParameter("disconnects", disconnects); addParameter("country-code", country_code); } virtual void afterOperation() { Online::XMLRequest::afterOperation(); const XMLNode* result = getXMLData(); std::string rec_success; if (!(result->get("success", &rec_success) && rec_success == "yes")) { Log::error("ServerLobby", "Failed to submit scores."); } } }; // UpdatePlayerRankingRequest // ======================================================================== // We use max priority for all server requests to avoid downloading of addons // icons blocking the poll request in all-in-one graphical client server #ifdef ENABLE_SQLITE3 // ---------------------------------------------------------------------------- static void upperIPv6SQL(sqlite3_context* context, int argc, sqlite3_value** argv) { if (argc != 1) { sqlite3_result_int64(context, 0); return; } char* ipv6 = (char*)sqlite3_value_text(argv[0]); if (ipv6 == NULL) { sqlite3_result_int64(context, 0); return; } sqlite3_result_int64(context, upperIPv6(ipv6)); } // ---------------------------------------------------------------------------- void insideIPv6CIDRSQL(sqlite3_context* context, int argc, sqlite3_value** argv) { if (argc != 2) { sqlite3_result_int(context, 0); return; } char* ipv6_cidr = (char*)sqlite3_value_text(argv[0]); char* ipv6_in = (char*)sqlite3_value_text(argv[1]); if (ipv6_cidr == NULL || ipv6_in == NULL) { sqlite3_result_int(context, 0); return; } sqlite3_result_int(context, insideIPv6CIDR(ipv6_cidr, ipv6_in)); } // insideIPv6CIDRSQL // ---------------------------------------------------------------------------- /* Copy below code so it can be use as loadable extension to be used in sqlite3 command interface (together with andIPv6 and insideIPv6CIDR from stk_ipv6) #include "sqlite3ext.h" SQLITE_EXTENSION_INIT1 // ---------------------------------------------------------------------------- sqlite3_extension_init(sqlite3* db, char** pzErrMsg, const sqlite3_api_routines* pApi) { SQLITE_EXTENSION_INIT2(pApi) sqlite3_create_function(db, "insideIPv6CIDR", 2, SQLITE_UTF8, NULL, insideIPv6CIDRSQL, NULL, NULL); sqlite3_create_function(db, "upperIPv6", 1, SQLITE_UTF8, 0, upperIPv6SQL, 0, 0); return 0; } // sqlite3_extension_init */ #endif /** This is the central game setup protocol running in the server. It is * mostly a finite state machine. Note that all nodes in ellipses and light * grey background are actual states; nodes in boxes and white background * are functions triggered from a state or triggering potentially a state * change. \dot digraph interaction { node [shape=box]; "Server Constructor"; "playerTrackVote"; "connectionRequested"; "signalRaceStartToClients"; "startedRaceOnClient"; "loadWorld"; node [shape=ellipse,style=filled,color=lightgrey]; "Server Constructor" -> "INIT_WAN" [label="If WAN game"] "Server Constructor" -> "WAITING_FOR_START_GAME" [label="If LAN game"] "INIT_WAN" -> "GETTING_PUBLIC_ADDRESS" [label="GetPublicAddress protocol callback"] "GETTING_PUBLIC_ADDRESS" -> "WAITING_FOR_START_GAME" [label="Register server"] "WAITING_FOR_START_GAME" -> "connectionRequested" [label="Client connection request"] "connectionRequested" -> "WAITING_FOR_START_GAME" "WAITING_FOR_START_GAME" -> "SELECTING" [label="Start race from authorised client"] "SELECTING" -> "SELECTING" [label="Client selects kart, #laps, ..."] "SELECTING" -> "playerTrackVote" [label="Client selected track"] "playerTrackVote" -> "SELECTING" [label="Not all clients have selected"] "playerTrackVote" -> "LOAD_WORLD" [label="All clients have selected; signal load_world to clients"] "LOAD_WORLD" -> "loadWorld" "loadWorld" -> "WAIT_FOR_WORLD_LOADED" "WAIT_FOR_WORLD_LOADED" -> "WAIT_FOR_WORLD_LOADED" [label="Client or server loaded world"] "WAIT_FOR_WORLD_LOADED" -> "signalRaceStartToClients" [label="All clients and server ready"] "signalRaceStartToClients" -> "WAIT_FOR_RACE_STARTED" "WAIT_FOR_RACE_STARTED" -> "startedRaceOnClient" [label="Client has started race"] "startedRaceOnClient" -> "WAIT_FOR_RACE_STARTED" [label="Not all clients have started"] "startedRaceOnClient" -> "DELAY_SERVER" [label="All clients have started"] "DELAY_SERVER" -> "DELAY_SERVER" [label="Not done waiting"] "DELAY_SERVER" -> "RACING" [label="Server starts race now"] } \enddot * It starts with detecting the public ip address and port of this * host (GetPublicAddress). */ ServerLobby::ServerLobby() : LobbyProtocol() { m_client_server_host_id.store(0); m_lobby_players.store(0); std::vector all_t = track_manager->getTracksInGroup("standard"); std::vector all_arenas = track_manager->getArenasInGroup("standard", false); std::vector all_soccers = track_manager->getArenasInGroup("standard", true); all_t.insert(all_t.end(), all_arenas.begin(), all_arenas.end()); all_t.insert(all_t.end(), all_soccers.begin(), all_soccers.end()); m_official_kts.first = OfficialKarts::getOfficialKarts(); for (int track : all_t) { Track* t = track_manager->getTrack(track); if (!t->isAddon()) m_official_kts.second.insert(t->getIdent()); } updateAddons(); m_rs_state.store(RS_NONE); m_last_success_poll_time.store(StkTime::getMonoTimeMs() + 30000); m_last_unsuccess_poll_time = StkTime::getMonoTimeMs(); m_server_owner_id.store(-1); m_registered_for_once_only = false; setHandleDisconnections(true); m_state = SET_PUBLIC_ADDRESS; m_save_server_config = true; if (ServerConfig::m_ranked) { Log::info("ServerLobby", "This server will submit ranking scores to " "the STK addons server. Don't bother hosting one without the " "corresponding permissions, as they would be rejected."); } m_result_ns = getNetworkString(); m_result_ns->setSynchronous(true); m_items_complete_state = new BareNetworkString(); m_server_id_online.store(0); m_difficulty.store(ServerConfig::m_server_difficulty); m_game_mode.store(ServerConfig::m_server_mode); m_default_vote = new PeerVote(); m_player_reports_table_exists = false; initDatabase(); } // ServerLobby //----------------------------------------------------------------------------- /** Destructor. */ ServerLobby::~ServerLobby() { if (m_server_id_online.load() != 0) { // For child process the request manager will keep on running unregisterServer(m_process_type == PT_MAIN ? true : false/*now*/); } delete m_result_ns; delete m_items_complete_state; if (m_save_server_config) ServerConfig::writeServerConfigToDisk(); delete m_default_vote; destroyDatabase(); } // ~ServerLobby //----------------------------------------------------------------------------- void ServerLobby::initDatabase() { #ifdef ENABLE_SQLITE3 m_last_poll_db_time = StkTime::getMonoTimeMs(); m_db = NULL; m_ip_ban_table_exists = false; m_ipv6_ban_table_exists = false; m_online_id_ban_table_exists = false; m_ip_geolocation_table_exists = false; m_ipv6_geolocation_table_exists = false; if (!ServerConfig::m_sql_management) return; const std::string& path = ServerConfig::getConfigDirectory() + "/" + ServerConfig::m_database_file.c_str(); int ret = sqlite3_open_v2(path.c_str(), &m_db, SQLITE_OPEN_SHAREDCACHE | SQLITE_OPEN_FULLMUTEX | SQLITE_OPEN_READWRITE, NULL); if (ret != SQLITE_OK) { Log::error("ServerLobby", "Cannot open database: %s.", sqlite3_errmsg(m_db)); sqlite3_close(m_db); m_db = NULL; return; } sqlite3_busy_handler(m_db, [](void* data, int retry) { int retry_count = ServerConfig::m_database_timeout / 100; if (retry < retry_count) { sqlite3_sleep(100); // Return non-zero to let caller retry again return 1; } // Return zero to let caller return SQLITE_BUSY immediately return 0; }, NULL); sqlite3_create_function(m_db, "insideIPv6CIDR", 2, SQLITE_UTF8, NULL, &insideIPv6CIDRSQL, NULL, NULL); sqlite3_create_function(m_db, "upperIPv6", 1, SQLITE_UTF8, NULL, &upperIPv6SQL, NULL, NULL); checkTableExists(ServerConfig::m_ip_ban_table, m_ip_ban_table_exists); checkTableExists(ServerConfig::m_ipv6_ban_table, m_ipv6_ban_table_exists); checkTableExists(ServerConfig::m_online_id_ban_table, m_online_id_ban_table_exists); checkTableExists(ServerConfig::m_player_reports_table, m_player_reports_table_exists); checkTableExists(ServerConfig::m_ip_geolocation_table, m_ip_geolocation_table_exists); checkTableExists(ServerConfig::m_ipv6_geolocation_table, m_ipv6_geolocation_table_exists); #endif } // initDatabase //----------------------------------------------------------------------------- void ServerLobby::initServerStatsTable() { #ifdef ENABLE_SQLITE3 if (!ServerConfig::m_sql_management || !m_db) return; std::string table_name = std::string("v") + StringUtils::toString(ServerConfig::m_server_db_version) + "_" + ServerConfig::m_server_uid + "_stats"; std::ostringstream oss; oss << "CREATE TABLE IF NOT EXISTS " << table_name << " (\n" " host_id INTEGER UNSIGNED NOT NULL PRIMARY KEY, -- Unique host id in STKHost of each connection session for a STKPeer\n" " ip INTEGER UNSIGNED NOT NULL, -- IP decimal of host\n"; if (ServerConfig::m_ipv6_connection) oss << " ipv6 TEXT NOT NULL DEFAULT '', -- IPv6 (if exists) in string of host\n"; oss << " port INTEGER UNSIGNED NOT NULL, -- Port of host\n" " online_id INTEGER UNSIGNED NOT NULL, -- Online if of the host (0 for offline account)\n" " username TEXT NOT NULL, -- First player name in the host (if the host has splitscreen player)\n" " player_num INTEGER UNSIGNED NOT NULL, -- Number of player(s) from the host, more than 1 if it has splitscreen player\n" " country_code TEXT NULL DEFAULT NULL, -- 2-letter country code of the host\n" " version TEXT NOT NULL, -- SuperTuxKart version of the host\n" " os TEXT NOT NULL, -- Operating system of the host\n" " connected_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Time when connected\n" " disconnected_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Time when disconnected (saved when disconnected)\n" " ping INTEGER UNSIGNED NOT NULL DEFAULT 0, -- Ping of the host\n" " packet_loss INTEGER NOT NULL DEFAULT 0 -- Mean packet loss count from ENet (saved when disconnected)\n" ") WITHOUT ROWID;"; std::string query = oss.str(); sqlite3_stmt* stmt = NULL; int ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0); if (ret == SQLITE_OK) { ret = sqlite3_step(stmt); ret = sqlite3_finalize(stmt); if (ret == SQLITE_OK) m_server_stats_table = table_name; else { Log::error("ServerLobby", "Error finalize database for query %s: %s", query.c_str(), sqlite3_errmsg(m_db)); } } else { Log::error("ServerLobby", "Error preparing database for query %s: %s", query.c_str(), sqlite3_errmsg(m_db)); } if (m_server_stats_table.empty()) return; // Extra default table _countries: // Server owner need to initialise this table himself, check NETWORKING.md std::string country_table_name = std::string("v") + StringUtils::toString( ServerConfig::m_server_db_version) + "_countries"; query = StringUtils::insertValues( "CREATE TABLE IF NOT EXISTS %s (\n" " country_code TEXT NOT NULL PRIMARY KEY UNIQUE, -- Unique 2-letter country code\n" " country_flag TEXT NOT NULL, -- Unicode country flag representation of 2-letter country code\n" " country_name TEXT NOT NULL -- Readable name of this country\n" ") WITHOUT ROWID;", country_table_name.c_str()); easySQLQuery(query); // Default views: // _full_stats // Full stats with ip in human readable format and time played of each // players in minutes std::string full_stats_view_name = std::string("v") + StringUtils::toString(ServerConfig::m_server_db_version) + "_" + ServerConfig::m_server_uid + "_full_stats"; oss.str(""); oss << "CREATE VIEW IF NOT EXISTS " << full_stats_view_name << " AS\n" << " SELECT host_id, ip,\n" << " ((ip >> 24) & 255) ||'.'|| ((ip >> 16) & 255) ||'.'|| ((ip >> 8) & 255) ||'.'|| ((ip ) & 255) AS ip_readable,\n"; if (ServerConfig::m_ipv6_connection) oss << " ipv6,"; oss << " port, online_id, username, player_num,\n" << " " << m_server_stats_table << ".country_code AS country_code, country_flag, country_name, version, os,\n" << " ROUND((STRFTIME(\"%s\", disconnected_time) - STRFTIME(\"%s\", connected_time)) / 60.0, 2) AS time_played,\n" << " connected_time, disconnected_time, ping, packet_loss FROM " << m_server_stats_table << "\n" << " LEFT JOIN " << country_table_name << " ON " << country_table_name << ".country_code = " << m_server_stats_table << ".country_code\n" << " ORDER BY connected_time DESC;"; query = oss.str(); easySQLQuery(query); // _current_players // Current players in server with ip in human readable format and time // played of each players in minutes std::string current_players_view_name = std::string("v") + StringUtils::toString(ServerConfig::m_server_db_version) + "_" + ServerConfig::m_server_uid + "_current_players"; oss.str(""); oss.clear(); oss << "CREATE VIEW IF NOT EXISTS " << current_players_view_name << " AS\n" << " SELECT host_id, ip,\n" << " ((ip >> 24) & 255) ||'.'|| ((ip >> 16) & 255) ||'.'|| ((ip >> 8) & 255) ||'.'|| ((ip ) & 255) AS ip_readable,\n"; if (ServerConfig::m_ipv6_connection) oss << " ipv6,"; oss << " port, online_id, username, player_num,\n" << " " << m_server_stats_table << ".country_code AS country_code, country_flag, country_name, version, os,\n" << " ROUND((STRFTIME(\"%s\", 'now') - STRFTIME(\"%s\", connected_time)) / 60.0, 2) AS time_played,\n" << " connected_time, ping FROM " << m_server_stats_table << "\n" << " LEFT JOIN " << country_table_name << " ON " << country_table_name << ".country_code = " << m_server_stats_table << ".country_code\n" << " WHERE connected_time = disconnected_time;"; query = oss.str(); easySQLQuery(query); // _player_stats // All players with online id and username with their time played stats // in this server since creation of this database // If sqlite supports window functions (since 3.25), it will include last session player info (ip, country, ping...) std::string player_stats_view_name = std::string("v") + StringUtils::toString(ServerConfig::m_server_db_version) + "_" + ServerConfig::m_server_uid + "_player_stats"; oss.str(""); oss.clear(); if (sqlite3_libversion_number() < 3025000) { oss << "CREATE VIEW IF NOT EXISTS " << player_stats_view_name << " AS\n" << " SELECT online_id, username, COUNT(online_id) AS num_connections,\n" << " MIN(connected_time) AS first_connected_time,\n" << " MAX(connected_time) AS last_connected_time,\n" << " ROUND(SUM((STRFTIME(\"%s\", disconnected_time) - STRFTIME(\"%s\", connected_time)) / 60.0), 2) AS total_time_played,\n" << " ROUND(AVG((STRFTIME(\"%s\", disconnected_time) - STRFTIME(\"%s\", connected_time)) / 60.0), 2) AS average_time_played,\n" << " ROUND(MIN((STRFTIME(\"%s\", disconnected_time) - STRFTIME(\"%s\", connected_time)) / 60.0), 2) AS min_time_played,\n" << " ROUND(MAX((STRFTIME(\"%s\", disconnected_time) - STRFTIME(\"%s\", connected_time)) / 60.0), 2) AS max_time_played\n" << " FROM " << m_server_stats_table << "\n" << " WHERE online_id != 0 GROUP BY online_id ORDER BY num_connections DESC;"; } else { oss << "CREATE VIEW IF NOT EXISTS " << player_stats_view_name << " AS\n" << " SELECT a.online_id, a.username, a.ip, a.ip_readable,\n"; if (ServerConfig::m_ipv6_connection) oss << " a.ipv6,"; oss << " a.port, a.player_num,\n" << " a.country_code, a.country_flag, a.country_name, a.version, a.os, a.ping, a.packet_loss,\n" << " b.num_connections, b.first_connected_time, b.first_disconnected_time,\n" << " a.connected_time AS last_connected_time, a.disconnected_time AS last_disconnected_time,\n" << " a.time_played AS last_time_played, b.total_time_played, b.average_time_played,\n" << " b.min_time_played, b.max_time_played\n" << " FROM\n" << " (\n" << " SELECT *,\n" << " ROW_NUMBER() OVER\n" << " (\n" << " PARTITION BY online_id\n" << " ORDER BY connected_time DESC\n" << " ) RowNum\n" << " FROM " << full_stats_view_name << " where online_id != 0\n" << " ) as a\n" << " JOIN\n" << " (\n" << " SELECT online_id, COUNT(online_id) AS num_connections,\n" << " MIN(connected_time) AS first_connected_time,\n" << " MIN(disconnected_time) AS first_disconnected_time,\n" << " ROUND(SUM((STRFTIME(\"%s\", disconnected_time) - STRFTIME(\"%s\", connected_time)) / 60.0), 2) AS total_time_played,\n" << " ROUND(AVG((STRFTIME(\"%s\", disconnected_time) - STRFTIME(\"%s\", connected_time)) / 60.0), 2) AS average_time_played,\n" << " ROUND(MIN((STRFTIME(\"%s\", disconnected_time) - STRFTIME(\"%s\", connected_time)) / 60.0), 2) AS min_time_played,\n" << " ROUND(MAX((STRFTIME(\"%s\", disconnected_time) - STRFTIME(\"%s\", connected_time)) / 60.0), 2) AS max_time_played\n" << " FROM " << m_server_stats_table << " WHERE online_id != 0 GROUP BY online_id\n" << " ) AS b\n" << " ON b.online_id = a.online_id\n" << " WHERE RowNum = 1 ORDER BY num_connections DESC;\n"; } query = oss.str(); easySQLQuery(query); uint32_t last_host_id = 0; query = StringUtils::insertValues("SELECT MAX(host_id) FROM %s;", m_server_stats_table.c_str()); ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0); if (ret == SQLITE_OK) { ret = sqlite3_step(stmt); if (ret == SQLITE_ROW && sqlite3_column_type(stmt, 0) != SQLITE_NULL) { last_host_id = (unsigned)sqlite3_column_int64(stmt, 0); Log::info("ServerLobby", "%u was last server session max host id.", last_host_id); } ret = sqlite3_finalize(stmt); if (ret != SQLITE_OK) { Log::error("ServerLobby", "Error finalize database for query %s: %s", query.c_str(), sqlite3_errmsg(m_db)); m_server_stats_table = ""; } } else { Log::error("ServerLobby", "Error preparing database for query %s: %s", query.c_str(), sqlite3_errmsg(m_db)); m_server_stats_table = ""; } STKHost::get()->setNextHostId(last_host_id); // Update disconnected time (if stk crashed it will not be written) query = StringUtils::insertValues( "UPDATE %s SET disconnected_time = datetime('now') " "WHERE connected_time = disconnected_time;", m_server_stats_table.c_str()); easySQLQuery(query); #endif } // initServerStatsTable //----------------------------------------------------------------------------- void ServerLobby::destroyDatabase() { #ifdef ENABLE_SQLITE3 auto peers = STKHost::get()->getPeers(); for (auto& peer : peers) writeDisconnectInfoTable(peer.get()); if (m_db != NULL) sqlite3_close(m_db); #endif } // destroyDatabase //----------------------------------------------------------------------------- void ServerLobby::writeDisconnectInfoTable(STKPeer* peer) { #ifdef ENABLE_SQLITE3 if (m_server_stats_table.empty()) return; std::string query = StringUtils::insertValues( "UPDATE %s SET disconnected_time = datetime('now'), " "ping = %d, packet_loss = %d " "WHERE host_id = %u;", m_server_stats_table.c_str(), peer->getAveragePing(), peer->getPacketLoss(), peer->getHostId()); easySQLQuery(query); #endif } // writeDisconnectInfoTable //----------------------------------------------------------------------------- void ServerLobby::updateAddons() { m_addon_kts.first.clear(); m_addon_kts.second.clear(); m_addon_arenas.clear(); m_addon_soccers.clear(); std::set total_addons; 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()); } for (unsigned i = 0; i < track_manager->getNumberOfTracks(); i++) { const Track* track = track_manager->getTrack(i); if (track->isAddon()) total_addons.insert(track->getIdent()); } for (auto& addon : total_addons) { const KartProperties* kp = kart_properties_manager->getKart(addon); if (kp && kp->isAddon()) { m_addon_kts.first.insert(kp->getIdent()); continue; } Track* t = track_manager->getTrack(addon); if (!t || !t->isAddon() || t->isInternal()) continue; if (t->isArena()) m_addon_arenas.insert(t->getIdent()); else if (t->isSoccer()) m_addon_soccers.insert(t->getIdent()); else m_addon_kts.second.insert(t->getIdent()); } std::vector 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 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); if (ServerConfig::m_live_players) m_available_kts.first = m_official_kts.first; else m_available_kts.first = { all_k.begin(), all_k.end() }; } // updateAddons //----------------------------------------------------------------------------- /** Called whenever server is reset or game mode is changed. */ void ServerLobby::updateTracksForMode() { auto all_t = track_manager->getAllTrackIdentifiers(); if (all_t.size() >= 65536) all_t.resize(65535); m_available_kts.second = { all_t.begin(), all_t.end() }; RaceManager::MinorRaceModeType m = ServerConfig::getLocalGameMode(m_game_mode.load()).first; switch (m) { case RaceManager::MINOR_MODE_NORMAL_RACE: case RaceManager::MINOR_MODE_TIME_TRIAL: case RaceManager::MINOR_MODE_FOLLOW_LEADER: { auto it = m_available_kts.second.begin(); while (it != m_available_kts.second.end()) { Track* t = track_manager->getTrack(*it); if (t->isArena() || t->isSoccer() || t->isInternal()) { it = m_available_kts.second.erase(it); } else it++; } break; } case RaceManager::MINOR_MODE_FREE_FOR_ALL: case RaceManager::MINOR_MODE_CAPTURE_THE_FLAG: { auto it = m_available_kts.second.begin(); while (it != m_available_kts.second.end()) { Track* t = track_manager->getTrack(*it); if (RaceManager::get()->getMinorMode() == RaceManager::MINOR_MODE_CAPTURE_THE_FLAG) { if (!t->isCTF() || t->isInternal()) { it = m_available_kts.second.erase(it); } else it++; } else { if (!t->isArena() || t->isInternal()) { it = m_available_kts.second.erase(it); } else it++; } } break; } case RaceManager::MINOR_MODE_SOCCER: { auto it = m_available_kts.second.begin(); while (it != m_available_kts.second.end()) { Track* t = track_manager->getTrack(*it); if (!t->isSoccer() || t->isInternal()) { it = m_available_kts.second.erase(it); } else it++; } break; } default: assert(false); break; } } // updateTracksForMode //----------------------------------------------------------------------------- void ServerLobby::setup() { LobbyProtocol::setup(); m_battle_hit_capture_limit = 0; m_battle_time_limit = 0.0f; m_item_seed = 0; m_winner_peer_id = 0; m_client_starting_time = 0; m_ai_count = 0; auto players = STKHost::get()->getPlayersForNewGame(); if (m_game_setup->isGrandPrix() && !m_game_setup->isGrandPrixStarted()) { for (auto player : players) player->resetGrandPrixData(); } if (!m_game_setup->isGrandPrix() || !m_game_setup->isGrandPrixStarted()) { for (auto player : players) player->setKartName(""); } if (auto ai = m_ai_peer.lock()) { for (auto player : ai->getPlayerProfiles()) player->setKartName(""); } for (auto ai : m_ai_profiles) ai->setKartName(""); StateManager::get()->resetActivePlayers(); // We use maximum 16bit unsigned limit auto all_k = kart_properties_manager->getAllAvailableKarts(); if (all_k.size() >= 65536) all_k.resize(65535); if (ServerConfig::m_live_players) m_available_kts.first = m_official_kts.first; else m_available_kts.first = { all_k.begin(), all_k.end() }; NetworkConfig::get()->setTuxHitboxAddon(ServerConfig::m_live_players); updateTracksForMode(); m_server_has_loaded_world.store(false); // Initialise the data structures to detect if all clients and // the server are ready: resetPeersReady(); m_timeout.store(std::numeric_limits::max()); m_server_started_at = m_server_delay = 0; Log::info("ServerLobby", "Resetting the server to its initial state."); } // setup //----------------------------------------------------------------------------- bool ServerLobby::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()); // message not empty uint8_t message_type; message_type = data.getUInt8(); Log::info("ServerLobby", "Synchronous message of type %d received.", message_type); switch (message_type) { case LE_RACE_FINISHED_ACK: playerFinishedResult(event); break; case LE_LIVE_JOIN: liveJoinRequest(event); break; case LE_CLIENT_LOADED_WORLD: finishedLoadingLiveJoinClient(event); break; case LE_KART_INFO: handleKartInfo(event); break; case LE_CLIENT_BACK_LOBBY: clientInGameWantsToBackLobby(event); break; default: Log::error("ServerLobby", "Unknown message of type %d - ignored.", message_type); break; } // switch message_type return true; } // notifyEvent //----------------------------------------------------------------------------- void ServerLobby::handleChat(Event* event) { if (!checkDataSize(event, 1) || !ServerConfig::m_chat) return; // Update so that the peer is not kicked event->getPeer()->updateLastActivity(); const bool sender_in_game = event->getPeer()->isWaitingForGame(); int64_t last_message = event->getPeer()->getLastMessage(); int64_t elapsed_time = (int64_t)StkTime::getMonoTimeMs() - last_message; // Read ServerConfig for formula and details if (ServerConfig::m_chat_consecutive_interval > 0 && elapsed_time < ServerConfig::m_chat_consecutive_interval * 1000) event->getPeer()->updateConsecutiveMessages(true); else event->getPeer()->updateConsecutiveMessages(false); if (ServerConfig::m_chat_consecutive_interval > 0 && event->getPeer()->getConsecutiveMessages() > ServerConfig::m_chat_consecutive_interval / 2) { NetworkString* chat = getNetworkString(); chat->setSynchronous(true); core::stringw warn = "Spam detected"; chat->addUInt8(LE_CHAT).encodeString16(warn); event->getPeer()->sendPacket(chat, true/*reliable*/); delete chat; return; } core::stringw message; event->data().decodeString16(&message, 360/*max_len*/); KartTeam target_team = KART_TEAM_NONE; if (event->data().size() > 0) target_team = (KartTeam)event->data().getUInt8(); if (message.size() > 0) { // Red or blue square emoji if (target_team == KART_TEAM_RED) message = StringUtils::utf32ToWide({0x1f7e5, 0x20}) + message; else if (target_team == KART_TEAM_BLUE) message = StringUtils::utf32ToWide({0x1f7e6, 0x20}) + message; NetworkString* chat = getNetworkString(); chat->setSynchronous(true); chat->addUInt8(LE_CHAT).encodeString16(message); const bool game_started = m_state.load() != WAITING_FOR_START_GAME; core::stringw sender_name = event->getPeer()->getPlayerProfiles()[0]->getName(); STKHost::get()->sendPacketToAllPeersWith( [game_started, sender_in_game, target_team, sender_name, this] (STKPeer* p) { if (game_started) { if (p->isWaitingForGame() && !sender_in_game) return false; if (!p->isWaitingForGame() && sender_in_game) return false; if (target_team != KART_TEAM_NONE) { if (p->isSpectator()) return false; for (auto& player : p->getPlayerProfiles()) { if (player->getTeam() == target_team) return true; } return false; } } for (auto& peer : m_peers_muted_players) { if (auto peer_sp = peer.first.lock()) { if (peer_sp.get() == p && peer.second.find(sender_name) != peer.second.end()) return false; } } return true; }, chat); event->getPeer()->updateLastMessage(); delete chat; } } // handleChat //----------------------------------------------------------------------------- void ServerLobby::changeTeam(Event* event) { if (!ServerConfig::m_team_choosing || !RaceManager::get()->teamEnabled()) return; if (!checkDataSize(event, 1)) return; NetworkString& data = event->data(); uint8_t local_id = data.getUInt8(); auto& player = event->getPeer()->getPlayerProfiles().at(local_id); auto red_blue = STKHost::get()->getAllPlayersTeamInfo(); // At most 7 players on each team (for live join) if (player->getTeam() == KART_TEAM_BLUE) { if (red_blue.first >= 7) return; player->setTeam(KART_TEAM_RED); } else { if (red_blue.second >= 7) return; player->setTeam(KART_TEAM_BLUE); } updatePlayerList(); } // changeTeam //----------------------------------------------------------------------------- void ServerLobby::kickHost(Event* event) { if (m_server_owner.lock() != event->getPeerSP()) return; if (!checkDataSize(event, 4)) return; NetworkString& data = event->data(); uint32_t host_id = data.getUInt32(); std::shared_ptr peer = STKHost::get()->findPeerByHostId(host_id); // Ignore kicking ai peer if ai handling is on if (peer && (!ServerConfig::m_ai_handling || !peer->isAIPeer())) peer->kick(); } // kickHost //----------------------------------------------------------------------------- bool ServerLobby::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()); // message not empty uint8_t message_type; message_type = data.getUInt8(); Log::info("ServerLobby", "Message of type %d received.", message_type); switch(message_type) { case LE_CONNECTION_REQUESTED: connectionRequested(event); break; case LE_KART_SELECTION: kartSelectionRequested(event); break; case LE_CLIENT_LOADED_WORLD: finishedLoadingWorldClient(event); break; case LE_VOTE: handlePlayerVote(event); break; case LE_KICK_HOST: kickHost(event); break; case LE_CHANGE_TEAM: changeTeam(event); break; case LE_REQUEST_BEGIN: startSelection(event); break; case LE_CHAT: handleChat(event); break; case LE_CONFIG_SERVER: handleServerConfiguration(event); break; case LE_CHANGE_HANDICAP: changeHandicap(event); break; case LE_CLIENT_BACK_LOBBY: clientSelectingAssetsWantsToBackLobby(event); break; case LE_REPORT_PLAYER: writePlayerReport(event); break; case LE_ASSETS_UPDATE: handleAssets(event->data(), event->getPeer()); break; case LE_COMMAND: handleServerCommand(event, event->getPeerSP()); break; default: break; } // switch } // if (event->getType() == EVENT_TYPE_MESSAGE) else if (event->getType() == EVENT_TYPE_DISCONNECTED) { clientDisconnected(event); } // if (event->getType() == EVENT_TYPE_DISCONNECTED) return true; } // notifyEventAsynchronous //----------------------------------------------------------------------------- #ifdef ENABLE_SQLITE3 /* Every 1 minute STK will poll database: * 1. Set disconnected time to now for non-exists host. * 2. Clear expired player reports if necessary * 3. Kick active peer from ban list */ void ServerLobby::pollDatabase() { if (!ServerConfig::m_sql_management || !m_db) return; if (StkTime::getMonoTimeMs() < m_last_poll_db_time + 60000) return; m_last_poll_db_time = StkTime::getMonoTimeMs(); if (m_ip_ban_table_exists) { std::string query = "SELECT ip_start, ip_end, reason, description FROM "; query += ServerConfig::m_ip_ban_table; query += " WHERE datetime('now') > datetime(starting_time) AND " "(expired_days is NULL OR datetime" "(starting_time, '+'||expired_days||' days') > datetime('now'));"; auto peers = STKHost::get()->getPeers(); sqlite3_exec(m_db, query.c_str(), [](void* ptr, int count, char** data, char** columns) { std::vector >* peers_ptr = (std::vector >*)ptr; for (std::shared_ptr& p : *peers_ptr) { // IPv4 ban list atm if (p->isAIPeer() || p->getAddress().isIPv6()) continue; uint32_t ip_start = 0; uint32_t ip_end = 0; if (!StringUtils::fromString(data[0], ip_start)) continue; if (!StringUtils::fromString(data[1], ip_end)) continue; uint32_t peer_addr = p->getAddress().getIP(); if (ip_start <= peer_addr && ip_end >= peer_addr) { Log::info("ServerLobby", "Kick %s, reason: %s, description: %s", p->getAddress().toString().c_str(), data[2], data[3]); p->kick(); } } return 0; }, &peers, NULL); } if (m_ipv6_ban_table_exists) { std::string query = "SELECT ipv6_cidr, reason, description FROM "; query += ServerConfig::m_ipv6_ban_table; query += " WHERE datetime('now') > datetime(starting_time) AND " "(expired_days is NULL OR datetime" "(starting_time, '+'||expired_days||' days') > datetime('now'));"; auto peers = STKHost::get()->getPeers(); sqlite3_exec(m_db, query.c_str(), [](void* ptr, int count, char** data, char** columns) { std::vector >* peers_ptr = (std::vector >*)ptr; for (std::shared_ptr& p : *peers_ptr) { std::string ipv6; if (p->getAddress().isIPv6()) ipv6 = p->getAddress().toString(false); // IPv6 ban list atm if (p->isAIPeer() || ipv6.empty()) continue; char* ipv6_cidr = data[0]; if (insideIPv6CIDR(ipv6_cidr, ipv6.c_str()) == 1) { Log::info("ServerLobby", "Kick %s, reason: %s, description: %s", ipv6.c_str(), data[1], data[2]); p->kick(); } } return 0; }, &peers, NULL); } if (m_online_id_ban_table_exists) { std::string query = "SELECT online_id, reason, description FROM "; query += ServerConfig::m_online_id_ban_table; query += " WHERE datetime('now') > datetime(starting_time) AND " "(expired_days is NULL OR datetime" "(starting_time, '+'||expired_days||' days') > datetime('now'));"; auto peers = STKHost::get()->getPeers(); sqlite3_exec(m_db, query.c_str(), [](void* ptr, int count, char** data, char** columns) { std::vector >* peers_ptr = (std::vector >*)ptr; for (std::shared_ptr& p : *peers_ptr) { if (p->isAIPeer() || p->getPlayerProfiles().empty()) continue; uint32_t online_id = 0; if (!StringUtils::fromString(data[0], online_id)) continue; if (online_id == p->getPlayerProfiles()[0]->getOnlineId()) { Log::info("ServerLobby", "Kick %s, reason: %s, description: %s", p->getAddress().toString().c_str(), data[1], data[2]); p->kick(); } } return 0; }, &peers, NULL); } if (m_player_reports_table_exists && ServerConfig::m_player_reports_expired_days != 0.0f) { std::string query = StringUtils::insertValues( "DELETE FROM %s " "WHERE datetime" "(reported_time, '+%f days') < datetime('now');", ServerConfig::m_player_reports_table.c_str(), ServerConfig::m_player_reports_expired_days); easySQLQuery(query); } if (m_server_stats_table.empty()) return; std::string query; auto peers = STKHost::get()->getPeers(); std::vector exist_hosts; if (!peers.empty()) { for (auto& peer : peers) { if (!peer->isValidated()) continue; exist_hosts.push_back(peer->getHostId()); } } if (peers.empty() || exist_hosts.empty()) { query = StringUtils::insertValues( "UPDATE %s SET disconnected_time = datetime('now') " "WHERE connected_time = disconnected_time;", m_server_stats_table.c_str()); } else { std::ostringstream oss; oss << "UPDATE " << m_server_stats_table << " SET disconnected_time = datetime('now')" << " WHERE connected_time = disconnected_time AND" << " host_id NOT IN ("; for (unsigned i = 0; i < exist_hosts.size(); i++) { oss << exist_hosts[i]; if (i != (exist_hosts.size() - 1)) oss << ","; } oss << ");"; query = oss.str(); } easySQLQuery(query); } // pollDatabase //----------------------------------------------------------------------------- /** Run simple query with write lock waiting and optional function, this * function has no callback for the return (if any) by the query. * Return true if no error occurs */ bool ServerLobby::easySQLQuery(const std::string& query, std::function bind_function) const { if (!m_db) return false; sqlite3_stmt* stmt = NULL; int ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0); if (ret == SQLITE_OK) { if (bind_function) bind_function(stmt); ret = sqlite3_step(stmt); ret = sqlite3_finalize(stmt); if (ret != SQLITE_OK) { Log::error("ServerLobby", "Error finalize database for easy query %s: %s", query.c_str(), sqlite3_errmsg(m_db)); return false; } } else { Log::error("ServerLobby", "Error preparing database for easy query %s: %s", query.c_str(), sqlite3_errmsg(m_db)); return false; } return true; } // easySQLQuery //----------------------------------------------------------------------------- /* Write true to result if table name exists in database. */ void ServerLobby::checkTableExists(const std::string& table, bool& result) { if (!m_db) return; sqlite3_stmt* stmt = NULL; if (!table.empty()) { std::string query = StringUtils::insertValues( "SELECT count(type) FROM sqlite_master " "WHERE type='table' AND name='%s';", table.c_str()); int ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0); if (ret == SQLITE_OK) { ret = sqlite3_step(stmt); if (ret == SQLITE_ROW) { int number = sqlite3_column_int(stmt, 0); if (number == 1) { Log::info("ServerLobby", "Table named %s will used.", table.c_str()); result = true; } } ret = sqlite3_finalize(stmt); if (ret != SQLITE_OK) { Log::error("ServerLobby", "Error finalize database for query %s: %s", query.c_str(), sqlite3_errmsg(m_db)); } } } if (!result && !table.empty()) { Log::warn("ServerLobby", "Table named %s not found in database.", table.c_str()); } } // checkTableExists //----------------------------------------------------------------------------- std::string ServerLobby::ip2Country(const SocketAddress& addr) const { if (!m_db || !m_ip_geolocation_table_exists || addr.isLAN()) return ""; std::string cc_code; std::string query = StringUtils::insertValues( "SELECT country_code FROM %s " "WHERE `ip_start` <= %d AND `ip_end` >= %d " "ORDER BY `ip_start` DESC LIMIT 1;", ServerConfig::m_ip_geolocation_table.c_str(), addr.getIP(), addr.getIP()); sqlite3_stmt* stmt = NULL; int ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0); if (ret == SQLITE_OK) { ret = sqlite3_step(stmt); if (ret == SQLITE_ROW) { const char* country_code = (char*)sqlite3_column_text(stmt, 0); cc_code = country_code; } ret = sqlite3_finalize(stmt); if (ret != SQLITE_OK) { Log::error("ServerLobby", "Error finalize database for query %s: %s", query.c_str(), sqlite3_errmsg(m_db)); } } else { Log::error("ServerLobby", "Error preparing database for query %s: %s", query.c_str(), sqlite3_errmsg(m_db)); return ""; } return cc_code; } // ip2Country //----------------------------------------------------------------------------- std::string ServerLobby::ipv62Country(const SocketAddress& addr) const { if (!m_db || !m_ipv6_geolocation_table_exists) return ""; std::string cc_code; const std::string& ipv6 = addr.toString(false/*show_port*/); std::string query = StringUtils::insertValues( "SELECT country_code FROM %s " "WHERE `ip_start` <= upperIPv6(\"%s\") AND `ip_end` >= upperIPv6(\"%s\") " "ORDER BY `ip_start` DESC LIMIT 1;", ServerConfig::m_ipv6_geolocation_table.c_str(), ipv6.c_str(), ipv6.c_str()); sqlite3_stmt* stmt = NULL; int ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0); if (ret == SQLITE_OK) { ret = sqlite3_step(stmt); if (ret == SQLITE_ROW) { const char* country_code = (char*)sqlite3_column_text(stmt, 0); cc_code = country_code; } ret = sqlite3_finalize(stmt); if (ret != SQLITE_OK) { Log::error("ServerLobby", "Error finalize database for query %s: %s", query.c_str(), sqlite3_errmsg(m_db)); } } else { Log::error("ServerLobby", "Error preparing database for query %s: %s", query.c_str(), sqlite3_errmsg(m_db)); return ""; } return cc_code; } // ipv62Country #endif //----------------------------------------------------------------------------- void ServerLobby::writePlayerReport(Event* event) { #ifdef ENABLE_SQLITE3 if (!m_db || !m_player_reports_table_exists) return; STKPeer* reporter = event->getPeer(); if (!reporter->hasPlayerProfiles()) return; auto reporter_npp = reporter->getPlayerProfiles()[0]; uint32_t reporting_host_id = event->data().getUInt32(); core::stringw info; event->data().decodeString16(&info); if (info.empty()) return; auto reporting_peer = STKHost::get()->findPeerByHostId(reporting_host_id); if (!reporting_peer || !reporting_peer->hasPlayerProfiles()) return; auto reporting_npp = reporting_peer->getPlayerProfiles()[0]; std::string query; if (ServerConfig::m_ipv6_connection) { query = StringUtils::insertValues( "INSERT INTO %s " "(server_uid, reporter_ip, reporter_ipv6, reporter_online_id, reporter_username, " "info, reporting_ip, reporting_ipv6, reporting_online_id, reporting_username) " "VALUES (?, %u, \"%s\", %u, ?, ?, %u, \"%s\", %u, ?);", ServerConfig::m_player_reports_table.c_str(), !reporter->getAddress().isIPv6() ? reporter->getAddress().getIP() : 0, reporter->getAddress().isIPv6() ? reporter->getAddress().toString(false) : "", reporter_npp->getOnlineId(), !reporting_peer->getAddress().isIPv6() ? reporting_peer->getAddress().getIP() : 0, reporting_peer->getAddress().isIPv6() ? reporting_peer->getAddress().toString(false) : "", reporting_npp->getOnlineId()); } else { query = StringUtils::insertValues( "INSERT INTO %s " "(server_uid, reporter_ip, reporter_online_id, reporter_username, " "info, reporting_ip, reporting_online_id, reporting_username) " "VALUES (?, %u, %u, ?, ?, %u, %u, ?);", ServerConfig::m_player_reports_table.c_str(), reporter->getAddress().getIP(), reporter_npp->getOnlineId(), reporting_peer->getAddress().getIP(), reporting_npp->getOnlineId()); } bool written = easySQLQuery(query, [reporter_npp, reporting_npp, info](sqlite3_stmt* stmt) { // SQLITE_TRANSIENT to copy string if (sqlite3_bind_text(stmt, 1, ServerConfig::m_server_uid.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK) { Log::error("easySQLQuery", "Failed to bind %s.", ServerConfig::m_server_uid.c_str()); } if (sqlite3_bind_text(stmt, 2, StringUtils::wideToUtf8(reporter_npp->getName()).c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK) { Log::error("easySQLQuery", "Failed to bind %s.", StringUtils::wideToUtf8(reporter_npp->getName()).c_str()); } if (sqlite3_bind_text(stmt, 3, StringUtils::wideToUtf8(info).c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK) { Log::error("easySQLQuery", "Failed to bind %s.", StringUtils::wideToUtf8(info).c_str()); } if (sqlite3_bind_text(stmt, 4, StringUtils::wideToUtf8(reporting_npp->getName()).c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK) { Log::error("easySQLQuery", "Failed to bind %s.", StringUtils::wideToUtf8(reporting_npp->getName()).c_str()); } }); if (written) { NetworkString* success = getNetworkString(); success->setSynchronous(true); success->addUInt8(LE_REPORT_PLAYER).addUInt8(1) .encodeString(reporting_npp->getName()); event->getPeer()->sendPacket(success, true/*reliable*/); delete success; } #endif } // writePlayerReport //----------------------------------------------------------------------------- /** Find out the public IP server or poll STK server asynchronously. */ void ServerLobby::asynchronousUpdate() { if (m_rs_state.load() == RS_ASYNC_RESET) { resetVotingTime(); resetServer(); m_rs_state.store(RS_NONE); } for (auto it = m_peers_muted_players.begin(); it != m_peers_muted_players.end();) { if (it->first.expired()) it = m_peers_muted_players.erase(it); else it++; } #ifdef ENABLE_SQLITE3 pollDatabase(); #endif // Check if server owner has left updateServerOwner(); if (ServerConfig::m_ranked && m_state.load() == WAITING_FOR_START_GAME) clearDisconnectedRankedPlayer(); if (allowJoinedPlayersWaiting() || (m_game_setup->isGrandPrix() && m_state.load() == WAITING_FOR_START_GAME)) { // Only poll the STK server if server has been registered. if (m_server_id_online.load() != 0 && m_state.load() != REGISTER_SELF_ADDRESS) checkIncomingConnectionRequests(); handlePendingConnection(); } if (m_server_id_online.load() != 0 && allowJoinedPlayersWaiting() && StkTime::getMonoTimeMs() > m_last_unsuccess_poll_time && StkTime::getMonoTimeMs() > m_last_success_poll_time.load() + 30000) { Log::warn("ServerLobby", "Trying auto server recovery."); // For auto server recovery wait 3 seconds for next try m_last_unsuccess_poll_time = StkTime::getMonoTimeMs() + 3000; registerServer(false/*first_time*/); } switch (m_state.load()) { case SET_PUBLIC_ADDRESS: { // In case of LAN we don't need our public address or register with the // STK server, so we can directly go to the accepting clients state. if (NetworkConfig::get()->isLAN()) { m_state = WAITING_FOR_START_GAME; updatePlayerList(); STKHost::get()->startListening(); return; } auto ip_type = NetworkConfig::get()->getIPType(); // Set the IPv6 address first for possible IPv6 only server if (isIPv6Socket() && ip_type >= NetworkConfig::IP_V6) { STKHost::get()->setPublicAddress(AF_INET6); } if (ip_type == NetworkConfig::IP_V4 || ip_type == NetworkConfig::IP_DUAL_STACK) { STKHost::get()->setPublicAddress(AF_INET); } if (STKHost::get()->getPublicAddress().isUnset() && STKHost::get()->getPublicIPv6Address().empty()) { m_state = ERROR_LEAVE; } else { STKHost::get()->startListening(); m_state = REGISTER_SELF_ADDRESS; } break; } case REGISTER_SELF_ADDRESS: { if (m_game_setup->isGrandPrixStarted() || m_registered_for_once_only) { m_state = WAITING_FOR_START_GAME; updatePlayerList(); break; } // Register this server with the STK server. This will block // this thread, because there is no need for the protocol manager // to react to any requests before the server is registered. if (m_server_registering.expired() && m_server_id_online.load() == 0) registerServer(true/*first_time*/); if (m_server_registering.expired()) { // Finished registering server if (m_server_id_online.load() != 0) { // For non grand prix server we only need to register to stk // addons once if (allowJoinedPlayersWaiting()) m_registered_for_once_only = true; m_state = WAITING_FOR_START_GAME; updatePlayerList(); } } break; } case WAITING_FOR_START_GAME: { if (ServerConfig::m_owner_less) { unsigned players = 0; STKHost::get()->updatePlayers(&players); if (((int)players >= ServerConfig::m_min_start_game_players || m_game_setup->isGrandPrixStarted()) && m_timeout.load() == std::numeric_limits::max()) { m_timeout.store((int64_t)StkTime::getMonoTimeMs() + (int64_t) (ServerConfig::m_start_game_counter * 1000.0f)); } else if ((int)players < ServerConfig::m_min_start_game_players && !m_game_setup->isGrandPrixStarted()) { resetPeersReady(); if (m_timeout.load() != std::numeric_limits::max()) updatePlayerList(); m_timeout.store(std::numeric_limits::max()); } if (m_timeout.load() < (int64_t)StkTime::getMonoTimeMs() || (checkPeersReady(true/*ignore_ai_peer*/) && (int)players >= ServerConfig::m_min_start_game_players)) { resetPeersReady(); startSelection(); return; } } break; } case ERROR_LEAVE: { requestTerminate(); m_state = EXITING; STKHost::get()->requestShutdown(); break; } case WAIT_FOR_WORLD_LOADED: { // For WAIT_FOR_WORLD_LOADED and SELECTING make sure there are enough // players to start next game, otherwise exiting and let main thread // reset if (m_end_voting_period.load() == 0) return; unsigned player_in_game = 0; STKHost::get()->updatePlayers(&player_in_game); // Reset lobby will be done in main thread if ((player_in_game == 1 && ServerConfig::m_ranked) || player_in_game == 0) { resetVotingTime(); return; } // m_server_has_loaded_world is set by main thread with atomic write if (m_server_has_loaded_world.load() == false) return; if (!checkPeersReady( ServerConfig::m_ai_handling && m_ai_count == 0/*ignore_ai_peer*/)) return; // Reset for next state usage resetPeersReady(); configPeersStartTime(); break; } case SELECTING: { if (m_end_voting_period.load() == 0) return; unsigned player_in_game = 0; STKHost::get()->updatePlayers(&player_in_game); if ((player_in_game == 1 && ServerConfig::m_ranked) || player_in_game == 0) { resetVotingTime(); return; } PeerVote winner_vote; m_winner_peer_id = std::numeric_limits::max(); bool go_on_race = false; if (ServerConfig::m_track_voting) go_on_race = handleAllVotes(&winner_vote, &m_winner_peer_id); else if (m_game_setup->isGrandPrixStarted() || isVotingOver()) { winner_vote = *m_default_vote; go_on_race = true; } if (go_on_race) { *m_default_vote = winner_vote; m_item_seed = (uint32_t)StkTime::getTimeSinceEpoch(); ItemManager::updateRandomSeed(m_item_seed); m_game_setup->setRace(winner_vote); bool has_always_on_spectators = false; auto players = STKHost::get() ->getPlayersForNewGame(&has_always_on_spectators); auto ai_instance = m_ai_peer.lock(); if (supportsAI()) { if (ai_instance) { auto ai_profiles = ai_instance->getPlayerProfiles(); if (m_ai_count > 0) { ai_profiles.resize(m_ai_count); players.insert(players.end(), ai_profiles.begin(), ai_profiles.end()); } } else if (!m_ai_profiles.empty()) { players.insert(players.end(), m_ai_profiles.begin(), m_ai_profiles.end()); } } m_game_setup->sortPlayersForGrandPrix(players); m_game_setup->sortPlayersForGame(players); for (unsigned i = 0; i < players.size(); i++) { std::shared_ptr& player = players[i]; std::shared_ptr peer = player->getPeer(); if (peer) peer->clearAvailableKartIDs(); } for (unsigned i = 0; i < players.size(); i++) { std::shared_ptr& player = players[i]; std::shared_ptr peer = player->getPeer(); if (peer) peer->addAvailableKartID(i); } getHitCaptureLimit(); // Add placeholder players for live join addLiveJoinPlaceholder(players); // If player chose random / hasn't chose any kart for (unsigned i = 0; i < players.size(); i++) { if (players[i]->getKartName().empty()) { RandomGenerator rg; std::set::iterator it = m_available_kts.first.begin(); std::advance(it, rg.get((int)m_available_kts.first.size())); players[i]->setKartName(*it); } } NetworkString* load_world_message = getLoadWorldMessage(players, false/*live_join*/); m_game_setup->setHitCaptureTime(m_battle_hit_capture_limit, m_battle_time_limit); uint16_t flag_return_time = (uint16_t)stk_config->time2Ticks( ServerConfig::m_flag_return_timeout); RaceManager::get()->setFlagReturnTicks(flag_return_time); uint16_t flag_deactivated_time = (uint16_t)stk_config->time2Ticks( ServerConfig::m_flag_deactivated_time); RaceManager::get()->setFlagDeactivatedTicks(flag_deactivated_time); configRemoteKart(players, 0); // Reset for next state usage resetPeersReady(); m_state = LOAD_WORLD; sendMessageToPeers(load_world_message); // updatePlayerList so the in lobby players (if any) can see always // spectators join the game if (has_always_on_spectators) updatePlayerList(); delete load_world_message; } break; } default: break; } } // asynchronousUpdate //----------------------------------------------------------------------------- void ServerLobby::encodePlayers(BareNetworkString* bns, std::vector >& players) const { bns->addUInt8((uint8_t)players.size()); for (unsigned i = 0; i < players.size(); i++) { std::shared_ptr& player = players[i]; bns->encodeString(player->getName()) .addUInt32(player->getHostId()) .addFloat(player->getDefaultKartColor()) .addUInt32(player->getOnlineId()) .addUInt8(player->getHandicap()) .addUInt8(player->getLocalPlayerId()) .addUInt8( RaceManager::get()->teamEnabled() ? player->getTeam() : KART_TEAM_NONE) .encodeString(player->getCountryCode()); bns->encodeString(player->getKartName()); } } // encodePlayers //----------------------------------------------------------------------------- NetworkString* ServerLobby::getLoadWorldMessage( std::vector >& players, bool live_join) const { NetworkString* load_world_message = getNetworkString(); load_world_message->setSynchronous(true); load_world_message->addUInt8(LE_LOAD_WORLD); load_world_message->addUInt32(m_winner_peer_id); m_default_vote->encode(load_world_message); load_world_message->addUInt8(live_join ? 1 : 0); encodePlayers(load_world_message, players); load_world_message->addUInt32(m_item_seed); if (RaceManager::get()->isBattleMode()) { load_world_message->addUInt32(m_battle_hit_capture_limit) .addFloat(m_battle_time_limit); uint16_t flag_return_time = (uint16_t)stk_config->time2Ticks( ServerConfig::m_flag_return_timeout); load_world_message->addUInt16(flag_return_time); uint16_t flag_deactivated_time = (uint16_t)stk_config->time2Ticks( ServerConfig::m_flag_deactivated_time); load_world_message->addUInt16(flag_deactivated_time); } for (unsigned i = 0; i < players.size(); i++) players[i]->getKartData().encode(load_world_message); return load_world_message; } // getLoadWorldMessage //----------------------------------------------------------------------------- /** Returns true if server can be live joined or spectating */ bool ServerLobby::canLiveJoinNow() const { bool live_join = ServerConfig::m_live_players && worldIsActive(); if (!live_join) return false; if (RaceManager::get()->modeHasLaps()) { // No spectate when fastest kart is nearly finish, because if there // is endcontroller the spectating remote may not be knowing this // on time LinearWorld* w = dynamic_cast(World::getWorld()); if (!w) return false; AbstractKart* fastest_kart = NULL; for (unsigned i = 0; i < w->getNumKarts(); i++) { fastest_kart = w->getKartAtPosition(i + 1); if (fastest_kart && !fastest_kart->isEliminated()) break; } if (!fastest_kart) return false; float progress = w->getOverallDistance( fastest_kart->getWorldKartId()) / (Track::getCurrentTrack()->getTrackLength() * (float)RaceManager::get()->getNumLaps()); if (progress > 0.9f) return false; } return live_join; } // canLiveJoinNow //----------------------------------------------------------------------------- /** Returns true if world is active for clients to live join, spectate or * going back to lobby live */ bool ServerLobby::worldIsActive() const { return World::getWorld() && RaceEventManager::get()->isRunning() && !RaceEventManager::get()->isRaceOver() && World::getWorld()->getPhase() == WorldStatus::RACE_PHASE; } // worldIsActive //----------------------------------------------------------------------------- /** \ref STKPeer peer will be reset back to the lobby with reason * \ref BackLobbyReason blr */ void ServerLobby::rejectLiveJoin(STKPeer* peer, BackLobbyReason blr) { NetworkString* reset = getNetworkString(2); reset->setSynchronous(true); reset->addUInt8(LE_BACK_LOBBY).addUInt8(blr); peer->sendPacket(reset, /*reliable*/true); delete reset; updatePlayerList(); NetworkString* server_info = getNetworkString(); server_info->setSynchronous(true); server_info->addUInt8(LE_SERVER_INFO); m_game_setup->addServerInfo(server_info); peer->sendPacket(server_info, /*reliable*/true); delete server_info; } // rejectLiveJoin //----------------------------------------------------------------------------- /** This message is like kartSelectionRequested, but it will send the peer * load world message if he can join the current started game. */ void ServerLobby::liveJoinRequest(Event* event) { STKPeer* peer = event->getPeer(); const NetworkString& data = event->data(); if (!canLiveJoinNow()) { rejectLiveJoin(peer, BLR_NO_GAME_FOR_LIVE_JOIN); return; } bool spectator = data.getUInt8() == 1; if (RaceManager::get()->modeHasLaps() && !spectator) { // No live join for linear race rejectLiveJoin(peer, BLR_NO_GAME_FOR_LIVE_JOIN); return; } peer->clearAvailableKartIDs(); if (!spectator) { auto spectators_by_limit = getSpectatorsByLimit(); setPlayerKarts(data, peer); std::vector used_id; for (unsigned i = 0; i < peer->getPlayerProfiles().size(); i++) { int id = getReservedId(peer->getPlayerProfiles()[i], i); if (id == -1) break; used_id.push_back(id); } if ((used_id.size() != peer->getPlayerProfiles().size()) || (spectators_by_limit.find(event->getPeerSP()) != spectators_by_limit.end())) { for (unsigned i = 0; i < peer->getPlayerProfiles().size(); i++) peer->getPlayerProfiles()[i]->setKartName(""); for (unsigned i = 0; i < used_id.size(); i++) { RemoteKartInfo& rki = RaceManager::get()->getKartInfo(used_id[i]); rki.makeReserved(); } Log::info("ServerLobby", "Too many players (%d) try to live join", (int)peer->getPlayerProfiles().size()); rejectLiveJoin(peer, BLR_NO_PLACE_FOR_LIVE_JOIN); return; } for (int id : used_id) { Log::info("ServerLobby", "%s live joining with reserved kart id %d.", peer->getAddress().toString().c_str(), id); peer->addAvailableKartID(id); } } else { Log::info("ServerLobby", "%s spectating now.", peer->getAddress().toString().c_str()); } std::vector > players = getLivePlayers(); NetworkString* load_world_message = getLoadWorldMessage(players, true/*live_join*/); peer->sendPacket(load_world_message, true/*reliable*/); delete load_world_message; peer->updateLastActivity(); } // liveJoinRequest //----------------------------------------------------------------------------- /** Get a list of current ingame players for live join or spectate. */ std::vector > ServerLobby::getLivePlayers() const { std::vector > players; for (unsigned i = 0; i < RaceManager::get()->getNumPlayers(); i++) { const RemoteKartInfo& rki = RaceManager::get()->getKartInfo(i); std::shared_ptr player = rki.getNetworkPlayerProfile().lock(); if (!player) { if (RaceManager::get()->modeHasLaps()) { player = std::make_shared( nullptr, rki.getPlayerName(), std::numeric_limits::max(), rki.getDefaultKartColor(), rki.getOnlineId(), rki.getHandicap(), rki.getLocalPlayerId(), KART_TEAM_NONE, rki.getCountryCode()); player->setKartName(rki.getKartName()); } else { player = NetworkPlayerProfile::getReservedProfile( RaceManager::get()->getMinorMode() == RaceManager::MINOR_MODE_FREE_FOR_ALL ? KART_TEAM_NONE : rki.getKartTeam()); } } players.push_back(player); } return players; } // getLivePlayers //----------------------------------------------------------------------------- /** Decide where to put the live join player depends on his team and game mode. */ int ServerLobby::getReservedId(std::shared_ptr& p, unsigned local_id) const { const bool is_ffa = RaceManager::get()->getMinorMode() == RaceManager::MINOR_MODE_FREE_FOR_ALL; int red_count = 0; int blue_count = 0; for (unsigned i = 0; i < RaceManager::get()->getNumPlayers(); i++) { RemoteKartInfo& rki = RaceManager::get()->getKartInfo(i); if (rki.isReserved()) continue; bool disconnected = rki.disconnected(); if (RaceManager::get()->getKartInfo(i).getKartTeam() == KART_TEAM_RED && !disconnected) red_count++; else if (RaceManager::get()->getKartInfo(i).getKartTeam() == KART_TEAM_BLUE && !disconnected) blue_count++; } KartTeam target_team = red_count > blue_count ? KART_TEAM_BLUE : KART_TEAM_RED; for (unsigned i = 0; i < RaceManager::get()->getNumPlayers(); i++) { RemoteKartInfo& rki = RaceManager::get()->getKartInfo(i); std::shared_ptr player = rki.getNetworkPlayerProfile().lock(); if (!player) { if (is_ffa) { rki.copyFrom(p, local_id); return i; } if (ServerConfig::m_team_choosing) { if ((p->getTeam() == KART_TEAM_RED && rki.getKartTeam() == KART_TEAM_RED) || (p->getTeam() == KART_TEAM_BLUE && rki.getKartTeam() == KART_TEAM_BLUE)) { rki.copyFrom(p, local_id); return i; } } else { if (rki.getKartTeam() == target_team) { p->setTeam(target_team); rki.copyFrom(p, local_id); return i; } } } } return -1; } // getReservedId //----------------------------------------------------------------------------- /** Finally put the kart in the world and inform client the current world * status, (including current confirmed item state, kart scores...) */ void ServerLobby::finishedLoadingLiveJoinClient(Event* event) { std::shared_ptr peer = event->getPeerSP(); if (!canLiveJoinNow()) { rejectLiveJoin(peer.get(), BLR_NO_GAME_FOR_LIVE_JOIN); return; } bool live_joined_in_time = true; for (const int id : peer->getAvailableKartIDs()) { const RemoteKartInfo& rki = RaceManager::get()->getKartInfo(id); if (rki.isReserved()) { live_joined_in_time = false; break; } } if (!live_joined_in_time) { Log::warn("ServerLobby", "%s can't live-join in time.", peer->getAddress().toString().c_str()); rejectLiveJoin(peer.get(), BLR_NO_GAME_FOR_LIVE_JOIN); return; } World* w = World::getWorld(); assert(w); uint64_t live_join_start_time = STKHost::get()->getNetworkTimer(); // Instead of using getTicksSinceStart we caculate the current world ticks // only from network timer, because if the server hangs in between the // world ticks may not be up to date // 2000 is the time for ready set, remove 3 ticks after for minor // correction (make it more looks like getTicksSinceStart if server has no // hang int cur_world_ticks = stk_config->time2Ticks( (live_join_start_time - m_server_started_at - 2000) / 1000.f) - 3; // Give 3 seconds for all peers to get new kart info m_last_live_join_util_ticks = cur_world_ticks + stk_config->time2Ticks(3.0f); live_join_start_time -= m_server_delay; live_join_start_time += 3000; bool spectator = false; for (const int id : peer->getAvailableKartIDs()) { World::getWorld()->addReservedKart(id); const RemoteKartInfo& rki = RaceManager::get()->getKartInfo(id); addLiveJoiningKart(id, rki, m_last_live_join_util_ticks); Log::info("ServerLobby", "%s succeeded live-joining with kart id %d.", peer->getAddress().toString().c_str(), id); } if (peer->getAvailableKartIDs().empty()) { Log::info("ServerLobby", "%s spectating succeeded.", peer->getAddress().toString().c_str()); spectator = true; } const uint8_t cc = (uint8_t)Track::getCurrentTrack()->getCheckManager()->getCheckStructureCount(); NetworkString* ns = getNetworkString(10); ns->setSynchronous(true); ns->addUInt8(LE_LIVE_JOIN_ACK).addUInt64(m_client_starting_time) .addUInt8(cc).addUInt64(live_join_start_time) .addUInt32(m_last_live_join_util_ticks); NetworkItemManager* nim = dynamic_cast (Track::getCurrentTrack()->getItemManager()); assert(nim); nim->saveCompleteState(ns); nim->addLiveJoinPeer(peer); w->saveCompleteState(ns, peer.get()); if (RaceManager::get()->supportsLiveJoining()) { // Only needed in non-racing mode as no need players can added after // starting of race std::vector > players = getLivePlayers(); encodePlayers(ns, players); for (unsigned i = 0; i < players.size(); i++) players[i]->getKartData().encode(ns); } m_peers_ready[peer] = false; peer->setWaitingForGame(false); peer->setSpectator(spectator); peer->sendPacket(ns, true/*reliable*/); delete ns; updatePlayerList(); peer->updateLastActivity(); } // finishedLoadingLiveJoinClient //----------------------------------------------------------------------------- /** Simple finite state machine. Once this * is known, register the server and its address with the stk server so that * client can find it. */ void ServerLobby::update(int ticks) { World* w = World::getWorld(); bool world_started = m_state.load() >= WAIT_FOR_WORLD_LOADED && m_state.load() <= RACING && m_server_has_loaded_world.load(); bool all_players_in_world_disconnected = (w != NULL && world_started); int sec = ServerConfig::m_kick_idle_player_seconds; if (world_started) { for (unsigned i = 0; i < RaceManager::get()->getNumPlayers(); i++) { RemoteKartInfo& rki = RaceManager::get()->getKartInfo(i); std::shared_ptr player = rki.getNetworkPlayerProfile().lock(); if (player) { if (w) all_players_in_world_disconnected = false; } else continue; auto peer = player->getPeer(); if (!peer) continue; if (peer->idleForSeconds() > 60 && w && w->getKart(i)->isEliminated()) { // Remove loading world too long (60 seconds) live join peer Log::info("ServerLobby", "%s hasn't live-joined within" " 60 seconds, remove it.", peer->getAddress().toString().c_str()); rki.makeReserved(); continue; } if (!peer->isAIPeer() && sec > 0 && peer->idleForSeconds() > sec && !peer->isDisconnected() && NetworkConfig::get()->isWAN()) { if (w && w->getKart(i)->hasFinishedRace()) continue; // Don't kick in game GUI server host so he can idle in game if (m_process_type == PT_CHILD && peer->getHostId() == m_client_server_host_id.load()) continue; Log::info("ServerLobby", "%s %s has been idle for more than" " %d seconds, kick.", peer->getAddress().toString().c_str(), StringUtils::wideToUtf8(rki.getPlayerName()).c_str(), sec); peer->kick(); } } } if (w) setGameStartedProgress(w->getGameStartedProgress()); else resetGameStartedProgress(); if (w && w->getPhase() == World::RACE_PHASE) { storePlayingTrack(RaceManager::get()->getTrackName()); } else storePlayingTrack(""); // Reset server to initial state if no more connected players if (m_rs_state.load() == RS_WAITING) { if ((RaceEventManager::get() && !RaceEventManager::get()->protocolStopped()) || !GameProtocol::emptyInstance()) return; exitGameState(); m_rs_state.store(RS_ASYNC_RESET); } STKHost::get()->updatePlayers(); if (m_rs_state.load() == RS_NONE && (m_state.load() > WAITING_FOR_START_GAME || m_game_setup->isGrandPrixStarted()) && (STKHost::get()->getPlayersInGame() == 0 || all_players_in_world_disconnected)) { if (RaceEventManager::get() && RaceEventManager::get()->isRunning()) { // Send a notification to all players who may have start live join // or spectate to go back to lobby NetworkString* back_to_lobby = getNetworkString(2); back_to_lobby->setSynchronous(true); back_to_lobby->addUInt8(LE_BACK_LOBBY).addUInt8(BLR_NONE); sendMessageToPeersInServer(back_to_lobby, /*reliable*/true); delete back_to_lobby; RaceEventManager::get()->stop(); RaceEventManager::get()->getProtocol()->requestTerminate(); GameProtocol::lock()->requestTerminate(); } else if (auto ai = m_ai_peer.lock()) { // Reset AI peer for empty server, which will delete world NetworkString* back_to_lobby = getNetworkString(2); back_to_lobby->setSynchronous(true); back_to_lobby->addUInt8(LE_BACK_LOBBY).addUInt8(BLR_NONE); ai->sendPacket(back_to_lobby, /*reliable*/true); delete back_to_lobby; } resetVotingTime(); m_game_setup->stopGrandPrix(); m_rs_state.store(RS_WAITING); return; } if (m_rs_state.load() != RS_NONE) return; // Reset for ranked server if in kart / track selection has only 1 player if (ServerConfig::m_ranked && m_state.load() == SELECTING && STKHost::get()->getPlayersInGame() == 1) { NetworkString* back_lobby = getNetworkString(2); back_lobby->setSynchronous(true); back_lobby->addUInt8(LE_BACK_LOBBY) .addUInt8(BLR_ONE_PLAYER_IN_RANKED_MATCH); sendMessageToPeers(back_lobby, /*reliable*/true); delete back_lobby; resetVotingTime(); m_game_setup->stopGrandPrix(); m_rs_state.store(RS_ASYNC_RESET); } handlePlayerDisconnection(); switch (m_state.load()) { case SET_PUBLIC_ADDRESS: case REGISTER_SELF_ADDRESS: case WAITING_FOR_START_GAME: case WAIT_FOR_WORLD_LOADED: case WAIT_FOR_RACE_STARTED: { // Waiting for asynchronousUpdate break; } case SELECTING: // The function playerTrackVote will trigger the next state // once all track votes have been received. break; case LOAD_WORLD: Log::info("ServerLobbyRoom", "Starting the race loading."); // This will create the world instance, i.e. load track and karts loadWorld(); m_state = WAIT_FOR_WORLD_LOADED; break; case RACING: if (World::getWorld() && RaceEventManager::get() && RaceEventManager::get()->isRunning()) { checkRaceFinished(); } break; case WAIT_FOR_RACE_STOPPED: if (!RaceEventManager::get()->protocolStopped() || !GameProtocol::emptyInstance()) return; // This will go back to lobby in server (and exit the current race) exitGameState(); // Reset for next state usage resetPeersReady(); // Set the delay before the server forces all clients to exit the race // result screen and go back to the lobby m_timeout.store((int64_t)StkTime::getMonoTimeMs() + 15000); m_state = RESULT_DISPLAY; sendMessageToPeers(m_result_ns, /*reliable*/ true); Log::info("ServerLobby", "End of game message sent"); break; case RESULT_DISPLAY: if (checkPeersReady(true/*ignore_ai_peer*/) || (int64_t)StkTime::getMonoTimeMs() > m_timeout.load()) { // Send a notification to all clients to exit // the race result screen NetworkString* back_to_lobby = getNetworkString(2); back_to_lobby->setSynchronous(true); back_to_lobby->addUInt8(LE_BACK_LOBBY).addUInt8(BLR_NONE); sendMessageToPeersInServer(back_to_lobby, /*reliable*/true); delete back_to_lobby; m_rs_state.store(RS_ASYNC_RESET); } break; case ERROR_LEAVE: case EXITING: break; } } // update //----------------------------------------------------------------------------- /** Register this server (i.e. its public address) with the STK server * so that clients can find it. It blocks till a response from the * stk server is received (this function is executed from the * ProtocolManager thread). The information about this client is added * to the table 'server'. */ void ServerLobby::registerServer(bool first_time) { // ======================================================================== class RegisterServerRequest : public Online::XMLRequest { private: std::weak_ptr m_server_lobby; bool m_first_time; protected: virtual void afterOperation() { Online::XMLRequest::afterOperation(); const XMLNode* result = getXMLData(); std::string rec_success; auto sl = m_server_lobby.lock(); if (!sl) return; if (result->get("success", &rec_success) && rec_success == "yes") { const XMLNode* server = result->getNode("server"); assert(server); const XMLNode* server_info = server->getNode("server-info"); assert(server_info); unsigned server_id_online = 0; server_info->get("id", &server_id_online); assert(server_id_online != 0); bool is_official = false; server_info->get("official", &is_official); if (!is_official && ServerConfig::m_ranked) { Log::fatal("ServerLobby", "You don't have permission to " "host a ranked server."); } Log::info("ServerLobby", "Server %d is now online.", server_id_online); sl->m_server_id_online.store(server_id_online); sl->m_last_success_poll_time.store(StkTime::getMonoTimeMs()); return; } Log::error("ServerLobby", "%s", StringUtils::wideToUtf8(getInfo()).c_str()); // Exit now if failed to register to stk addons for first time if (m_first_time) sl->m_state.store(ERROR_LEAVE); } public: RegisterServerRequest(std::shared_ptr sl, bool first_time) : XMLRequest(Online::RequestManager::HTTP_MAX_PRIORITY), m_server_lobby(sl), m_first_time(first_time) {} }; // RegisterServerRequest auto request = std::make_shared( std::dynamic_pointer_cast(shared_from_this()), first_time); NetworkConfig::get()->setServerDetails(request, "create"); const SocketAddress& addr = STKHost::get()->getPublicAddress(); request->addParameter("address", addr.getIP() ); request->addParameter("port", addr.getPort() ); request->addParameter("private_port", STKHost::get()->getPrivatePort() ); request->addParameter("name", m_game_setup->getServerNameUtf8()); request->addParameter("max_players", ServerConfig::m_server_max_players); int difficulty = m_difficulty.load(); request->addParameter("difficulty", difficulty); int game_mode = m_game_mode.load(); request->addParameter("game_mode", game_mode); const std::string& pw = ServerConfig::m_private_server_password; request->addParameter("password", (unsigned)(!pw.empty())); request->addParameter("version", (unsigned)ServerConfig::m_server_version); bool ipv6_only = addr.isUnset(); if (!ipv6_only) { Log::info("ServerLobby", "Public IPv4 server address %s", addr.toString().c_str()); } if (!STKHost::get()->getPublicIPv6Address().empty()) { request->addParameter("address_ipv6", STKHost::get()->getPublicIPv6Address()); Log::info("ServerLobby", "Public IPv6 server address %s", STKHost::get()->getPublicIPv6Address().c_str()); } request->queue(); m_server_registering = request; } // registerServer //----------------------------------------------------------------------------- /** Unregister this server (i.e. its public address) with the STK server, * currently when karts enter kart selection screen it will be done or quit * stk. */ void ServerLobby::unregisterServer(bool now, std::weak_ptr sl) { // ======================================================================== class UnRegisterServerRequest : public Online::XMLRequest { private: std::weak_ptr m_server_lobby; protected: virtual void afterOperation() { Online::XMLRequest::afterOperation(); const XMLNode* result = getXMLData(); std::string rec_success; if (result->get("success", &rec_success) && rec_success == "yes") { // Clear the server online for next register // For grand prix server if (auto sl = m_server_lobby.lock()) sl->m_server_id_online.store(0); return; } Log::error("ServerLobby", "%s", StringUtils::wideToUtf8(getInfo()).c_str()); } public: UnRegisterServerRequest(std::weak_ptr sl) : XMLRequest(Online::RequestManager::HTTP_MAX_PRIORITY), m_server_lobby(sl) {} }; // UnRegisterServerRequest auto request = std::make_shared(sl); NetworkConfig::get()->setServerDetails(request, "stop"); const SocketAddress& addr = STKHost::get()->getPublicAddress(); request->addParameter("address", addr.getIP()); request->addParameter("port", addr.getPort()); bool ipv6_only = addr.isUnset(); if (!ipv6_only) { Log::info("ServerLobby", "Unregister server address %s", addr.toString().c_str()); } else { Log::info("ServerLobby", "Unregister server address %s", STKHost::get()->getValidPublicAddress().c_str()); } // No need to check for result as server will be auto-cleared anyway // when no polling is done if (now) { request->executeNow(); } else request->queue(); } // unregisterServer //----------------------------------------------------------------------------- /** Instructs all clients to start the kart selection. If event is NULL, * the command comes from the owner less server. */ void ServerLobby::startSelection(const Event *event) { if (event != NULL) { if (m_state != WAITING_FOR_START_GAME) { Log::warn("ServerLobby", "Received startSelection while being in state %d.", m_state.load()); return; } if (ServerConfig::m_owner_less) { m_peers_ready.at(event->getPeerSP()) = !m_peers_ready.at(event->getPeerSP()); updatePlayerList(); return; } if (event->getPeerSP() != m_server_owner.lock()) { Log::warn("ServerLobby", "Client %d is not authorised to start selection.", event->getPeer()->getHostId()); return; } } if (!ServerConfig::m_owner_less && ServerConfig::m_team_choosing && RaceManager::get()->teamEnabled()) { auto red_blue = STKHost::get()->getAllPlayersTeamInfo(); if ((red_blue.first == 0 || red_blue.second == 0) && red_blue.first + red_blue.second != 1) { Log::warn("ServerLobby", "Bad team choosing."); if (event) { NetworkString* bt = getNetworkString(); bt->setSynchronous(true); bt->addUInt8(LE_BAD_TEAM); event->getPeer()->sendPacket(bt, true/*reliable*/); delete bt; } return; } } // Remove karts / tracks from server that are not supported on all clients std::set karts_erase, tracks_erase; auto peers = STKHost::get()->getPeers(); std::set always_spectate_peers; bool has_peer_plays_game = false; for (auto peer : peers) { if (!peer->isValidated() || peer->isWaitingForGame()) continue; peer->eraseServerKarts(m_available_kts.first, karts_erase); peer->eraseServerTracks(m_available_kts.second, tracks_erase); if (peer->alwaysSpectate()) always_spectate_peers.insert(peer.get()); else if (!peer->isAIPeer()) has_peer_plays_game = true; } // Disable always spectate peers if no players join the game if (!has_peer_plays_game) { for (STKPeer* peer : always_spectate_peers) peer->setAlwaysSpectate(ASM_NONE); always_spectate_peers.clear(); } else { // We make those always spectate peer waiting for game so it won't // be able to vote, this will be reset in STKHost::getPlayersForNewGame // This will also allow a correct number of in game players for max // arena players handling for (STKPeer* peer : always_spectate_peers) peer->setWaitingForGame(true); } unsigned max_player = 0; STKHost::get()->updatePlayers(&max_player); // Set late coming player to spectate if too many players auto spectators_by_limit = getSpectatorsByLimit(); if (spectators_by_limit.size() == peers.size()) { Log::error("ServerLobby", "Too many players and cannot set " "spectate for late coming players!"); return; } for(auto &peer : spectators_by_limit) { peer->setAlwaysSpectate(ASM_FULL); peer->setWaitingForGame(true); always_spectate_peers.insert(peer.get()); } for (const std::string& kart_erase : karts_erase) { m_available_kts.first.erase(kart_erase); } for (const std::string& track_erase : tracks_erase) { m_available_kts.second.erase(track_erase); } max_player = 0; STKHost::get()->updatePlayers(&max_player); if (auto ai = m_ai_peer.lock()) { if (supportsAI()) { unsigned total_ai_available = (unsigned)ai->getPlayerProfiles().size(); m_ai_count = max_player > total_ai_available ? 0 : total_ai_available - max_player + 1; // Disable ai peer for this game if (m_ai_count == 0) ai->setValidated(false); else ai->setValidated(true); } else { ai->setValidated(false); m_ai_count = 0; } } else m_ai_count = 0; if (RaceManager::get()->getMinorMode() == RaceManager::MINOR_MODE_FREE_FOR_ALL) { auto it = m_available_kts.second.begin(); while (it != m_available_kts.second.end()) { Track* t = track_manager->getTrack(*it); if (t->getMaxArenaPlayers() < max_player) { it = m_available_kts.second.erase(it); } else it++; } } if (m_available_kts.second.empty()) { Log::error("ServerLobby", "No tracks for playing!"); return; } RandomGenerator rg; std::set::iterator it = m_available_kts.second.begin(); std::advance(it, rg.get((int)m_available_kts.second.size())); m_default_vote->m_track_name = *it; switch (RaceManager::get()->getMinorMode()) { case RaceManager::MINOR_MODE_NORMAL_RACE: case RaceManager::MINOR_MODE_TIME_TRIAL: case RaceManager::MINOR_MODE_FOLLOW_LEADER: { Track* t = track_manager->getTrack(*it); assert(t); m_default_vote->m_num_laps = t->getDefaultNumberOfLaps(); m_default_vote->m_reverse = rg.get(2) == 0; break; } case RaceManager::MINOR_MODE_FREE_FOR_ALL: { m_default_vote->m_num_laps = 0; m_default_vote->m_reverse = rg.get(2) == 0; break; } case RaceManager::MINOR_MODE_CAPTURE_THE_FLAG: { m_default_vote->m_num_laps = 0; m_default_vote->m_reverse = 0; break; } case RaceManager::MINOR_MODE_SOCCER: { if (m_game_setup->isSoccerGoalTarget()) { m_default_vote->m_num_laps = (uint8_t)(UserConfigParams::m_num_goals); if (m_default_vote->m_num_laps > 10) m_default_vote->m_num_laps = (uint8_t)5; } else { m_default_vote->m_num_laps = (uint8_t)(UserConfigParams::m_soccer_time_limit); if (m_default_vote->m_num_laps > 15) m_default_vote->m_num_laps = (uint8_t)7; } m_default_vote->m_reverse = rg.get(2) == 0; break; } default: assert(false); break; } if (!allowJoinedPlayersWaiting()) { ProtocolManager::lock()->findAndTerminate(PROTOCOL_CONNECTION); if (m_server_id_online.load() != 0) { unregisterServer(false/*now*/, std::dynamic_pointer_cast(shared_from_this())); } } startVotingPeriod(ServerConfig::m_voting_timeout); NetworkString *ns = getNetworkString(1); // Start selection - must be synchronous since the receiver pushes // a new screen, which must be done from the main thread. ns->setSynchronous(true); ns->addUInt8(LE_START_SELECTION) .addFloat(ServerConfig::m_voting_timeout) .addUInt8(m_game_setup->isGrandPrixStarted() ? 1 : 0) .addUInt8(ServerConfig::m_auto_game_time_ratio > 0.0f ? 1 : 0) .addUInt8(ServerConfig::m_track_voting ? 1 : 0); const auto& all_k = m_available_kts.first; const auto& all_t = m_available_kts.second; 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); } sendMessageToPeers(ns, /*reliable*/true); delete ns; m_state = SELECTING; if (!always_spectate_peers.empty()) { NetworkString* back_lobby = getNetworkString(2); back_lobby->setSynchronous(true); back_lobby->addUInt8(LE_BACK_LOBBY).addUInt8(BLR_SPECTATING_NEXT_GAME); STKHost::get()->sendPacketToAllPeersWith( [always_spectate_peers](STKPeer* peer) { return always_spectate_peers.find(peer) != always_spectate_peers.end(); }, back_lobby, /*reliable*/true); delete back_lobby; updatePlayerList(); } if (!allowJoinedPlayersWaiting()) { // Drop all pending players and keys if doesn't allow joinning-waiting for (auto& p : m_pending_connection) { if (auto peer = p.first.lock()) peer->disconnect(); } m_pending_connection.clear(); std::unique_lock ul(m_keys_mutex); m_keys.clear(); ul.unlock(); } // Will be changed after the first vote received m_timeout.store(std::numeric_limits::max()); } // startSelection //----------------------------------------------------------------------------- /** Query the STK server for connection requests. For each connection request * start a ConnectToPeer protocol. */ void ServerLobby::checkIncomingConnectionRequests() { // First poll every 5 seconds. Return if no polling needs to be done. const uint64_t POLL_INTERVAL = 5000; static uint64_t last_poll_time = 0; if (StkTime::getMonoTimeMs() < last_poll_time + POLL_INTERVAL || StkTime::getMonoTimeMs() > m_last_success_poll_time.load() + 30000) return; // Keep the port open, it can be sent to anywhere as we will send to the // correct peer later in ConnectToPeer. if (ServerConfig::m_firewalled_server) { BareNetworkString data; data.addUInt8(0); const SocketAddress* stun_v4 = STKHost::get()->getStunIPv4Address(); const SocketAddress* stun_v6 = STKHost::get()->getStunIPv6Address(); if (stun_v4) STKHost::get()->sendRawPacket(data, *stun_v4); if (stun_v6) STKHost::get()->sendRawPacket(data, *stun_v6); } // Now poll the stk server last_poll_time = StkTime::getMonoTimeMs(); // ======================================================================== class PollServerRequest : public Online::XMLRequest { private: std::weak_ptr m_server_lobby; std::weak_ptr m_protocol_manager; protected: virtual void afterOperation() { Online::XMLRequest::afterOperation(); const XMLNode* result = getXMLData(); std::string success; if (!result->get("success", &success) || success != "yes") { Log::error("ServerLobby", "Poll server request failed: %s", StringUtils::wideToUtf8(getInfo()).c_str()); return; } // Now start a ConnectToPeer protocol for each connection request const XMLNode * users_xml = result->getNode("users"); std::map keys; auto sl = m_server_lobby.lock(); if (!sl) return; sl->m_last_success_poll_time.store(StkTime::getMonoTimeMs()); if (sl->m_state.load() != WAITING_FOR_START_GAME && !sl->allowJoinedPlayersWaiting()) { sl->replaceKeys(keys); return; } sl->removeExpiredPeerConnection(); for (unsigned int i = 0; i < users_xml->getNumNodes(); i++) { uint32_t addr, id; uint16_t port; std::string ipv6; users_xml->getNode(i)->get("ip", &addr); users_xml->getNode(i)->get("ipv6", &ipv6); users_xml->getNode(i)->get("port", &port); users_xml->getNode(i)->get("id", &id); users_xml->getNode(i)->get("aes-key", &keys[id].m_aes_key); users_xml->getNode(i)->get("aes-iv", &keys[id].m_aes_iv); users_xml->getNode(i)->get("username", &keys[id].m_name); users_xml->getNode(i)->get("country-code", &keys[id].m_country_code); keys[id].m_tried = false; if (ServerConfig::m_firewalled_server) { SocketAddress peer_addr(addr, port); if (!ipv6.empty()) peer_addr.init(ipv6, port); peer_addr.convertForIPv6Socket(isIPv6Socket()); std::string peer_addr_str = peer_addr.toString(); if (sl->m_pending_peer_connection.find(peer_addr_str) != sl->m_pending_peer_connection.end()) { continue; } auto ctp = std::make_shared(peer_addr); if (auto pm = m_protocol_manager.lock()) pm->requestStart(ctp); sl->addPeerConnection(peer_addr_str); } } sl->replaceKeys(keys); } public: PollServerRequest(std::shared_ptr sl, std::shared_ptr pm) : XMLRequest(Online::RequestManager::HTTP_MAX_PRIORITY), m_server_lobby(sl), m_protocol_manager(pm) { m_disable_sending_log = true; } }; // PollServerRequest // ======================================================================== auto request = std::make_shared( std::dynamic_pointer_cast(shared_from_this()), ProtocolManager::lock()); NetworkConfig::get()->setServerDetails(request, "poll-connection-requests"); const SocketAddress& addr = STKHost::get()->getPublicAddress(); request->addParameter("address", addr.getIP() ); request->addParameter("port", addr.getPort()); request->addParameter("current-players", getLobbyPlayers()); request->addParameter("game-started", m_state.load() == WAITING_FOR_START_GAME ? 0 : 1); std::string current_track = getPlayingTrackIdent(); if (!current_track.empty()) request->addParameter("current-track", getPlayingTrackIdent()); request->queue(); } // checkIncomingConnectionRequests //----------------------------------------------------------------------------- /** Checks if the race is finished, and if so informs the clients and switches * to state RESULT_DISPLAY, during which the race result gui is shown and all * clients can click on 'continue'. */ void ServerLobby::checkRaceFinished() { assert(RaceEventManager::get()->isRunning()); assert(World::getWorld()); if (!RaceEventManager::get()->isRaceOver()) return; Log::info("ServerLobby", "The game is considered finished."); // notify the network world that it is stopped RaceEventManager::get()->stop(); // stop race protocols before going back to lobby (end race) RaceEventManager::get()->getProtocol()->requestTerminate(); GameProtocol::lock()->requestTerminate(); // Save race result before delete the world m_result_ns->clear(); m_result_ns->addUInt8(LE_RACE_FINISHED); if (m_game_setup->isGrandPrix()) { // fastest lap int fastest_lap = static_cast(World::getWorld())->getFastestLapTicks(); m_result_ns->addUInt32(fastest_lap); m_result_ns->encodeString(static_cast(World::getWorld()) ->getFastestLapKartName()); // all gp tracks m_result_ns->addUInt8((uint8_t)m_game_setup->getTotalGrandPrixTracks()) .addUInt8((uint8_t)m_game_setup->getAllTracks().size()); for (const std::string& gp_track : m_game_setup->getAllTracks()) m_result_ns->encodeString(gp_track); // each kart score and total time m_result_ns->addUInt8((uint8_t)RaceManager::get()->getNumPlayers()); for (unsigned i = 0; i < RaceManager::get()->getNumPlayers(); i++) { int last_score = RaceManager::get()->getKartScore(i); int cur_score = last_score; float overall_time = RaceManager::get()->getOverallTime(i); if (auto player = RaceManager::get()->getKartInfo(i).getNetworkPlayerProfile().lock()) { last_score = player->getScore(); cur_score += last_score; overall_time = overall_time + player->getOverallTime(); player->setScore(cur_score); player->setOverallTime(overall_time); } m_result_ns->addUInt32(last_score).addUInt32(cur_score) .addFloat(overall_time); } } else if (RaceManager::get()->modeHasLaps()) { int fastest_lap = static_cast(World::getWorld())->getFastestLapTicks(); m_result_ns->addUInt32(fastest_lap); m_result_ns->encodeString(static_cast(World::getWorld()) ->getFastestLapKartName()); } uint8_t ranking_changes_indication = 0; if (ServerConfig::m_ranked && RaceManager::get()->modeHasLaps()) ranking_changes_indication = 1; m_result_ns->addUInt8(ranking_changes_indication); if (ServerConfig::m_ranked) { computeNewRankings(); submitRankingsToAddons(); } m_state.store(WAIT_FOR_RACE_STOPPED); } // checkRaceFinished //----------------------------------------------------------------------------- /** Compute the new player's rankings used in ranked servers */ void ServerLobby::computeNewRankings() { // TODO : go over the variables and look // for things that can be simplified away. // e.g. can new/prev be simplified ? // No ranking for battle mode if (!RaceManager::get()->modeHasLaps()) return; std::vector raw_scores_change; std::vector new_raw_scores; std::vector prev_raw_scores; std::vector prev_scores; std::vector new_rating_deviations; std::vector prev_rating_deviations; std::vector prev_disconnects; //bitflag std::vector disconnects; World* w = World::getWorld(); assert(w); unsigned player_count = RaceManager::get()->getNumPlayers(); m_result_ns->addUInt8((uint8_t)player_count); // If all players quitted the race, we assume something went wrong // and skip entirely rating and statistics updates. for (unsigned i = 0; i < player_count; i++) { if (!w->getKart(i)->isEliminated()) break; if ((i + 1) == player_count) return; } // Initialize data vectors for (unsigned i = 0; i < player_count; i++) { const uint32_t id = RaceManager::get()->getKartInfo(i).getOnlineId(); double prev_raw_score = m_raw_scores.at(id); new_raw_scores.push_back(prev_raw_score); prev_raw_scores.push_back(prev_raw_score); prev_scores.push_back(m_scores.at(id)); double prev_deviation = m_rating_deviations.at(id); new_rating_deviations.push_back(prev_deviation); prev_rating_deviations.push_back(prev_deviation); prev_disconnects.push_back(m_num_ranked_disconnects.at(id)); } // Update some variables for (unsigned i = 0; i < player_count; i++) { const uint32_t id = RaceManager::get()->getKartInfo(i).getOnlineId(); //First, update the number of ranked races m_num_ranked_races.at(id)++; // Update the number of disconnects // We store the last 64 results as bit flags in a 64-bit int. // This way, shifting flushes the oldest result. m_num_ranked_disconnects.at(id) <<= 1; if (w->getKart(i)->isEliminated()) m_num_ranked_disconnects.at(id)++; // std::popcount is C++20 only std::bitset<64> b(m_num_ranked_disconnects.at(id)); disconnects.push_back(b.count()); } // In this loop, considering the race as a set // of head to head minimatches, we compute : // I - Point changes for each ordered player pair. // In a (p1, p2) pair, only p1's rating is changed. // However, the loop will also go over (p2, p1). // Point changes can be assymetric. // II - Rating deviation changes for (unsigned i = 0; i < player_count; i++) { raw_scores_change.push_back(0.0); double player1_raw_scores = new_raw_scores[i]; if (w->getKart(i)->getHandicap()) player1_raw_scores -= HANDICAP_OFFSET; // If the player has quitted before the race end, // the time value will be incorrect, but it will not be used double player1_time = RaceManager::get()->getKartRaceTime(i); double player1_rd = prev_rating_deviations[i]; // On a disconnect, increase RD once, // no matter how many opponents if (w->getKart(i)->isEliminated() && disconnects[i] >= 3) new_rating_deviations[i] = prev_rating_deviations[i] + BASE_RD_PER_DISCONNECT + VAR_RD_PER_DISCONNECT * (disconnects[i] - 3); // Loop over all opponents for (unsigned j = 0; j < player_count; j++) { // Don't compare a player with himself if (i == j) continue; // No change between two quitting players if ( w->getKart(i)->isEliminated() && w->getKart(j)->isEliminated()) continue; double diff, result, expected_result, ranking_importance, max_time; diff = result = expected_result = ranking_importance = max_time = 0.0; double player2_raw_scores = new_raw_scores[j]; if (w->getKart(j)->getHandicap()) player2_raw_scores -= HANDICAP_OFFSET; double player2_time = RaceManager::get()->getKartRaceTime(j); double player2_rd = prev_rating_deviations[j]; // Each result can be viewed as new data helping to refine our previous // estimates. But first, we need to assess how reliable this new data is // compared to existing estimates. bool handicap_used = w->getKart(i)->getHandicap() || w->getKart(j)->getHandicap(); double accuracy = computeDataAccuracy(player1_rd, player2_rd, player1_raw_scores, player2_raw_scores, player_count, handicap_used); // Now that we've computed the reliability value, // we can proceed with computing the points gained or lost // Compute the result and race ranking importance double mode_factor = getModeFactor(); if (w->getKart(i)->isEliminated()) { // Recurring disconnects are punished through // increased RD and higher RD floor, // not through higher raw score loss result = 0.0; player1_time = player2_time * 1.2; // for getTimeSpread } else if (w->getKart(j)->isEliminated()) { result = 1.0; player2_time = player1_time * 1.2; } else { result = computeH2HResult(player1_time, player2_time); } max_time = std::min(MAX_SCALING_TIME, std::max(player1_time, player2_time)); ranking_importance = accuracy * mode_factor * scalingValueForTime(max_time); // Compute the expected result using an ELO-like function diff = player2_raw_scores - player1_raw_scores; expected_result = 1.0/ (1.0 + std::pow(10.0, diff / ( BASE_RANKING_POINTS / 2.0 * getModeSpread() * getTimeSpread(std::min(player1_time, player2_time))))); // Compute the ranking change raw_scores_change[i] += ranking_importance * (result - expected_result); // We now update the rating deviation. The change // depends on the current RD, on the result's accuracy, // on how expected the result was (upsets can increase RD) // If there was a disconnect in this race, RD was handled once already if (!w->getKart(i)->isEliminated()) { // First the RD reduction based on accuracy and current RD double rd_change_factor = accuracy * 0.0016; double rd_change = (-1) * prev_rating_deviations[i] * rd_change_factor; // If the unexpected result happened, we add a RD increase // TODO : more reliable would be accumulating an expected_result/result // differential over time, weighted through relative RDs. // If that differential goes high, then increase RD while decaying // the differential. Some work needed to ensure sensible maths. double upset = std::abs(result - expected_result); if (upset > 0.5) { // Renormalize so expected result 50% is 1.0 and expected result 100% is 0.0 upset = 2.0 - 2 * upset; upset = std::max(0.02, upset); // If upsets happen at the rate predicted by expected score, // this won't prevent the rating deviation from going down. // However, if upsets are at least twice more frequent than expected, RD will go up. rd_change += MIN_RATING_DEVIATION * rd_change_factor / upset; } // Minimum RD will be handled after all iterative RD change have been done, // so as to avoid the order in which player pairs are computed changing results. new_rating_deviations[i] += rd_change; } } } // Don't merge it in the main loop as new_scores value are used there for (unsigned i = 0; i < player_count; i++) { new_raw_scores[i] += raw_scores_change[i]; const uint32_t id = RaceManager::get()->getKartInfo(i).getOnlineId(); m_raw_scores.at(id) = new_raw_scores[i]; // Ensure RD doesn't go below the RD floor. // The minimum RD is increased in case of repeated disconnects double disconnects_floor = 0; if (disconnects[i] >= 3) { int n = disconnects[i] - 3; disconnects_floor = (disconnects[i]-2) * BASE_RD_PER_DISCONNECT + VAR_RD_PER_DISCONNECT * (n * (n+1)) / 2; } new_rating_deviations[i] = std::max(new_rating_deviations[i], MIN_RATING_DEVIATION + disconnects_floor); m_rating_deviations.at(id) = new_rating_deviations[i]; // Update the main public rating. At min RD, it is equal to the raw score. m_scores.at(id) = m_raw_scores.at(id) - 3*new_rating_deviations[i] + 3*MIN_RATING_DEVIATION; if (m_scores.at(id) > m_max_scores.at(id)) m_max_scores.at(id) = m_scores.at(id); } // Used to display rating change at the end of a race for (unsigned i = 0; i < player_count; i++) { const uint32_t id = RaceManager::get()->getKartInfo(i).getOnlineId(); double change = m_scores.at(id) - prev_scores[i]; m_result_ns->addFloat((float)change); } } // computeNewRankings //----------------------------------------------------------------------------- /** Returns the mode race importance factor, * used to make ranking move slower in more random modes. */ double ServerLobby::getModeFactor() { if (RaceManager::get()->isTimeTrialMode()) return 1.0; return 0.75; } // getModeFactor //----------------------------------------------------------------------------- /** Returns the mode spread factor, used so that a similar difference in * skill will result in a similar ranking difference in more random modes. */ double ServerLobby::getModeSpread() { if (RaceManager::get()->isTimeTrialMode()) return 1.0; //TODO: the value used here for normal races is a wild guess. // When hard data to the spread tendencies of time-trial // and normal mode becomes available, update this to make // the spreads more comparable return 1.25; } // getModeSpread //----------------------------------------------------------------------------- /** Returns the time spread factor. * Short races are more random, so the expected result changes depending * on race duration. */ double ServerLobby::getTimeSpread(double time) { return sqrt(120.0 / time); } // getTimeSpread //----------------------------------------------------------------------------- /** Compute the scaling value of a given time * This is linear to race duration, getTimeSpread takes care * of expecting a more random result in shorter races. */ double ServerLobby::scalingValueForTime(double time) { return time * BASE_POINTS_PER_SECOND; } // scalingValueForTime //----------------------------------------------------------------------------- /** Computes the score of a head-to-head minimatch. * If time difference > 2,5% ; the result is 1 (complete win of player 1) * or 0 (complete loss of player 1) * Otherwise, it is averaged between 0 and 1. */ double ServerLobby::computeH2HResult(double player1_time, double player2_time) { double max_time = std::max(player1_time, player2_time); double min_time = std::min(player1_time, player2_time); double result = (max_time - min_time) / (min_time / 20.0); result = std::min(1.0, 0.5 + result); if (player2_time <= player1_time) result = 1.0 - result; return result; } // computeH2HResult //----------------------------------------------------------------------------- /** Computes a relative factor indicating how much informative value * the new race result gives us. * * For a player with a high own rating deviation, the current rating is unreliable * so any new data holds more importance. This is crucial to allow reasonably * fast rating convergence of new players, provided they play accurately rated opponents. * * When the opponent has a high rating deviation, the expected scores are likely off. * Therefore, the information from such a result is much less valuable. * * We also reduce rating changes when the player ratings are very different, even * after considering the uncertainties from rating deviation. * This is multi-purpose : * - With a very high rating difference, random race events (very poor luck, disconnects) * are very likely to be the cause of any upset, so the rate of legitimate upsets is * unreliable. No rating method is safe. * - Attempting to "farm" much lower rated players against which a practical 100% winrate * may be reached (outside of random events) becomes very ineffective. Instead, * to gain rating points, the player has incentive to play well-rated opponents. * - The primary goal is to ensure that two players of equal rating would be about * evenly matched in head-to-head. If two strong players each beat a much weaker third * player, very little information is gained on how a direct head-to-head between the * strong players would go. * For the purposes of this rating computation, we assume that the informational value * of a race is roughly proportional to the likelihood of the weaker player winning. * We cap the effect so that losing to a much weaker player still costs rating points. * * In a race with many players, a single event can have a significant impact on the * results of all the H2H. To avoid races with high players count having too strong * rating swings, we apply a modifier scaling down accuracy. * * Finally, while handicap is allowed in ranked races and a rating offset is applied * to keep expected results realistic (without incentivizing playing handicap-only), * the results of such races are much less reliable. */ double ServerLobby::computeDataAccuracy(double player1_rd, double player2_rd, double player1_scores, double player2_scores, int player_count, bool handicap_used) { double accuracy = player1_rd / (sqrt(player2_rd) * sqrt(MIN_RATING_DEVIATION)); double strong_lowerbound = (player1_scores > player2_scores) ? player1_scores - 3 * player1_rd : player2_scores - 3 * player2_rd; double weak_upperbound = (player1_scores > player2_scores) ? player2_scores + 3 * player2_rd : player1_scores + 3 * player1_rd; if (weak_upperbound < strong_lowerbound) { double diff = strong_lowerbound - weak_upperbound; diff = diff / (BASE_RANKING_POINTS / 2.0); // The expected result is that of the weaker player and is between 0 and 0.5 double expected_result = 1.0/ (1.0 + std::pow(10.0, diff)); expected_result = std::max(0.2, sqrt(2*expected_result)); accuracy *= expected_result; } // Reduce the importance of single h2h in a race with many players. // The overall impact of a race with more players is still always bigger. double player_count_modifier = 2.0 / sqrt((double) player_count); accuracy *= player_count_modifier; // Races with handicap are unreliable for ranking if (handicap_used) accuracy *= 0.25; return accuracy; } //----------------------------------------------------------------------------- /** Called when a client disconnects. * \param event The disconnect event. */ void ServerLobby::clientDisconnected(Event* event) { auto players_on_peer = event->getPeer()->getPlayerProfiles(); if (players_on_peer.empty()) return; NetworkString* msg = getNetworkString(2); const bool waiting_peer_disconnected = event->getPeer()->isWaitingForGame(); msg->setSynchronous(true); msg->addUInt8(LE_PLAYER_DISCONNECTED); msg->addUInt8((uint8_t)players_on_peer.size()) .addUInt32(event->getPeer()->getHostId()); for (auto p : players_on_peer) { std::string name = StringUtils::wideToUtf8(p->getName()); msg->encodeString(name); Log::info("ServerLobby", "%s disconnected", name.c_str()); } // Don't show waiting peer disconnect message to in game player STKHost::get()->sendPacketToAllPeersWith([waiting_peer_disconnected] (STKPeer* p) { if (!p->isValidated()) return false; if (!p->isWaitingForGame() && waiting_peer_disconnected) return false; return true; }, msg); updatePlayerList(); delete msg; writeDisconnectInfoTable(event->getPeer()); } // clientDisconnected //----------------------------------------------------------------------------- void ServerLobby::clearDisconnectedRankedPlayer() { for (auto it = m_ranked_players.begin(); it != m_ranked_players.end();) { if (it->second.expired()) { const uint32_t id = it->first; m_scores.erase(id); m_max_scores.erase(id); m_num_ranked_races.erase(id); m_raw_scores.erase(id); m_rating_deviations.erase(id); m_num_ranked_disconnects.erase(id); it = m_ranked_players.erase(it); } else { it++; } } } // clearDisconnectedRankedPlayer //----------------------------------------------------------------------------- void ServerLobby::kickPlayerWithReason(STKPeer* peer, const char* reason) const { NetworkString *message = getNetworkString(2); message->setSynchronous(true); message->addUInt8(LE_CONNECTION_REFUSED).addUInt8(RR_BANNED); message->encodeString(std::string(reason)); peer->cleanPlayerProfiles(); peer->sendPacket(message, true/*reliable*/, false/*encrypted*/); peer->reset(); delete message; } // kickPlayerWithReason //----------------------------------------------------------------------------- void ServerLobby::saveIPBanTable(const SocketAddress& addr) { #ifdef ENABLE_SQLITE3 if (addr.isIPv6() || !m_db || !m_ip_ban_table_exists) return; std::string query = StringUtils::insertValues( "INSERT INTO %s (ip_start, ip_end) " "VALUES (%u, %u);", ServerConfig::m_ip_ban_table.c_str(), addr.getIP(), addr.getIP()); easySQLQuery(query); #endif } // saveIPBanTable //----------------------------------------------------------------------------- bool ServerLobby::handleAssets(const NetworkString& ns, STKPeer* peer) { std::set client_karts, client_tracks; const unsigned kart_num = ns.getUInt16(); const unsigned track_num = ns.getUInt16(); for (unsigned i = 0; i < kart_num; i++) { std::string kart; ns.decodeString(&kart); client_karts.insert(kart); } for (unsigned i = 0; i < track_num; i++) { std::string track; ns.decodeString(&track); client_tracks.insert(track); } // Drop this player if he doesn't have at least 1 kart / track the same // as server float okt = 0.0f; float ott = 0.0f; for (auto& client_kart : client_karts) { if (m_official_kts.first.find(client_kart) != m_official_kts.first.end()) okt += 1.0f; } okt = okt / (float)m_official_kts.first.size(); for (auto& client_track : client_tracks) { if (m_official_kts.second.find(client_track) != m_official_kts.second.end()) ott += 1.0f; } ott = ott / (float)m_official_kts.second.size(); std::set karts_erase, tracks_erase; for (const std::string& server_kart : m_available_kts.first) { if (client_karts.find(server_kart) == client_karts.end()) { karts_erase.insert(server_kart); } } for (const std::string& server_track : m_available_kts.second) { if (client_tracks.find(server_track) == client_tracks.end()) { tracks_erase.insert(server_track); } } if (karts_erase.size() == m_available_kts.first.size() || tracks_erase.size() == m_available_kts.second.size() || okt < ServerConfig::m_official_karts_threshold || ott < ServerConfig::m_official_tracks_threshold) { NetworkString *message = getNetworkString(2); message->setSynchronous(true); message->addUInt8(LE_CONNECTION_REFUSED) .addUInt8(RR_INCOMPATIBLE_DATA); peer->cleanPlayerProfiles(); peer->sendPacket(message, true/*reliable*/, false/*encrypted*/); peer->reset(); delete message; Log::verbose("ServerLobby", "Player has incompatible karts / tracks."); return false; } std::array addons_scores = {{ -1, -1, -1, -1 }}; size_t addon_kart = 0; size_t addon_track = 0; size_t addon_arena = 0; size_t addon_soccer = 0; for (auto& kart : m_addon_kts.first) { if (client_karts.find(kart) != client_karts.end()) addon_kart++; } for (auto& track : m_addon_kts.second) { if (client_tracks.find(track) != client_tracks.end()) addon_track++; } for (auto& arena : m_addon_arenas) { if (client_tracks.find(arena) != client_tracks.end()) addon_arena++; } for (auto& soccer : m_addon_soccers) { if (client_tracks.find(soccer) != client_tracks.end()) addon_soccer++; } if (!m_addon_kts.first.empty()) { addons_scores[AS_KART] = int ((float)addon_kart / (float)m_addon_kts.first.size() * 100.0); } if (!m_addon_kts.second.empty()) { addons_scores[AS_TRACK] = int ((float)addon_track / (float)m_addon_kts.second.size() * 100.0); } if (!m_addon_arenas.empty()) { addons_scores[AS_ARENA] = int ((float)addon_arena / (float)m_addon_arenas.size() * 100.0); } if (!m_addon_soccers.empty()) { addons_scores[AS_SOCCER] = int ((float)addon_soccer / (float)m_addon_soccers.size() * 100.0); } // Save available karts and tracks from clients in STKPeer so if this peer // disconnects later in lobby it won't affect current players peer->setAvailableKartsTracks(client_karts, client_tracks); peer->setAddonsScores(addons_scores); if (m_process_type == PT_CHILD && peer->getHostId() == m_client_server_host_id.load()) { // Update child process addons list too so player can choose later updateAddons(); updateTracksForMode(); } return true; } // handleAssets //----------------------------------------------------------------------------- void ServerLobby::connectionRequested(Event* event) { std::shared_ptr peer = event->getPeerSP(); NetworkString& data = event->data(); if (!checkDataSize(event, 14)) return; peer->cleanPlayerProfiles(); // can we add the player ? if (!allowJoinedPlayersWaiting() && (m_state.load() != WAITING_FOR_START_GAME || m_game_setup->isGrandPrixStarted())) { NetworkString *message = getNetworkString(2); message->setSynchronous(true); message->addUInt8(LE_CONNECTION_REFUSED).addUInt8(RR_BUSY); // send only to the peer that made the request and disconnect it now peer->sendPacket(message, true/*reliable*/, false/*encrypted*/); peer->reset(); delete message; Log::verbose("ServerLobby", "Player refused: selection started"); return; } // Check server version int version = data.getUInt32(); if (version < stk_config->m_min_server_version || version > stk_config->m_max_server_version) { NetworkString *message = getNetworkString(2); message->setSynchronous(true); message->addUInt8(LE_CONNECTION_REFUSED) .addUInt8(RR_INCOMPATIBLE_DATA); peer->sendPacket(message, true/*reliable*/, false/*encrypted*/); peer->reset(); delete message; Log::verbose("ServerLobby", "Player refused: wrong server version"); return; } std::string user_version; data.decodeString(&user_version); event->getPeer()->setUserVersion(user_version); unsigned list_caps = data.getUInt16(); std::set caps; for (unsigned i = 0; i < list_caps; i++) { std::string cap; data.decodeString(&cap); caps.insert(cap); } event->getPeer()->setClientCapabilities(caps); if (!handleAssets(data, event->getPeer())) return; unsigned player_count = data.getUInt8(); uint32_t online_id = 0; uint32_t encrypted_size = 0; online_id = data.getUInt32(); encrypted_size = data.getUInt32(); // Will be disconnected if banned by IP testBannedForIP(peer.get()); if (peer->isDisconnected()) return; testBannedForIPv6(peer.get()); if (peer->isDisconnected()) return; if (online_id != 0) testBannedForOnlineId(peer.get(), online_id); // Will be disconnected if banned by online id if (peer->isDisconnected()) return; unsigned total_players = 0; STKHost::get()->updatePlayers(NULL, NULL, &total_players); if (total_players + player_count + m_ai_profiles.size() > (unsigned)ServerConfig::m_server_max_players) { NetworkString *message = getNetworkString(2); message->setSynchronous(true); message->addUInt8(LE_CONNECTION_REFUSED).addUInt8(RR_TOO_MANY_PLAYERS); peer->sendPacket(message, true/*reliable*/, false/*encrypted*/); peer->reset(); delete message; Log::verbose("ServerLobby", "Player refused: too many players"); return; } // Reject non-valiated player joinning if WAN server and not disabled // encforement of validation, unless it's player from localhost or lan // And no duplicated online id or split screen players in ranked server // AIPeer only from lan and only 1 if ai handling std::set all_online_ids = STKHost::get()->getAllPlayerOnlineIds(); bool duplicated_ranked_player = all_online_ids.find(online_id) != all_online_ids.end(); if (((encrypted_size == 0 || online_id == 0) && !(peer->getAddress().isPublicAddressLocalhost() || peer->getAddress().isLAN()) && NetworkConfig::get()->isWAN() && ServerConfig::m_validating_player) || (ServerConfig::m_strict_players && (player_count != 1 || online_id == 0 || duplicated_ranked_player)) || (peer->isAIPeer() && !peer->getAddress().isLAN() &&!ServerConfig::m_ai_anywhere) || (peer->isAIPeer() && ServerConfig::m_ai_handling && !m_ai_peer.expired())) { NetworkString* message = getNetworkString(2); message->setSynchronous(true); message->addUInt8(LE_CONNECTION_REFUSED).addUInt8(RR_INVALID_PLAYER); peer->sendPacket(message, true/*reliable*/, false/*encrypted*/); peer->reset(); delete message; Log::verbose("ServerLobby", "Player refused: invalid player"); return; } if (ServerConfig::m_ai_handling && peer->isAIPeer()) m_ai_peer = peer; if (encrypted_size != 0) { m_pending_connection[peer] = std::make_pair(online_id, BareNetworkString(data.getCurrentData(), encrypted_size)); } else { core::stringw online_name; if (online_id > 0) data.decodeStringW(&online_name); handleUnencryptedConnection(peer, data, online_id, online_name, false/*is_pending_connection*/); } } // connectionRequested //----------------------------------------------------------------------------- void ServerLobby::handleUnencryptedConnection(std::shared_ptr peer, BareNetworkString& data, uint32_t online_id, const core::stringw& online_name, bool is_pending_connection, std::string country_code) { if (data.size() < 2) return; // Check for password std::string password; data.decodeString(&password); const std::string& server_pw = ServerConfig::m_private_server_password; if (password != server_pw) { NetworkString *message = getNetworkString(2); message->setSynchronous(true); message->addUInt8(LE_CONNECTION_REFUSED) .addUInt8(RR_INCORRECT_PASSWORD); peer->sendPacket(message, true/*reliable*/, false/*encrypted*/); peer->reset(); delete message; Log::verbose("ServerLobby", "Player refused: incorrect password"); return; } // Check again max players and duplicated player in ranked server, // if this is a pending connection unsigned total_players = 0; unsigned player_count = data.getUInt8(); if (is_pending_connection) { STKHost::get()->updatePlayers(NULL, NULL, &total_players); if (total_players + player_count > (unsigned)ServerConfig::m_server_max_players) { NetworkString *message = getNetworkString(2); message->setSynchronous(true); message->addUInt8(LE_CONNECTION_REFUSED) .addUInt8(RR_TOO_MANY_PLAYERS); peer->sendPacket(message, true/*reliable*/, false/*encrypted*/); peer->reset(); delete message; Log::verbose("ServerLobby", "Player refused: too many players"); return; } std::set all_online_ids = STKHost::get()->getAllPlayerOnlineIds(); bool duplicated_ranked_player = all_online_ids.find(online_id) != all_online_ids.end(); if (ServerConfig::m_ranked && duplicated_ranked_player) { NetworkString* message = getNetworkString(2); message->setSynchronous(true); message->addUInt8(LE_CONNECTION_REFUSED) .addUInt8(RR_INVALID_PLAYER); peer->sendPacket(message, true/*reliable*/, false/*encrypted*/); peer->reset(); delete message; Log::verbose("ServerLobby", "Player refused: invalid player"); return; } } #ifdef ENABLE_SQLITE3 if (country_code.empty() && !peer->getAddress().isIPv6()) country_code = ip2Country(peer->getAddress()); if (country_code.empty() && peer->getAddress().isIPv6()) country_code = ipv62Country(peer->getAddress()); #endif auto red_blue = STKHost::get()->getAllPlayersTeamInfo(); for (unsigned i = 0; i < player_count; i++) { core::stringw name; data.decodeStringW(&name); // 30 to make it consistent with stk-addons max user name length if (name.empty()) name = L"unnamed"; else if (name.size() > 30) name = name.subString(0, 30); float default_kart_color = data.getFloat(); HandicapLevel handicap = (HandicapLevel)data.getUInt8(); auto player = std::make_shared (peer, i == 0 && !online_name.empty() && !peer->isAIPeer() ? online_name : name, peer->getHostId(), default_kart_color, i == 0 ? online_id : 0, handicap, (uint8_t)i, KART_TEAM_NONE, country_code); if (ServerConfig::m_team_choosing) { KartTeam cur_team = KART_TEAM_NONE; if (red_blue.first > red_blue.second) { cur_team = KART_TEAM_BLUE; red_blue.second++; } else { cur_team = KART_TEAM_RED; red_blue.first++; } player->setTeam(cur_team); } peer->addPlayer(player); } peer->setValidated(true); // send a message to the one that asked to connect NetworkString* server_info = getNetworkString(); server_info->setSynchronous(true); server_info->addUInt8(LE_SERVER_INFO); m_game_setup->addServerInfo(server_info); peer->sendPacket(server_info); delete server_info; const bool game_started = m_state.load() != WAITING_FOR_START_GAME; NetworkString* message_ack = getNetworkString(4); message_ack->setSynchronous(true); // connection success -- return the host id of peer float auto_start_timer = 0.0f; if (m_timeout.load() == std::numeric_limits::max()) auto_start_timer = std::numeric_limits::max(); else { auto_start_timer = (m_timeout.load() - (int64_t)StkTime::getMonoTimeMs()) / 1000.0f; } message_ack->addUInt8(LE_CONNECTION_ACCEPTED).addUInt32(peer->getHostId()) .addUInt32(ServerConfig::m_server_version); message_ack->addUInt16( (uint16_t)stk_config->m_network_capabilities.size()); for (const std::string& cap : stk_config->m_network_capabilities) message_ack->encodeString(cap); message_ack->addFloat(auto_start_timer) .addUInt32(ServerConfig::m_state_frequency) .addUInt8(ServerConfig::m_chat ? 1 : 0) .addUInt8(m_player_reports_table_exists ? 1 : 0); peer->setSpectator(false); // The 127.* or ::1/128 will be in charged for controlling AI if (m_ai_profiles.empty() && peer->getAddress().isLoopback()) { unsigned ai_add = NetworkConfig::get()->getNumFixedAI(); unsigned max_players = ServerConfig::m_server_max_players; // We need to reserve at least 1 slot for new player if (player_count + ai_add + 1 > max_players) ai_add = max_players - player_count - 1; for (unsigned i = 0; i < ai_add; i++) { #ifdef SERVER_ONLY core::stringw name = L"Bot"; #else core::stringw name = _("Bot"); #endif if (i > 0) name += core::stringw(" ") + StringUtils::toWString(i); m_ai_profiles.push_back(std::make_shared (peer, name, peer->getHostId(), 0.0f, 0, HANDICAP_NONE, player_count + i, KART_TEAM_NONE, "")); } } if (game_started) { peer->setWaitingForGame(true); updatePlayerList(); peer->sendPacket(message_ack); delete message_ack; } else { peer->setWaitingForGame(false); m_peers_ready[peer] = false; if (!ServerConfig::m_sql_management) { for (std::shared_ptr& npp : peer->getPlayerProfiles()) { Log::info("ServerLobby", "New player %s with online id %u from %s with %s.", StringUtils::wideToUtf8(npp->getName()).c_str(), npp->getOnlineId(), peer->getAddress().toString().c_str(), peer->getUserVersion().c_str()); } } updatePlayerList(); peer->sendPacket(message_ack); delete message_ack; if (ServerConfig::m_ranked) { getRankingForPlayer(peer->getPlayerProfiles()[0]); } } #ifdef ENABLE_SQLITE3 if (m_server_stats_table.empty() || peer->isAIPeer()) return; std::string query; if (ServerConfig::m_ipv6_connection && peer->getAddress().isIPv6()) { query = StringUtils::insertValues( "INSERT INTO %s " "(host_id, ip, ipv6 ,port, online_id, username, player_num, " "country_code, version, os, ping) " "VALUES (%u, 0, \"%s\" ,%u, %u, ?, %u, ?, ?, ?, %u);", m_server_stats_table.c_str(), peer->getHostId(), peer->getAddress().toString(false), peer->getAddress().getPort(), online_id, player_count, peer->getAveragePing()); } else { query = StringUtils::insertValues( "INSERT INTO %s " "(host_id, ip, port, online_id, username, player_num, " "country_code, version, os, ping) " "VALUES (%u, %u, %u, %u, ?, %u, ?, ?, ?, %u);", m_server_stats_table.c_str(), peer->getHostId(), peer->getAddress().getIP(), peer->getAddress().getPort(), online_id, player_count, peer->getAveragePing()); } easySQLQuery(query, [peer, country_code](sqlite3_stmt* stmt) { if (sqlite3_bind_text(stmt, 1, StringUtils::wideToUtf8( peer->getPlayerProfiles()[0]->getName()).c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK) { Log::error("easySQLQuery", "Failed to bind %s.", StringUtils::wideToUtf8( peer->getPlayerProfiles()[0]->getName()).c_str()); } if (country_code.empty()) { if (sqlite3_bind_null(stmt, 2) != SQLITE_OK) { Log::error("easySQLQuery", "Failed to bind NULL for country code."); } } else { if (sqlite3_bind_text(stmt, 2, country_code.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK) { Log::error("easySQLQuery", "Failed to bind country: %s.", country_code.c_str()); } } auto version_os = StringUtils::extractVersionOS(peer->getUserVersion()); if (sqlite3_bind_text(stmt, 3, version_os.first.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK) { Log::error("easySQLQuery", "Failed to bind %s.", version_os.first.c_str()); } if (sqlite3_bind_text(stmt, 4, version_os.second.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK) { Log::error("easySQLQuery", "Failed to bind %s.", version_os.second.c_str()); } } ); #endif } // handleUnencryptedConnection //----------------------------------------------------------------------------- /** Called when any players change their setting (team for example), or * connection / disconnection, it will use the game_started parameter to * determine if this should be send to all peers in server or just in game. * \param update_when_reset_server If true, this message will be sent to * all peers. */ void ServerLobby::updatePlayerList(bool update_when_reset_server) { const bool game_started = m_state.load() != WAITING_FOR_START_GAME && !update_when_reset_server; auto all_profiles = STKHost::get()->getAllPlayerProfiles(); size_t all_profiles_size = all_profiles.size(); for (auto& profile : all_profiles) { if (profile->getPeer()->alwaysSpectate()) all_profiles_size--; } auto spectators_by_limit = getSpectatorsByLimit(); // N - 1 AI auto ai_instance = m_ai_peer.lock(); if (supportsAI()) { if (ai_instance) { auto ai_profiles = ai_instance->getPlayerProfiles(); if (m_state.load() == WAITING_FOR_START_GAME || update_when_reset_server) { if (all_profiles_size > ai_profiles.size()) ai_profiles.clear(); else if (all_profiles_size != 0) { ai_profiles.resize( ai_profiles.size() - all_profiles_size + 1); } } else { // Use fixed number of AI calculated when started game ai_profiles.resize(m_ai_count); } all_profiles.insert(all_profiles.end(), ai_profiles.begin(), ai_profiles.end()); } else if (!m_ai_profiles.empty()) { all_profiles.insert(all_profiles.end(), m_ai_profiles.begin(), m_ai_profiles.end()); } } m_lobby_players.store((int)all_profiles.size()); // No need to update player list (for started grand prix currently) if (!allowJoinedPlayersWaiting() && m_state.load() > WAITING_FOR_START_GAME && !update_when_reset_server) return; NetworkString* pl = getNetworkString(); pl->setSynchronous(true); pl->addUInt8(LE_UPDATE_PLAYER_LIST) .addUInt8((uint8_t)(game_started ? 1 : 0)) .addUInt8((uint8_t)all_profiles.size()); for (auto profile : all_profiles) { auto profile_name = profile->getName(); // get OS information auto version_os = StringUtils::extractVersionOS(profile->getPeer()->getUserVersion()); std::string os_type_str = version_os.second; // if mobile OS if (os_type_str == "iOS" || os_type_str == "Android") // Add a Mobile emoji for mobile OS profile_name = StringUtils::utf32ToWide({ 0x1F4F1 }) + profile_name; // Add an hourglass emoji for players waiting because of the player limit if (spectators_by_limit.find(profile->getPeer()) != spectators_by_limit.end()) profile_name = StringUtils::utf32ToWide({ 0x231B }) + profile_name; pl->addUInt32(profile->getHostId()).addUInt32(profile->getOnlineId()) .addUInt8(profile->getLocalPlayerId()) .encodeString(profile_name); std::shared_ptr p = profile->getPeer(); uint8_t boolean_combine = 0; if (p && p->isWaitingForGame()) boolean_combine |= 1; if (p && (p->isSpectator() || ((m_state.load() == WAITING_FOR_START_GAME || update_when_reset_server) && p->alwaysSpectate()))) boolean_combine |= (1 << 1); if (p && m_server_owner_id.load() == p->getHostId()) boolean_combine |= (1 << 2); if (ServerConfig::m_owner_less && !game_started && m_peers_ready.find(p) != m_peers_ready.end() && m_peers_ready.at(p)) boolean_combine |= (1 << 3); if ((p && p->isAIPeer()) || isAIProfile(profile)) boolean_combine |= (1 << 4); pl->addUInt8(boolean_combine); pl->addUInt8(profile->getHandicap()); if (ServerConfig::m_team_choosing && RaceManager::get()->teamEnabled()) pl->addUInt8(profile->getTeam()); else pl->addUInt8(KART_TEAM_NONE); pl->encodeString(profile->getCountryCode()); } // Don't send this message to in-game players STKHost::get()->sendPacketToAllPeersWith([game_started] (STKPeer* p) { if (!p->isValidated()) return false; if (!p->isWaitingForGame() && game_started) return false; return true; }, pl); delete pl; } // updatePlayerList //----------------------------------------------------------------------------- void ServerLobby::updateServerOwner() { if (m_state.load() < WAITING_FOR_START_GAME || m_state.load() > RESULT_DISPLAY || ServerConfig::m_owner_less) return; if (!m_server_owner.expired()) return; auto peers = STKHost::get()->getPeers(); if (peers.empty()) return; std::sort(peers.begin(), peers.end(), [](const std::shared_ptr a, const std::shared_ptr b)->bool { return a->getHostId() < b->getHostId(); }); std::shared_ptr owner; for (auto peer: peers) { // Only matching host id can be server owner in case of // graphics-client-server if (peer->isValidated() && !peer->isAIPeer() && (m_process_type == PT_MAIN || peer->getHostId() == m_client_server_host_id.load())) { owner = peer; break; } } if (owner) { NetworkString* ns = getNetworkString(); ns->setSynchronous(true); ns->addUInt8(LE_SERVER_OWNERSHIP); owner->sendPacket(ns); delete ns; m_server_owner = owner; m_server_owner_id.store(owner->getHostId()); updatePlayerList(); } } // updateServerOwner //----------------------------------------------------------------------------- /*! \brief Called when a player asks to select karts. * \param event : Event providing the information. */ void ServerLobby::kartSelectionRequested(Event* event) { if (m_state != SELECTING || m_game_setup->isGrandPrixStarted()) { Log::warn("ServerLobby", "Received kart selection while in state %d.", m_state.load()); return; } if (!checkDataSize(event, 1) || event->getPeer()->getPlayerProfiles().empty()) return; const NetworkString& data = event->data(); STKPeer* peer = event->getPeer(); setPlayerKarts(data, peer); } // kartSelectionRequested //----------------------------------------------------------------------------- /*! \brief Called when a player votes for track(s), it will auto correct client * data if it sends some invalid data. * \param event : Event providing the information. */ void ServerLobby::handlePlayerVote(Event* event) { if (m_state != SELECTING || !ServerConfig::m_track_voting) { Log::warn("ServerLobby", "Received track vote while in state %d.", m_state.load()); return; } if (!checkDataSize(event, 4) || event->getPeer()->getPlayerProfiles().empty() || event->getPeer()->isWaitingForGame()) return; if (isVotingOver()) return; NetworkString& data = event->data(); PeerVote vote(data); Log::debug("ServerLobby", "Vote from client: host %d, track %s, laps %d, reverse %d.", event->getPeer()->getHostId(), vote.m_track_name.c_str(), vote.m_num_laps, vote.m_reverse); Track* t = track_manager->getTrack(vote.m_track_name); if (!t) { vote.m_track_name = *m_available_kts.second.begin(); t = track_manager->getTrack(vote.m_track_name); assert(t); } // Remove / adjust any invalid settings if (RaceManager::get()->modeHasLaps()) { if (ServerConfig::m_auto_game_time_ratio > 0.0f) { vote.m_num_laps = (uint8_t)(fmaxf(1.0f, (float)t->getDefaultNumberOfLaps() * ServerConfig::m_auto_game_time_ratio)); } else if (vote.m_num_laps == 0 || vote.m_num_laps > 20) vote.m_num_laps = (uint8_t)3; if (!t->reverseAvailable() && vote.m_reverse) vote.m_reverse = false; } else if (RaceManager::get()->isSoccerMode()) { if (m_game_setup->isSoccerGoalTarget()) { if (ServerConfig::m_auto_game_time_ratio > 0.0f) { vote.m_num_laps = (uint8_t)(ServerConfig::m_auto_game_time_ratio * UserConfigParams::m_num_goals); } else if (vote.m_num_laps > 10) vote.m_num_laps = (uint8_t)5; } else { if (ServerConfig::m_auto_game_time_ratio > 0.0f) { vote.m_num_laps = (uint8_t)(ServerConfig::m_auto_game_time_ratio * UserConfigParams::m_soccer_time_limit); } else if (vote.m_num_laps > 15) vote.m_num_laps = (uint8_t)7; } } else if (RaceManager::get()->getMinorMode() == RaceManager::MINOR_MODE_FREE_FOR_ALL) { vote.m_num_laps = 0; } else if (RaceManager::get()->getMinorMode() == RaceManager::MINOR_MODE_CAPTURE_THE_FLAG) { vote.m_num_laps = 0; vote.m_reverse = false; } // Store vote: vote.m_player_name = event->getPeer()->getPlayerProfiles()[0]->getName(); addVote(event->getPeer()->getHostId(), vote); // Now inform all clients about the vote NetworkString other = NetworkString(PROTOCOL_LOBBY_ROOM); other.setSynchronous(true); other.addUInt8(LE_VOTE); other.addUInt32(event->getPeer()->getHostId()); vote.encode(&other); sendMessageToPeers(&other); } // handlePlayerVote // ---------------------------------------------------------------------------- /** Select the track to be used based on all votes being received. * \param winner_vote The PeerVote that was picked. * \param winner_peer_id The host id of winner (unchanged if no vote). * \return True if race can go on, otherwise wait. */ bool ServerLobby::handleAllVotes(PeerVote* winner_vote, uint32_t* winner_peer_id) { // Determine majority agreement when 35% of voting time remains, // reserve some time for kart selection so it's not 50% if (getRemainingVotingTime() / getMaxVotingTime() > 0.35f) { return false; } // First remove all votes from disconnected hosts auto it = m_peers_votes.begin(); while (it != m_peers_votes.end()) { auto peer = STKHost::get()->findPeerByHostId(it->first); if (peer == nullptr) { it = m_peers_votes.erase(it); } else it++; } if (m_peers_votes.empty()) { if (isVotingOver()) { *winner_vote = *m_default_vote; return true; } return false; } // Count number of players float cur_players = 0.0f; auto peers = STKHost::get()->getPeers(); for (auto peer : peers) { if (peer->isAIPeer()) continue; if (peer->hasPlayerProfiles() && !peer->isWaitingForGame()) cur_players += 1.0f; } std::string top_track = m_default_vote->m_track_name; int top_laps = m_default_vote->m_num_laps; bool top_reverse = m_default_vote->m_reverse; std::map tracks; std::map laps; std::map reverses; // Ratio to determine majority agreement float tracks_rate = 0.0f; float laps_rate = 0.0f; float reverses_rate = 0.0f; RandomGenerator rg; for (auto& p : m_peers_votes) { auto track_vote = tracks.find(p.second.m_track_name); if (track_vote == tracks.end()) tracks[p.second.m_track_name] = 1; else track_vote->second++; auto lap_vote = laps.find(p.second.m_num_laps); if (lap_vote == laps.end()) laps[p.second.m_num_laps] = 1; else lap_vote->second++; auto reverse_vote = reverses.find(p.second.m_reverse); if (reverse_vote == reverses.end()) reverses[p.second.m_reverse] = 1; else reverse_vote->second++; } unsigned vote = 0; auto track_vote = tracks.begin(); // rg.get(2) == 0 will allow not always the "less" in map get picked for (auto c_vote = tracks.begin(); c_vote != tracks.end(); c_vote++) { if (c_vote->second > vote || (c_vote->second >= vote && rg.get(2) == 0)) { vote = c_vote->second; track_vote = c_vote; } } if (track_vote != tracks.end()) { top_track = track_vote->first; tracks_rate = float(track_vote->second) / cur_players; } vote = 0; auto lap_vote = laps.begin(); for (auto c_vote = laps.begin(); c_vote != laps.end(); c_vote++) { if (c_vote->second > vote || (c_vote->second >= vote && rg.get(2) == 0)) { vote = c_vote->second; lap_vote = c_vote; } } if (lap_vote != laps.end()) { top_laps = lap_vote->first; laps_rate = float(lap_vote->second) / cur_players; } vote = 0; auto reverse_vote = reverses.begin(); for (auto c_vote = reverses.begin(); c_vote != reverses.end(); c_vote++) { if (c_vote->second > vote || (c_vote->second >= vote && rg.get(2) == 0)) { vote = c_vote->second; reverse_vote = c_vote; } } if (reverse_vote != reverses.end()) { top_reverse = reverse_vote->first; reverses_rate = float(reverse_vote->second) / cur_players; } // End early if there is majority agreement which is all entries rate > 0.5 it = m_peers_votes.begin(); if (tracks_rate > 0.5f && laps_rate > 0.5f && reverses_rate > 0.5f) { while (it != m_peers_votes.end()) { if (it->second.m_track_name == top_track && it->second.m_num_laps == top_laps && it->second.m_reverse == top_reverse) break; else it++; } if (it == m_peers_votes.end()) { Log::warn("ServerLobby", "Missing track %s from majority.", top_track.c_str()); it = m_peers_votes.begin(); } *winner_peer_id = it->first; *winner_vote = it->second; return true; } else if (isVotingOver()) { // Pick the best lap (or soccer goal / time) from only the top track // if no majority agreement from all int diff = std::numeric_limits::max(); auto closest_lap = m_peers_votes.begin(); while (it != m_peers_votes.end()) { if (it->second.m_track_name == top_track && std::abs((int)it->second.m_num_laps - top_laps) < diff) { closest_lap = it; diff = std::abs((int)it->second.m_num_laps - top_laps); } else it++; } *winner_peer_id = closest_lap->first; *winner_vote = closest_lap->second; return true; } return false; } // handleAllVotes // ---------------------------------------------------------------------------- void ServerLobby::getHitCaptureLimit() { int hit_capture_limit = std::numeric_limits::max(); float time_limit = 0.0f; if (RaceManager::get()->getMinorMode() == RaceManager::MINOR_MODE_CAPTURE_THE_FLAG) { if (ServerConfig::m_capture_limit > 0) hit_capture_limit = ServerConfig::m_capture_limit; if (ServerConfig::m_time_limit_ctf > 0) time_limit = (float)ServerConfig::m_time_limit_ctf; } else { if (ServerConfig::m_hit_limit > 0) hit_capture_limit = ServerConfig::m_hit_limit; if (ServerConfig::m_time_limit_ffa > 0.0f) time_limit = (float)ServerConfig::m_time_limit_ffa; } m_battle_hit_capture_limit = hit_capture_limit; m_battle_time_limit = time_limit; } // getHitCaptureLimit // ---------------------------------------------------------------------------- /** Called from the RaceManager of the server when the world is loaded. Marks * the server to be ready to start the race. */ void ServerLobby::finishedLoadingWorld() { for (auto p : m_peers_ready) { if (auto peer = p.first.lock()) peer->updateLastActivity(); } m_server_has_loaded_world.store(true); } // finishedLoadingWorld; //----------------------------------------------------------------------------- /** Called when a client notifies the server that it has loaded the world. * When all clients and the server are ready, the race can be started. */ void ServerLobby::finishedLoadingWorldClient(Event *event) { std::shared_ptr peer = event->getPeerSP(); peer->updateLastActivity(); m_peers_ready.at(peer) = true; Log::info("ServerLobby", "Peer %d has finished loading world at %lf", peer->getHostId(), StkTime::getRealTime()); } // finishedLoadingWorldClient //----------------------------------------------------------------------------- /** Called when a client clicks on 'ok' on the race result screen. * If all players have clicked on 'ok', go back to the lobby. */ void ServerLobby::playerFinishedResult(Event *event) { if (m_rs_state.load() == RS_ASYNC_RESET || m_state.load() != RESULT_DISPLAY) return; std::shared_ptr peer = event->getPeerSP(); m_peers_ready.at(peer) = true; } // playerFinishedResult //----------------------------------------------------------------------------- bool ServerLobby::waitingForPlayers() const { if (m_game_setup->isGrandPrix() && m_game_setup->isGrandPrixStarted()) return false; return m_state.load() >= WAITING_FOR_START_GAME; } // waitingForPlayers //----------------------------------------------------------------------------- void ServerLobby::handlePendingConnection() { std::lock_guard lock(m_keys_mutex); for (auto it = m_pending_connection.begin(); it != m_pending_connection.end();) { auto peer = it->first.lock(); if (!peer) { it = m_pending_connection.erase(it); } else { const uint32_t online_id = it->second.first; auto key = m_keys.find(online_id); if (key != m_keys.end() && key->second.m_tried == false) { try { if (decryptConnectionRequest(peer, it->second.second, key->second.m_aes_key, key->second.m_aes_iv, online_id, key->second.m_name, key->second.m_country_code)) { it = m_pending_connection.erase(it); m_keys.erase(online_id); continue; } else key->second.m_tried = true; } catch (std::exception& e) { Log::error("ServerLobby", "handlePendingConnection error: %s", e.what()); key->second.m_tried = true; } } it++; } } } // handlePendingConnection //----------------------------------------------------------------------------- bool ServerLobby::decryptConnectionRequest(std::shared_ptr peer, BareNetworkString& data, const std::string& key, const std::string& iv, uint32_t online_id, const core::stringw& online_name, const std::string& country_code) { auto crypto = std::unique_ptr(new Crypto( Crypto::decode64(key), Crypto::decode64(iv))); if (crypto->decryptConnectionRequest(data)) { peer->setCrypto(std::move(crypto)); Log::info("ServerLobby", "%s validated", StringUtils::wideToUtf8(online_name).c_str()); handleUnencryptedConnection(peer, data, online_id, online_name, true/*is_pending_connection*/, country_code); return true; } return false; } // decryptConnectionRequest //----------------------------------------------------------------------------- void ServerLobby::getRankingForPlayer(std::shared_ptr p) { int priority = Online::RequestManager::HTTP_MAX_PRIORITY; auto request = std::make_shared(priority); NetworkConfig::get()->setUserDetails(request, "get-ranking"); const uint32_t id = p->getOnlineId(); request->addParameter("id", id); request->executeNow(); const XMLNode* result = request->getXMLData(); std::string rec_success; // Default result double raw_score = BASE_RANKING_POINTS; double score = BASE_RANKING_POINTS - 3*BASE_RATING_DEVIATION + 3*MIN_RATING_DEVIATION; double max_score = BASE_RANKING_POINTS - 3*BASE_RATING_DEVIATION + 3*MIN_RATING_DEVIATION; unsigned num_races = 0; double rating_deviation = BASE_RATING_DEVIATION; uint64_t disconnects = 0; if (result->get("success", &rec_success)) { if (rec_success == "yes") { result->get("scores", &score); result->get("max-scores", &max_score); result->get("num-races-done", &num_races); result->get("raw-scores", &raw_score); result->get("rating-deviation", &rating_deviation); result->get("disconnects", &disconnects); } else { Log::error("ServerLobby", "No ranking info found for player %s.", StringUtils::wideToUtf8(p->getName()).c_str()); // Kick the player to avoid his score being reset in case // connection to stk addons is broken auto peer = p->getPeer(); if (peer) { peer->kick(); return; } } } else { Log::error("ServerLobby", "No ranking info found for player %s.", StringUtils::wideToUtf8(p->getName()).c_str()); auto peer = p->getPeer(); if (peer) { peer->kick(); return; } } m_ranked_players[id] = p; m_scores[id] = score; m_max_scores[id] = max_score; m_num_ranked_races[id] = num_races; m_raw_scores[id] = raw_score; m_rating_deviations[id] = rating_deviation; m_num_ranked_disconnects[id] = disconnects; } // getRankingForPlayer //----------------------------------------------------------------------------- void ServerLobby::submitRankingsToAddons() { // No ranking for battle mode if (!RaceManager::get()->modeHasLaps()) return; for (unsigned i = 0; i < RaceManager::get()->getNumPlayers(); i++) { const uint32_t id = RaceManager::get()->getKartInfo(i).getOnlineId(); auto request = std::make_shared (id, m_scores.at(id), m_max_scores.at(id), m_num_ranked_races.at(id), m_raw_scores.at(id), m_rating_deviations.at(id), m_num_ranked_disconnects.at(id), RaceManager::get()->getKartInfo(i).getCountryCode()); NetworkConfig::get()->setUserDetails(request, "submit-ranking"); Log::info("ServerLobby", "Submiting ranking for %s (%d) : %lf, %lf %d", StringUtils::wideToUtf8( RaceManager::get()->getKartInfo(i).getPlayerName()).c_str(), id, m_scores.at(id), m_max_scores.at(id), m_num_ranked_races.at(id)); request->queue(); } } // submitRankingsToAddons //----------------------------------------------------------------------------- /** This function is called when all clients have loaded the world and * are therefore ready to start the race. It determine the start time in * network timer for client and server based on pings and then switches state * to WAIT_FOR_RACE_STARTED. */ void ServerLobby::configPeersStartTime() { uint32_t max_ping = 0; const unsigned max_ping_from_peers = ServerConfig::m_max_ping; bool peer_exceeded_max_ping = false; for (auto p : m_peers_ready) { auto peer = p.first.lock(); // Spectators don't send input so we don't need to delay for them if (!peer || peer->alwaysSpectate()) continue; if (peer->getAveragePing() > max_ping_from_peers) { Log::warn("ServerLobby", "Peer %s cannot catch up with max ping %d.", peer->getAddress().toString().c_str(), max_ping); peer_exceeded_max_ping = true; continue; } max_ping = std::max(peer->getAveragePing(), max_ping); } if ((ServerConfig::m_high_ping_workaround && peer_exceeded_max_ping) || (ServerConfig::m_live_players && RaceManager::get()->supportsLiveJoining())) { Log::info("ServerLobby", "Max ping to ServerConfig::m_max_ping for " "live joining or high ping workaround."); max_ping = ServerConfig::m_max_ping; } // Start up time will be after 2500ms, so even if this packet is sent late // (due to packet loss), the start time will still ahead of current time uint64_t start_time = STKHost::get()->getNetworkTimer() + (uint64_t)2500; powerup_manager->setRandomSeed(start_time); NetworkString* ns = getNetworkString(10); ns->setSynchronous(true); ns->addUInt8(LE_START_RACE).addUInt64(start_time); const uint8_t cc = (uint8_t)Track::getCurrentTrack()->getCheckManager()->getCheckStructureCount(); ns->addUInt8(cc); *ns += *m_items_complete_state; m_client_starting_time = start_time; sendMessageToPeers(ns, /*reliable*/true); const unsigned jitter_tolerance = ServerConfig::m_jitter_tolerance; Log::info("ServerLobby", "Max ping from peers: %d, jitter tolerance: %d", max_ping, jitter_tolerance); // Delay server for max ping / 2 from peers and jitter tolerance. m_server_delay = (uint64_t)(max_ping / 2) + (uint64_t)jitter_tolerance; start_time += m_server_delay; m_server_started_at = start_time; delete ns; m_state = WAIT_FOR_RACE_STARTED; World::getWorld()->setPhase(WorldStatus::SERVER_READY_PHASE); // 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]() { const uint64_t cur_time = stk_host->getNetworkTimer(); assert(start_time > cur_time); int sleep_time = (int)(start_time - cur_time); //Log::info("ServerLobby", "Start game after %dms", sleep_time); StkTime::sleep(sleep_time); //Log::info("ServerLobby", "Started at %lf", StkTime::getRealTime()); m_state.store(RACING); }); } // configPeersStartTime //----------------------------------------------------------------------------- bool ServerLobby::allowJoinedPlayersWaiting() const { return !m_game_setup->isGrandPrix(); } // allowJoinedPlayersWaiting //----------------------------------------------------------------------------- void ServerLobby::addWaitingPlayersToGame() { auto all_profiles = STKHost::get()->getAllPlayerProfiles(); for (auto& profile : all_profiles) { auto peer = profile->getPeer(); if (!peer || !peer->isValidated()) continue; peer->resetAlwaysSpectateFull(); peer->setWaitingForGame(false); peer->setSpectator(false); if (m_peers_ready.find(peer) == m_peers_ready.end()) { m_peers_ready[peer] = false; if (!ServerConfig::m_sql_management) { Log::info("ServerLobby", "New player %s with online id %u from %s with %s.", StringUtils::wideToUtf8(profile->getName()).c_str(), profile->getOnlineId(), peer->getAddress().toString().c_str(), peer->getUserVersion().c_str()); } } uint32_t online_id = profile->getOnlineId(); if (ServerConfig::m_ranked && (m_ranked_players.find(online_id) == m_ranked_players.end() || (m_ranked_players.find(online_id) != m_ranked_players.end() && m_ranked_players.at(online_id).expired()))) { getRankingForPlayer(peer->getPlayerProfiles()[0]); } } // Re-activiate the ai if (auto ai = m_ai_peer.lock()) ai->setValidated(true); } // addWaitingPlayersToGame //----------------------------------------------------------------------------- void ServerLobby::resetServer() { addWaitingPlayersToGame(); resetPeersReady(); updatePlayerList(true/*update_when_reset_server*/); NetworkString* server_info = getNetworkString(); server_info->setSynchronous(true); server_info->addUInt8(LE_SERVER_INFO); m_game_setup->addServerInfo(server_info); sendMessageToPeersInServer(server_info); delete server_info; setup(); m_state = NetworkConfig::get()->isLAN() ? WAITING_FOR_START_GAME : REGISTER_SELF_ADDRESS; updatePlayerList(); } // resetServer //----------------------------------------------------------------------------- void ServerLobby::testBannedForIP(STKPeer* peer) const { #ifdef ENABLE_SQLITE3 if (!m_db || !m_ip_ban_table_exists) return; // Test for IPv4 if (peer->getAddress().isIPv6()) return; int row_id = -1; unsigned ip_start = 0; unsigned ip_end = 0; std::string query = StringUtils::insertValues( "SELECT rowid, ip_start, ip_end, reason, description FROM %s " "WHERE ip_start <= %u AND ip_end >= %u " "AND datetime('now') > datetime(starting_time) AND " "(expired_days is NULL OR datetime" "(starting_time, '+'||expired_days||' days') > datetime('now')) " "LIMIT 1;", ServerConfig::m_ip_ban_table.c_str(), peer->getAddress().getIP(), peer->getAddress().getIP()); sqlite3_stmt* stmt = NULL; int ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0); if (ret == SQLITE_OK) { ret = sqlite3_step(stmt); if (ret == SQLITE_ROW) { row_id = sqlite3_column_int(stmt, 0); ip_start = (unsigned)sqlite3_column_int64(stmt, 1); ip_end = (unsigned)sqlite3_column_int64(stmt, 2); const char* reason = (char*)sqlite3_column_text(stmt, 3); const char* desc = (char*)sqlite3_column_text(stmt, 4); Log::info("ServerLobby", "%s banned by IP: %s " "(rowid: %d, description: %s).", peer->getAddress().toString().c_str(), reason, row_id, desc); kickPlayerWithReason(peer, reason); } ret = sqlite3_finalize(stmt); if (ret != SQLITE_OK) { Log::error("ServerLobby", "Error finalize database for query %s: %s", query.c_str(), sqlite3_errmsg(m_db)); } } else { Log::error("ServerLobby", "Error preparing database for query %s: %s", query.c_str(), sqlite3_errmsg(m_db)); return; } if (row_id != -1) { query = StringUtils::insertValues( "UPDATE %s SET trigger_count = trigger_count + 1, " "last_trigger = datetime('now') " "WHERE ip_start = %u AND ip_end = %u;", ServerConfig::m_ip_ban_table.c_str(), ip_start, ip_end); easySQLQuery(query); } #endif } // testBannedForIP //----------------------------------------------------------------------------- void ServerLobby::testBannedForIPv6(STKPeer* peer) const { #ifdef ENABLE_SQLITE3 if (!m_db || !m_ipv6_ban_table_exists) return; // Test for IPv6 if (!peer->getAddress().isIPv6()) return; int row_id = -1; std::string ipv6_cidr; std::string query = StringUtils::insertValues( "SELECT rowid, ipv6_cidr, reason, description FROM %s " "WHERE insideIPv6CIDR(ipv6_cidr, ?) = 1 " "AND datetime('now') > datetime(starting_time) AND " "(expired_days is NULL OR datetime" "(starting_time, '+'||expired_days||' days') > datetime('now')) " "LIMIT 1;", ServerConfig::m_ipv6_ban_table.c_str()); sqlite3_stmt* stmt = NULL; int ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0); if (ret == SQLITE_OK) { if (sqlite3_bind_text(stmt, 1, peer->getAddress().toString(false).c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK) { Log::error("ServerLobby", "Error binding ipv6 addr for query: %s", sqlite3_errmsg(m_db)); } ret = sqlite3_step(stmt); if (ret == SQLITE_ROW) { row_id = sqlite3_column_int(stmt, 0); ipv6_cidr = (char*)sqlite3_column_text(stmt, 1); const char* reason = (char*)sqlite3_column_text(stmt, 2); const char* desc = (char*)sqlite3_column_text(stmt, 3); Log::info("ServerLobby", "%s banned by IP: %s " "(rowid: %d, description: %s).", peer->getAddress().toString().c_str(), reason, row_id, desc); kickPlayerWithReason(peer, reason); } ret = sqlite3_finalize(stmt); if (ret != SQLITE_OK) { Log::error("ServerLobby", "Error finalize database for query %s: %s", query.c_str(), sqlite3_errmsg(m_db)); } } else { Log::error("ServerLobby", "Error preparing database for query %s: %s", query.c_str(), sqlite3_errmsg(m_db)); return; } if (row_id != -1) { query = StringUtils::insertValues( "UPDATE %s SET trigger_count = trigger_count + 1, " "last_trigger = datetime('now') " "WHERE ipv6_cidr = ?;", ServerConfig::m_ipv6_ban_table.c_str()); easySQLQuery(query, [ipv6_cidr](sqlite3_stmt* stmt) { if (sqlite3_bind_text(stmt, 1, ipv6_cidr.c_str(), -1, SQLITE_TRANSIENT) != SQLITE_OK) { Log::error("easySQLQuery", "Failed to bind %s.", ipv6_cidr.c_str()); } }); } #endif } // testBannedForIPv6 //----------------------------------------------------------------------------- void ServerLobby::testBannedForOnlineId(STKPeer* peer, uint32_t online_id) const { #ifdef ENABLE_SQLITE3 if (!m_db || !m_online_id_ban_table_exists) return; int row_id = -1; std::string query = StringUtils::insertValues( "SELECT rowid, reason, description FROM %s " "WHERE online_id = %u " "AND datetime('now') > datetime(starting_time) AND " "(expired_days is NULL OR datetime" "(starting_time, '+'||expired_days||' days') > datetime('now')) " "LIMIT 1;", ServerConfig::m_online_id_ban_table.c_str(), online_id); sqlite3_stmt* stmt = NULL; int ret = sqlite3_prepare_v2(m_db, query.c_str(), -1, &stmt, 0); if (ret == SQLITE_OK) { ret = sqlite3_step(stmt); if (ret == SQLITE_ROW) { row_id = sqlite3_column_int(stmt, 0); const char* reason = (char*)sqlite3_column_text(stmt, 1); const char* desc = (char*)sqlite3_column_text(stmt, 2); Log::info("ServerLobby", "%s banned by online id: %s " "(online id: %u rowid: %d, description: %s).", peer->getAddress().toString().c_str(), reason, online_id, row_id, desc); kickPlayerWithReason(peer, reason); } ret = sqlite3_finalize(stmt); if (ret != SQLITE_OK) { Log::error("ServerLobby", "Error finalize database: %s", sqlite3_errmsg(m_db)); } } else { Log::error("ServerLobby", "Error preparing database: %s", sqlite3_errmsg(m_db)); return; } if (row_id != -1) { query = StringUtils::insertValues( "UPDATE %s SET trigger_count = trigger_count + 1, " "last_trigger = datetime('now') " "WHERE online_id = %u;", ServerConfig::m_online_id_ban_table.c_str(), online_id); easySQLQuery(query); } #endif } // testBannedForOnlineId //----------------------------------------------------------------------------- void ServerLobby::listBanTable() { #ifdef ENABLE_SQLITE3 if (!m_db) return; auto printer = [](void* data, int argc, char** argv, char** name) { for (int i = 0; i < argc; i++) { std::cout << name[i] << " = " << (argv[i] ? argv[i] : "NULL") << "\n"; } std::cout << "\n"; return 0; }; if (m_ip_ban_table_exists) { std::string query = "SELECT * FROM "; query += ServerConfig::m_ip_ban_table; query += ";"; std::cout << "IP ban list:\n"; sqlite3_exec(m_db, query.c_str(), printer, NULL, NULL); } if (m_online_id_ban_table_exists) { std::string query = "SELECT * FROM "; query += ServerConfig::m_online_id_ban_table; query += ";"; std::cout << "Online Id ban list:\n"; sqlite3_exec(m_db, query.c_str(), printer, NULL, NULL); } #endif } // listBanTable //----------------------------------------------------------------------------- float ServerLobby::getStartupBoostOrPenaltyForKart(uint32_t ping, unsigned kart_id) { AbstractKart* k = World::getWorld()->getKart(kart_id); if (k->getStartupBoost() != 0.0f) return k->getStartupBoost(); uint64_t now = STKHost::get()->getNetworkTimer(); uint64_t client_time = now - ping / 2; uint64_t server_time = client_time + m_server_delay; int ticks = stk_config->time2Ticks( (float)(server_time - m_server_started_at) / 1000.0f); if (ticks < stk_config->time2Ticks(1.0f)) { PlayerController* pc = dynamic_cast(k->getController()); pc->displayPenaltyWarning(); return -1.0f; } float f = k->getStartupBoostFromStartTicks(ticks); k->setStartupBoost(f); return f; } // getStartupBoostOrPenaltyForKart //----------------------------------------------------------------------------- /*! \brief Called when the server owner request to change game mode or * difficulty. * \param event : Event providing the information. * * Format of the data : * Byte 0 1 2 * ----------------------------------------------- * Size | 1 | 1 | 1 | * Data | difficulty | game mode | soccer goal target | * ----------------------------------------------- */ void ServerLobby::handleServerConfiguration(Event* event) { if (m_state != WAITING_FOR_START_GAME) { Log::warn("ServerLobby", "Received handleServerConfiguration while being in state %d.", m_state.load()); return; } if (!ServerConfig::m_server_configurable) { Log::warn("ServerLobby", "server-configurable is not enabled."); return; } if (event->getPeerSP() != m_server_owner.lock()) { Log::warn("ServerLobby", "Client %d is not authorised to config server.", event->getPeer()->getHostId()); return; } NetworkString& data = event->data(); int new_difficulty = data.getUInt8(); int new_game_mode = data.getUInt8(); bool new_soccer_goal_target = data.getUInt8() == 1; auto modes = ServerConfig::getLocalGameMode(new_game_mode); if (modes.second == RaceManager::MAJOR_MODE_GRAND_PRIX) { Log::warn("ServerLobby", "Grand prix is used for new mode."); return; } RaceManager::get()->setMinorMode(modes.first); RaceManager::get()->setMajorMode(modes.second); RaceManager::get()->setDifficulty(RaceManager::Difficulty(new_difficulty)); m_game_setup->resetExtraServerInfo(); if (RaceManager::get()->getMinorMode() == RaceManager::MINOR_MODE_SOCCER) m_game_setup->setSoccerGoalTarget(new_soccer_goal_target); if (NetworkConfig::get()->isWAN() && (m_difficulty.load() != new_difficulty || m_game_mode.load() != new_game_mode)) { Log::info("ServerLobby", "Updating server info with new " "difficulty: %d, game mode: %d to stk-addons.", new_difficulty, new_game_mode); int priority = Online::RequestManager::HTTP_MAX_PRIORITY; auto request = std::make_shared(priority); NetworkConfig::get()->setServerDetails(request, "update-config"); const SocketAddress& addr = STKHost::get()->getPublicAddress(); request->addParameter("address", addr.getIP()); request->addParameter("port", addr.getPort()); request->addParameter("new-difficulty", new_difficulty); request->addParameter("new-game-mode", new_game_mode); request->queue(); } m_difficulty.store(new_difficulty); m_game_mode.store(new_game_mode); updateTracksForMode(); auto peers = STKHost::get()->getPeers(); for (auto& peer : peers) { auto assets = peer->getClientAssets(); if (!peer->isValidated() || assets.second.empty()) continue; std::set tracks_erase; for (const std::string& server_track : m_available_kts.second) { if (assets.second.find(server_track) == assets.second.end()) { tracks_erase.insert(server_track); } } if (tracks_erase.size() == m_available_kts.second.size()) { NetworkString *message = getNetworkString(2); message->setSynchronous(true); message->addUInt8(LE_CONNECTION_REFUSED) .addUInt8(RR_INCOMPATIBLE_DATA); peer->cleanPlayerProfiles(); peer->sendPacket(message, true/*reliable*/); peer->reset(); delete message; Log::verbose("ServerLobby", "Player has incompatible tracks for new game mode."); } } NetworkString* server_info = getNetworkString(); server_info->setSynchronous(true); server_info->addUInt8(LE_SERVER_INFO); m_game_setup->addServerInfo(server_info); sendMessageToPeers(server_info); delete server_info; updatePlayerList(); } // handleServerConfiguration //----------------------------------------------------------------------------- /*! \brief Called when a player want to change his handicap * \param event : Event providing the information. * * Format of the data : * Byte 0 1 * ---------------------------------- * Size | 1 | 1 | * Data | local player id | new handicap | * ---------------------------------- */ void ServerLobby::changeHandicap(Event* event) { NetworkString& data = event->data(); if (m_state.load() != WAITING_FOR_START_GAME && !event->getPeer()->isWaitingForGame()) { Log::warn("ServerLobby", "Set handicap at wrong time."); return; } uint8_t local_id = data.getUInt8(); auto& player = event->getPeer()->getPlayerProfiles().at(local_id); uint8_t handicap_id = data.getUInt8(); if (handicap_id >= HANDICAP_COUNT) { Log::warn("ServerLobby", "Wrong handicap %d.", handicap_id); return; } HandicapLevel h = (HandicapLevel)handicap_id; player->setHandicap(h); updatePlayerList(); } // changeHandicap //----------------------------------------------------------------------------- /** Update and see if any player disconnects, if so eliminate the kart in * world, so this function must be called in main thread. */ void ServerLobby::handlePlayerDisconnection() const { if (!World::getWorld() || World::getWorld()->getPhase() < WorldStatus::MUSIC_PHASE) { return; } int red_count = 0; int blue_count = 0; unsigned total = 0; for (unsigned i = 0; i < RaceManager::get()->getNumPlayers(); i++) { RemoteKartInfo& rki = RaceManager::get()->getKartInfo(i); if (rki.isReserved()) continue; bool disconnected = rki.disconnected(); if (RaceManager::get()->getKartInfo(i).getKartTeam() == KART_TEAM_RED && !disconnected) red_count++; else if (RaceManager::get()->getKartInfo(i).getKartTeam() == KART_TEAM_BLUE && !disconnected) blue_count++; if (!disconnected) { total++; continue; } else rki.makeReserved(); AbstractKart* k = World::getWorld()->getKart(i); if (!k->isEliminated() && !k->hasFinishedRace()) { CaptureTheFlag* ctf = dynamic_cast (World::getWorld()); if (ctf) ctf->loseFlagForKart(k->getWorldKartId()); World::getWorld()->eliminateKart(i, false/*notify_of_elimination*/); if (ServerConfig::m_ranked) { // Handle disconnection earlier to prevent cheating by joining // another ranked server // Real score will be submitted later in computeNewRankings const uint32_t id = RaceManager::get()->getKartInfo(i).getOnlineId(); unsigned num_races = m_num_ranked_races.at(id); uint64_t disconnects = m_num_ranked_disconnects.at(id) << 1; auto request = std::make_shared (id, m_scores.at(id) - 200.0, m_max_scores.at(id), ++num_races, m_raw_scores.at(id) - 200.0, m_rating_deviations.at(id), ++disconnects, RaceManager::get()->getKartInfo(i).getCountryCode()); NetworkConfig::get()->setUserDetails(request, "submit-ranking"); request->queue(); } k->setPosition( World::getWorld()->getCurrentNumKarts() + 1); k->finishedRace(World::getWorld()->getTime(), true/*from_server*/); } } // If live players is enabled, don't end the game if unfair team if (!ServerConfig::m_live_players && total != 1 && World::getWorld()->hasTeam() && (red_count == 0 || blue_count == 0)) World::getWorld()->setUnfairTeam(true); } // handlePlayerDisconnection //----------------------------------------------------------------------------- /** Add reserved players for live join later if required. */ void ServerLobby::addLiveJoinPlaceholder( std::vector >& players) const { if (!ServerConfig::m_live_players || !RaceManager::get()->supportsLiveJoining()) return; if (RaceManager::get()->getMinorMode() == RaceManager::MINOR_MODE_FREE_FOR_ALL) { Track* t = track_manager->getTrack(m_game_setup->getCurrentTrack()); assert(t); int max_players = std::min((int)ServerConfig::m_server_max_players, (int)t->getMaxArenaPlayers()); int add_size = max_players - (int)players.size(); assert(add_size >= 0); for (int i = 0; i < add_size; i++) { players.push_back( NetworkPlayerProfile::getReservedProfile(KART_TEAM_NONE)); } } else { // CTF or soccer, reserve at most 7 players on each team int red_count = 0; int blue_count = 0; for (unsigned i = 0; i < players.size(); i++) { if (players[i]->getTeam() == KART_TEAM_RED) red_count++; else blue_count++; } red_count = red_count >= 7 ? 0 : 7 - red_count; blue_count = blue_count >= 7 ? 0 : 7 - blue_count; for (int i = 0; i < red_count; i++) { players.push_back( NetworkPlayerProfile::getReservedProfile(KART_TEAM_RED)); } for (int i = 0; i < blue_count; i++) { players.push_back( NetworkPlayerProfile::getReservedProfile(KART_TEAM_BLUE)); } } } // addLiveJoinPlaceholder //----------------------------------------------------------------------------- void ServerLobby::setPlayerKarts(const NetworkString& ns, STKPeer* peer) const { unsigned player_count = ns.getUInt8(); for (unsigned i = 0; i < player_count; i++) { std::string kart; ns.decodeString(&kart); if (kart.find("randomkart") != std::string::npos || (kart.find("addon_") == std::string::npos && m_available_kts.first.find(kart) == m_available_kts.first.end())) { RandomGenerator rg; std::set::iterator it = m_available_kts.first.begin(); std::advance(it, rg.get((int)m_available_kts.first.size())); peer->getPlayerProfiles()[i]->setKartName(*it); } else { peer->getPlayerProfiles()[i]->setKartName(kart); } } if (peer->getClientCapabilities().find("real_addon_karts") == peer->getClientCapabilities().end() || ns.size() == 0) return; for (unsigned i = 0; i < player_count; i++) { KartData kart_data(ns); std::string type = kart_data.m_kart_type; auto& player = peer->getPlayerProfiles()[i]; const std::string& kart_id = player->getKartName(); if (NetworkConfig::get()->useTuxHitboxAddon() && StringUtils::startsWith(kart_id, "addon_") && kart_properties_manager->hasKartTypeCharacteristic(type)) { const KartProperties* real_addon = kart_properties_manager->getKart(kart_id); if (ServerConfig::m_real_addon_karts && real_addon) { kart_data = KartData(real_addon); } else { const KartProperties* tux_kp = kart_properties_manager->getKart("tux"); kart_data = KartData(tux_kp); kart_data.m_kart_type = type; } player->setKartData(kart_data); } } } // setPlayerKarts //----------------------------------------------------------------------------- /** Tell the client \ref RemoteKartInfo of a player when some player joining * live. */ void ServerLobby::handleKartInfo(Event* event) { World* w = World::getWorld(); if (!w) return; STKPeer* peer = event->getPeer(); const NetworkString& data = event->data(); uint8_t kart_id = data.getUInt8(); if (kart_id > RaceManager::get()->getNumPlayers()) return; AbstractKart* k = w->getKart(kart_id); int live_join_util_ticks = k->getLiveJoinUntilTicks(); const RemoteKartInfo& rki = RaceManager::get()->getKartInfo(kart_id); NetworkString* ns = getNetworkString(1); ns->setSynchronous(true); ns->addUInt8(LE_KART_INFO).addUInt32(live_join_util_ticks) .addUInt8(kart_id) .encodeString(rki.getPlayerName()) .addUInt32(rki.getHostId()).addFloat(rki.getDefaultKartColor()) .addUInt32(rki.getOnlineId()).addUInt8(rki.getHandicap()) .addUInt8((uint8_t)rki.getLocalPlayerId()) .encodeString(rki.getKartName()).encodeString(rki.getCountryCode()); if (peer->getClientCapabilities().find("real_addon_karts") != peer->getClientCapabilities().end()) rki.getKartData().encode(ns); peer->sendPacket(ns, true/*reliable*/); delete ns; } // handleKartInfo //----------------------------------------------------------------------------- /** Client if currently in-game (including spectator) wants to go back to * lobby. */ void ServerLobby::clientInGameWantsToBackLobby(Event* event) { World* w = World::getWorld(); std::shared_ptr peer = event->getPeerSP(); if (!w || !worldIsActive() || peer->isWaitingForGame()) { Log::warn("ServerLobby", "%s try to leave the game at wrong time.", peer->getAddress().toString().c_str()); return; } if (m_process_type == PT_CHILD && event->getPeer()->getHostId() == m_client_server_host_id.load()) { // For child server the remaining client cannot go on player when the // server owner quited the game (because the world will be deleted), so // we reset all players auto pm = ProtocolManager::lock(); if (RaceEventManager::get()) { RaceEventManager::get()->stop(); pm->findAndTerminate(PROTOCOL_GAME_EVENTS); } auto gp = GameProtocol::lock(); if (gp) { auto lock = gp->acquireWorldDeletingMutex(); pm->findAndTerminate(PROTOCOL_CONTROLLER_EVENTS); exitGameState(); } else exitGameState(); NetworkString* back_to_lobby = getNetworkString(2); back_to_lobby->setSynchronous(true); back_to_lobby->addUInt8(LE_BACK_LOBBY) .addUInt8(BLR_SERVER_ONWER_QUITED_THE_GAME); sendMessageToPeersInServer(back_to_lobby, /*reliable*/true); delete back_to_lobby; m_rs_state.store(RS_ASYNC_RESET); return; } for (const int id : peer->getAvailableKartIDs()) { RemoteKartInfo& rki = RaceManager::get()->getKartInfo(id); if (rki.getHostId() == peer->getHostId()) { Log::info("ServerLobby", "%s left the game with kart id %d.", peer->getAddress().toString().c_str(), id); rki.setNetworkPlayerProfile( std::shared_ptr()); } else { Log::error("ServerLobby", "%s doesn't exist anymore in server.", peer->getAddress().toString().c_str()); } } NetworkItemManager* nim = dynamic_cast (Track::getCurrentTrack()->getItemManager()); assert(nim); nim->erasePeerInGame(peer); m_peers_ready.erase(peer); peer->setWaitingForGame(true); peer->setSpectator(false); NetworkString* reset = getNetworkString(2); reset->setSynchronous(true); reset->addUInt8(LE_BACK_LOBBY).addUInt8(BLR_NONE); peer->sendPacket(reset, /*reliable*/true); delete reset; updatePlayerList(); NetworkString* server_info = getNetworkString(); server_info->setSynchronous(true); server_info->addUInt8(LE_SERVER_INFO); m_game_setup->addServerInfo(server_info); peer->sendPacket(server_info, /*reliable*/true); delete server_info; } // clientInGameWantsToBackLobby //----------------------------------------------------------------------------- /** Client if currently select assets wants to go back to lobby. */ void ServerLobby::clientSelectingAssetsWantsToBackLobby(Event* event) { std::shared_ptr peer = event->getPeerSP(); if (m_state.load() != SELECTING || peer->isWaitingForGame()) { Log::warn("ServerLobby", "%s try to leave selecting assets at wrong time.", peer->getAddress().toString().c_str()); return; } if (m_process_type == PT_CHILD && event->getPeer()->getHostId() == m_client_server_host_id.load()) { NetworkString* back_to_lobby = getNetworkString(2); back_to_lobby->setSynchronous(true); back_to_lobby->addUInt8(LE_BACK_LOBBY) .addUInt8(BLR_SERVER_ONWER_QUITED_THE_GAME); sendMessageToPeersInServer(back_to_lobby, /*reliable*/true); delete back_to_lobby; resetVotingTime(); resetServer(); m_rs_state.store(RS_NONE); return; } m_peers_ready.erase(peer); peer->setWaitingForGame(true); peer->setSpectator(false); NetworkString* reset = getNetworkString(2); reset->setSynchronous(true); reset->addUInt8(LE_BACK_LOBBY).addUInt8(BLR_NONE); peer->sendPacket(reset, /*reliable*/true); delete reset; updatePlayerList(); NetworkString* server_info = getNetworkString(); server_info->setSynchronous(true); server_info->addUInt8(LE_SERVER_INFO); m_game_setup->addServerInfo(server_info); peer->sendPacket(server_info, /*reliable*/true); delete server_info; } // clientSelectingAssetsWantsToBackLobby std::set> ServerLobby::getSpectatorsByLimit() { std::set> spectators_by_limit; auto peers = STKHost::get()->getPeers(); std::set> always_spectate_peers; unsigned player_limit = ServerConfig::m_max_players_in_game; // only 10 players allowed for battle or soccer if (RaceManager::get()->isBattleMode() || RaceManager::get()->isSoccerMode()) player_limit = std::min(player_limit, 10u); unsigned ingame_players = 0, waiting_players = 0, total_players = 0; STKHost::get()->updatePlayers(&ingame_players, &waiting_players, &total_players); if (total_players <= player_limit) return spectators_by_limit; std::sort(peers.begin(), peers.end(), [](const std::shared_ptr& a, const std::shared_ptr& b) { return a->getHostId() < b->getHostId(); }); if (m_state.load() >= RACING) { for (auto &peer : peers) if (peer->isSpectator()) ingame_players -= (int)peer->getPlayerProfiles().size(); } unsigned player_count = 0; for (unsigned i = 0; i < peers.size(); i++) { auto& peer = peers[i]; if (!peer->isValidated()) continue; if (m_state.load() < RACING) { if (peer->alwaysSpectate() || peer->isWaitingForGame()) continue; player_count += (unsigned)peer->getPlayerProfiles().size(); if (player_count > player_limit) spectators_by_limit.insert(peer); } else { if (peer->isSpectator()) continue; player_count += (unsigned)peer->getPlayerProfiles().size(); if (peer->isWaitingForGame() && (player_count > player_limit || ingame_players >= player_limit)) spectators_by_limit.insert(peer); } } return spectators_by_limit; } //----------------------------------------------------------------------------- void ServerLobby::saveInitialItems(std::shared_ptr nim) { m_items_complete_state->getBuffer().clear(); m_items_complete_state->reset(); nim->saveCompleteState(m_items_complete_state); } // saveInitialItems //----------------------------------------------------------------------------- bool ServerLobby::supportsAI() { return getGameMode() == 3 || getGameMode() == 4; } // supportsAI //----------------------------------------------------------------------------- bool ServerLobby::checkPeersReady(bool ignore_ai_peer) const { bool all_ready = true; for (auto p : m_peers_ready) { auto peer = p.first.lock(); if (!peer) continue; if (ignore_ai_peer && peer->isAIPeer()) continue; all_ready = all_ready && p.second; if (!all_ready) return false; } return true; } // checkPeersReady //----------------------------------------------------------------------------- void ServerLobby::handleServerCommand(Event* event, std::shared_ptr peer) { NetworkString& data = event->data(); std::string language; data.decodeString(&language); std::string cmd; data.decodeString(&cmd); auto argv = StringUtils::split(cmd, ' '); if (argv.size() == 0) return; if (argv[0] == "spectate") { if (m_game_setup->isGrandPrix() || !ServerConfig::m_live_players) { NetworkString* chat = getNetworkString(); chat->addUInt8(LE_CHAT); chat->setSynchronous(true); std::string msg = "Server doesn't support spectate"; chat->encodeString16(StringUtils::utf8ToWide(msg)); peer->sendPacket(chat, true/*reliable*/); delete chat; return; } if (argv.size() != 2 || (argv[1] != "0" && argv[1] != "1") || m_state.load() != WAITING_FOR_START_GAME) { NetworkString* chat = getNetworkString(); chat->addUInt8(LE_CHAT); chat->setSynchronous(true); std::string msg = "Usage: spectate [0 or 1], before game started"; chat->encodeString16(StringUtils::utf8ToWide(msg)); peer->sendPacket(chat, true/*reliable*/); delete chat; return; } if (argv[1] == "1") { if (m_process_type == PT_CHILD && peer->getHostId() == m_client_server_host_id.load()) { NetworkString* chat = getNetworkString(); chat->addUInt8(LE_CHAT); chat->setSynchronous(true); std::string msg = "Graphical client server cannot spectate"; chat->encodeString16(StringUtils::utf8ToWide(msg)); peer->sendPacket(chat, true/*reliable*/); delete chat; return; } peer->setAlwaysSpectate(ASM_COMMAND); } else peer->setAlwaysSpectate(ASM_NONE); updatePlayerList(); } else if (argv[0] == "listserveraddon") { NetworkString* chat = getNetworkString(); chat->addUInt8(LE_CHAT); chat->setSynchronous(true); bool has_options = argv.size() > 1 && (argv[1].compare("-track") == 0 || argv[1].compare("-arena") == 0 || argv[1].compare("-kart") == 0 || argv[1].compare("-soccer") == 0); if (argv.size() == 1 || argv.size() > 3 || argv[1].size() < 3 || (argv.size() == 2 && (argv[1].size() < 3 || has_options)) || (argv.size() == 3 && (!has_options || argv[2].size() < 3))) { chat->encodeString16( L"Usage: /listserveraddon [option][addon string to find " "(at least 3 characters)]. Available options: " "-track, -arena, -kart, -soccer."); } 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("-soccer" ) == 0) type = argv[1].substr(1); if((argv.size() == 2 && type.empty()) || argv.size() == 3) text = argv[argv.size()-1]; } std::set total_addons; if(type.empty() || // not specify addon type (!type.empty() && type.compare("kart") == 0)) // list kart addon { total_addons.insert(m_addon_kts.first.begin(), m_addon_kts.first.end()); } if(type.empty() || // not specify addon type (!type.empty() && type.compare("track") == 0)) { total_addons.insert(m_addon_kts.second.begin(), m_addon_kts.second.end()); } if(type.empty() || // not specify addon type (!type.empty() && type.compare("arena") == 0)) { total_addons.insert(m_addon_arenas.begin(), m_addon_arenas.end()); } if(type.empty() || // not specify addon type (!type.empty() && type.compare("soccer") == 0)) { total_addons.insert(m_addon_soccers.begin(), m_addon_soccers.end()); } 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()) chat->encodeString16(L"Addon not found"); else { msg = msg.substr(0, msg.size() - 2); chat->encodeString16(StringUtils::utf8ToWide( std::string("Server addon: ") + msg)); } } peer->sendPacket(chat, true/*reliable*/); delete chat; } else if (StringUtils::startsWith(cmd, "playerhasaddon")) { NetworkString* chat = getNetworkString(); chat->addUInt8(LE_CHAT); chat->setSynchronous(true); std::string part; if (cmd.length() > 15) part = cmd.substr(15); std::string addon_id = part.substr(0, part.find(' ')); std::string player_name; if (part.length() > addon_id.length() + 1) player_name = part.substr(addon_id.length() + 1); std::shared_ptr player_peer = STKHost::get()->findPeerByName( StringUtils::utf8ToWide(player_name)); if (player_name.empty() || !player_peer || addon_id.empty()) { chat->encodeString16( L"Usage: /playerhasaddon [addon_identity] [player name]"); } else { std::string addon_id_test = Addon::createAddonId(addon_id); bool found = false; const auto& kt = player_peer->getClientAssets(); for (auto& kart : kt.first) { if (kart == addon_id_test) { found = true; break; } } if (!found) { for (auto& track : kt.second) { if (track == addon_id_test) { found = true; break; } } } if (found) { chat->encodeString16(StringUtils::utf8ToWide (player_name + " has addon " + addon_id)); } else { chat->encodeString16(StringUtils::utf8ToWide (player_name + " has no addon " + addon_id)); } } peer->sendPacket(chat, true/*reliable*/); delete chat; } else if (StringUtils::startsWith(cmd, "kick")) { if (m_server_owner.lock() != peer) { NetworkString* chat = getNetworkString(); chat->addUInt8(LE_CHAT); chat->setSynchronous(true); chat->encodeString16(L"You are not server owner"); peer->sendPacket(chat, true/*reliable*/); delete chat; return; } std::string player_name; if (cmd.length() > 5) player_name = cmd.substr(5); std::shared_ptr player_peer = STKHost::get()->findPeerByName( StringUtils::utf8ToWide(player_name)); if (player_name.empty() || !player_peer || player_peer->isAIPeer()) { NetworkString* chat = getNetworkString(); chat->addUInt8(LE_CHAT); chat->setSynchronous(true); chat->encodeString16( L"Usage: /kick [player name]"); peer->sendPacket(chat, true/*reliable*/); delete chat; } else { player_peer->kick(); } } else if (StringUtils::startsWith(cmd, "playeraddonscore")) { NetworkString* chat = getNetworkString(); chat->addUInt8(LE_CHAT); chat->setSynchronous(true); std::string player_name; if (cmd.length() > 17) player_name = cmd.substr(17); std::shared_ptr player_peer = STKHost::get()->findPeerByName( StringUtils::utf8ToWide(player_name)); if (player_name.empty() || !player_peer) { chat->encodeString16( L"Usage: /playeraddonscore [player name] (return 0-100)"); } else { auto& scores = player_peer->getAddonsScores(); if (scores[AS_KART] == -1 && scores[AS_TRACK] == -1 && scores[AS_ARENA] == -1 && scores[AS_SOCCER] == -1) { chat->encodeString16(StringUtils::utf8ToWide (player_name + " has no addon")); } else { std::string msg = player_name; msg += " addon:"; if (scores[AS_KART] != -1) msg += " kart: " + StringUtils::toString(scores[AS_KART]) + ","; if (scores[AS_TRACK] != -1) msg += " track: " + StringUtils::toString(scores[AS_TRACK]) + ","; if (scores[AS_ARENA] != -1) msg += " arena: " + StringUtils::toString(scores[AS_ARENA]) + ","; if (scores[AS_SOCCER] != -1) msg += " soccer: " + StringUtils::toString(scores[AS_SOCCER]) + ","; msg = msg.substr(0, msg.size() - 1); chat->encodeString16(StringUtils::utf8ToWide(msg)); } } peer->sendPacket(chat, true/*reliable*/); delete chat; } else if (argv[0] == "serverhasaddon") { NetworkString* chat = getNetworkString(); chat->addUInt8(LE_CHAT); chat->setSynchronous(true); if (argv.size() != 2) { chat->encodeString16( L"Usage: /serverhasaddon [addon_identity]"); } else { std::set total_addons; total_addons.insert(m_addon_kts.first.begin(), m_addon_kts.first.end()); total_addons.insert(m_addon_kts.second.begin(), m_addon_kts.second.end()); total_addons.insert(m_addon_arenas.begin(), m_addon_arenas.end()); total_addons.insert(m_addon_soccers.begin(), m_addon_soccers.end()); std::string addon_id_test = Addon::createAddonId(argv[1]); bool found = total_addons.find(addon_id_test) != total_addons.end(); if (found) { chat->encodeString16(StringUtils::utf8ToWide(std::string ("Server has addon ") + argv[1])); } else { chat->encodeString16(StringUtils::utf8ToWide(std::string ("Server has no addon ") + argv[1])); } } peer->sendPacket(chat, true/*reliable*/); delete chat; } else if (argv[0] == "mute") { std::shared_ptr player_peer; std::string result_msg; core::stringw player_name; NetworkString* result = NULL; if (argv.size() != 2 || argv[1].empty()) goto mute_error; player_name = StringUtils::utf8ToWide(argv[1]); player_peer = STKHost::get()->findPeerByName(player_name); if (!player_peer || player_peer == peer) goto mute_error; m_peers_muted_players[peer].insert(player_name); result = getNetworkString(); result->addUInt8(LE_CHAT); result->setSynchronous(true); result_msg = "Muted player "; result_msg += argv[1]; result->encodeString16(StringUtils::utf8ToWide(result_msg)); peer->sendPacket(result, true/*reliable*/); delete result; return; mute_error: NetworkString* error = getNetworkString(); error->addUInt8(LE_CHAT); error->setSynchronous(true); std::string msg = "Usage: /mute player_name (not including yourself)"; error->encodeString16(StringUtils::utf8ToWide(msg)); peer->sendPacket(error, true/*reliable*/); delete error; } else if (argv[0] == "unmute") { std::shared_ptr player_peer; std::string result_msg; core::stringw player_name; NetworkString* result = NULL; if (argv.size() != 2 || argv[1].empty()) goto unmute_error; player_name = StringUtils::utf8ToWide(argv[1]); for (auto it = m_peers_muted_players[peer].begin(); it != m_peers_muted_players[peer].end();) { if (*it == player_name) { it = m_peers_muted_players[peer].erase(it); goto unmute_found; } else { it++; } } goto unmute_error; unmute_found: result = getNetworkString(); result->addUInt8(LE_CHAT); result->setSynchronous(true); result_msg = "Unmuted player "; result_msg += argv[1]; result->encodeString16(StringUtils::utf8ToWide(result_msg)); peer->sendPacket(result, true/*reliable*/); delete result; return; unmute_error: NetworkString* error = getNetworkString(); error->addUInt8(LE_CHAT); error->setSynchronous(true); std::string msg = "Usage: /unmute player_name"; error->encodeString16(StringUtils::utf8ToWide(msg)); peer->sendPacket(error, true/*reliable*/); delete error; } else if (argv[0] == "listmute") { NetworkString* chat = getNetworkString(); chat->addUInt8(LE_CHAT); chat->setSynchronous(true); core::stringw total; for (auto& name : m_peers_muted_players[peer]) { total += name; total += " "; } if (total.empty()) chat->encodeString16("No player has been muted by you"); else { total += "muted"; chat->encodeString16(total); } peer->sendPacket(chat, true/*reliable*/); delete chat; } else { NetworkString* chat = getNetworkString(); chat->addUInt8(LE_CHAT); chat->setSynchronous(true); std::string msg = "Unknown command: "; msg += cmd; chat->encodeString16(StringUtils::utf8ToWide(msg)); peer->sendPacket(chat, true/*reliable*/); delete chat; } } // handleServerCommand