diff --git a/src/main.cpp b/src/main.cpp index 8bc53baa9..4a2e4c8d0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1440,7 +1440,7 @@ int handleCmdLine(bool has_server_config, bool has_parent_process) { std::string cmd = std::string("--stdout=server_ai.log --no-graphics" - " --auto-connect --connect-now=127.0.0.1:") + + " --connect-now=127.0.0.1:") + StringUtils::toString(STKHost::get()->getPrivatePort()) + " --no-console-log --network-ai=" + StringUtils::toString(ai_num); diff --git a/src/network/protocols/server_lobby.cpp b/src/network/protocols/server_lobby.cpp index 5e232a3a0..2d08457d8 100644 --- a/src/network/protocols/server_lobby.cpp +++ b/src/network/protocols/server_lobby.cpp @@ -536,6 +536,7 @@ void ServerLobby::setup() 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()) { @@ -547,6 +548,11 @@ void ServerLobby::setup() for (auto player : players) player->setKartName(""); } + if (auto ai = m_ai_peer.lock()) + { + for (auto player : ai->getPlayerProfiles()) + player->setKartName(""); + } StateManager::get()->resetActivePlayers(); // We use maximum 16bit unsigned limit @@ -664,7 +670,8 @@ void ServerLobby::kickHost(Event* event) NetworkString& data = event->data(); uint32_t host_id = data.getUInt32(); std::shared_ptr peer = STKHost::get()->findPeerByHostId(host_id); - if (peer) + // Ignore kicking ai peer if ai handling is on + if (peer && (!ServerConfig::m_ai_handling || !peer->isAIPeer())) peer->kick(); } // kickHost @@ -1113,7 +1120,7 @@ void ServerLobby::asynchronousUpdate() m_timeout.store(std::numeric_limits::max()); } if (m_timeout.load() < (int64_t)StkTime::getMonoTimeMs() || - (checkPeersReady() && + (checkPeersReady(true/*ignore_ai_peer*/) && (int)players >= ServerConfig::m_min_start_game_players)) { resetPeersReady(); @@ -1151,7 +1158,8 @@ void ServerLobby::asynchronousUpdate() // m_server_has_loaded_world is set by main thread with atomic write if (m_server_has_loaded_world.load() == false) return; - if (!checkPeersReady()) + if (!checkPeersReady( + ServerConfig::m_ai_handling && m_ai_count == 0/*ignore_ai_peer*/)) return; // Reset for next state usage resetPeersReady(); @@ -1188,6 +1196,17 @@ void ServerLobby::asynchronousUpdate() ItemManager::updateRandomSeed(m_item_seed); m_game_setup->setRace(winner_vote); auto players = STKHost::get()->getPlayersForNewGame(); + auto ai = m_ai_peer.lock(); + if (supportsAI() && ai) + { + auto ai_profiles = ai->getPlayerProfiles(); + if (m_ai_count > 0) + { + ai_profiles.resize(m_ai_count); + players.insert(players.end(), ai_profiles.begin(), + ai_profiles.end()); + } + } m_game_setup->sortPlayersForGrandPrix(players); m_game_setup->sortPlayersForGame(players); for (unsigned i = 0; i < players.size(); i++) @@ -1795,7 +1814,7 @@ void ServerLobby::update(int ticks) Log::info("ServerLobby", "End of game message sent"); break; case RESULT_DISPLAY: - if (checkPeersReady() || + if (checkPeersReady(true/*ignore_ai_peer*/) || (int64_t)StkTime::getMonoTimeMs() > m_timeout.load()) { // Send a notification to all clients to exit @@ -2016,10 +2035,33 @@ void ServerLobby::startSelection(const Event *event) m_available_kts.second.erase(track_erase); } + unsigned 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 (race_manager->getMinorMode() == RaceManager::MINOR_MODE_FREE_FOR_ALL) { - unsigned max_player = 0; - STKHost::get()->updatePlayers(&max_player); auto it = m_available_kts.second.begin(); while (it != m_available_kts.second.end()) { @@ -2842,6 +2884,7 @@ void ServerLobby::connectionRequested(Event* event) // 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 = @@ -2853,7 +2896,11 @@ void ServerLobby::connectionRequested(Event* event) NetworkConfig::get()->isWAN() && ServerConfig::m_validating_player) || (ServerConfig::m_strict_players && - (player_count != 1 || online_id == 0 || duplicated_ranked_player))) + (player_count != 1 || online_id == 0 || duplicated_ranked_player)) || + (peer->isAIPeer() && !peer->getAddress().isLAN()) || + (peer->isAIPeer() && + ServerConfig::m_ai_handling && !m_ai_peer.expired()) || + (peer->isAIPeer() && m_game_setup->isGrandPrix())) { NetworkString* message = getNetworkString(2); message->setSynchronous(true); @@ -2865,6 +2912,9 @@ void ServerLobby::connectionRequested(Event* event) 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, @@ -2984,7 +3034,7 @@ void ServerLobby::handleUnencryptedConnection(std::shared_ptr peer, peer->addPlayer(player); } - peer->setValidated(); + peer->setValidated(true); // send a message to the one that asked to connect NetworkString* server_info = getNetworkString(); @@ -3053,7 +3103,7 @@ void ServerLobby::handleUnencryptedConnection(std::shared_ptr peer, } } #ifdef ENABLE_SQLITE3 - if (m_server_stats_table.empty()) + if (m_server_stats_table.empty() || peer->isAIPeer()) return; std::string query; if (ServerConfig::m_ipv6_server && !peer->getIPV6Address().empty()) @@ -3135,6 +3185,31 @@ void ServerLobby::updatePlayerList(bool update_when_reset_server) return; auto all_profiles = STKHost::get()->getAllPlayerProfiles(); + // N - 1 AI + auto ai = m_ai_peer.lock(); + if (supportsAI() && ai) + { + auto ai_profiles = ai->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.empty()) + { + 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()); + } + NetworkString* pl = getNetworkString(); pl->setSynchronous(true); pl->addUInt8(LE_UPDATE_PLAYER_LIST) @@ -3390,6 +3465,8 @@ bool ServerLobby::handleAllVotes(PeerVote* winner_vote, auto peers = STKHost::get()->getPeers(); for (auto peer : peers) { + if (peer->isAIPeer()) + continue; if (peer->hasPlayerProfiles() && !peer->isWaitingForGame()) cur_players += 1.0f; } @@ -3862,6 +3939,9 @@ void ServerLobby::addWaitingPlayersToGame() getRankingForPlayer(peer->getPlayerProfiles()[0]); } } + // Re-activiate the ai + if (auto ai = m_ai_peer.lock()) + ai->setValidated(true); } // addWaitingPlayersToGame //----------------------------------------------------------------------------- @@ -4465,3 +4545,27 @@ void ServerLobby::saveInitialItems() assert(nim); 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 (ignore_ai_peer && peer->isAIPeer()) + continue; + if (!peer) + continue; + all_ready = all_ready && p.second; + if (!all_ready) + return false; + } + return true; +} // checkPeersReady diff --git a/src/network/protocols/server_lobby.hpp b/src/network/protocols/server_lobby.hpp index 4d1af011f..020f1ac39 100644 --- a/src/network/protocols/server_lobby.hpp +++ b/src/network/protocols/server_lobby.hpp @@ -116,6 +116,8 @@ private: * (disconnected). */ std::weak_ptr m_server_owner; + std::weak_ptr m_ai_peer; + std::atomic m_server_owner_id; /** Official karts and tracks available in server. */ @@ -207,6 +209,9 @@ private: uint64_t m_client_starting_time; + // Calculated before each game started + unsigned m_ai_count; + // connection management void clientDisconnected(Event* event); void connectionRequested(Event* event); @@ -227,19 +232,7 @@ private: void updateServerOwner(); void handleServerConfiguration(Event* event); void updateTracksForMode(); - bool checkPeersReady() const - { - bool all_ready = true; - for (auto p : m_peers_ready) - { - if (p.first.expired()) - continue; - all_ready = all_ready && p.second; - if (!all_ready) - return false; - } - return true; - } + bool checkPeersReady(bool ignore_ai_peer) const; void resetPeersReady() { for (auto it = m_peers_ready.begin(); it != m_peers_ready.end();) @@ -333,6 +326,7 @@ private: void testBannedForOnlineId(STKPeer* peer, uint32_t online_id) const; void writeDisconnectInfoTable(STKPeer* peer); void writePlayerReport(Event* event); + bool supportsAI(); public: ServerLobby(); virtual ~ServerLobby(); diff --git a/src/network/server_config.hpp b/src/network/server_config.hpp index ecdeed43a..e5cb4d68b 100644 --- a/src/network/server_config.hpp +++ b/src/network/server_config.hpp @@ -385,6 +385,12 @@ namespace ServerConfig "empty to disable. " "This table can be shared for all servers if you use the same name.")); + SERVER_CFG_PREFIX BoolServerConfigParam m_ai_handling + SERVER_CFG_DEFAULT(BoolServerConfigParam(false, "ai-handling", + "If true this server will auto add / remove AI connected with " + "network-ai=x, which will kick N - 1 bot(s) where N is the number " + "of human players. Only use this for non-GP racing server.")); + // ======================================================================== /** Server version, will be advanced if there are protocol changes. */ static const uint32_t m_server_version = 6; diff --git a/src/network/stk_host.cpp b/src/network/stk_host.cpp index bd230a203..ddfe281e3 100644 --- a/src/network/stk_host.cpp +++ b/src/network/stk_host.cpp @@ -266,7 +266,11 @@ STKHost::STKHost(bool server) if (addr.port == 0 && !UserConfigParams::m_random_server_port) addr.port = stk_config->m_server_port; // Reserve 1 peer to deliver full server message - m_network = new Network(ServerConfig::m_server_max_players + 1, + int peer_count = ServerConfig::m_server_max_players + 1; + // 1 more peer to hold ai peer + if (ServerConfig::m_ai_handling) + peer_count++; + m_network = new Network(peer_count, /*channel_limit*/EVENT_CHANNEL_COUNT, /*max_in_bandwidth*/0, /*max_out_bandwidth*/ 0, &addr, true/*change_port_if_bound*/); } @@ -1059,7 +1063,8 @@ void STKHost::mainLoop() // Remove peer which has not been validated after a specific time // It is validated when the first connection request has finished - if (!it->second->isValidated() && + if (!it->second->isAIPeer() && + !it->second->isValidated() && it->second->getConnectedTime() > timeout) { Log::info("STKHost", "%s has not been validated for more" @@ -1155,7 +1160,7 @@ void STKHost::mainLoop() getPeerCount()); // Client always trust the server if (!is_server) - stk_peer->setValidated(); + stk_peer->setValidated(true); } // ENET_EVENT_TYPE_CONNECT else if (event.type == ENET_EVENT_TYPE_DISCONNECT) { @@ -1510,6 +1515,8 @@ std::vector > { if (peer.second->isDisconnected() || !peer.second->isValidated()) continue; + if (ServerConfig::m_ai_handling && peer.second->isAIPeer()) + continue; auto peer_profile = peer.second->getPlayerProfiles(); p.insert(p.end(), peer_profile.begin(), peer_profile.end()); } @@ -1561,7 +1568,7 @@ void STKHost::initClientNetwork(ENetEvent& event, Network* new_network) } auto stk_peer = std::make_shared(event.peer, this, m_next_unique_host_id++); - stk_peer->setValidated(); + stk_peer->setValidated(true); m_peers[event.peer] = stk_peer; setPrivatePort(); auto pm = ProtocolManager::lock(); @@ -1599,6 +1606,8 @@ std::vector > auto& stk_peer = p.second; if (stk_peer->isWaitingForGame()) continue; + if (ServerConfig::m_ai_handling && stk_peer->isAIPeer()) + continue; for (auto& q : stk_peer->getPlayerProfiles()) players.push_back(q); } @@ -1623,6 +1632,8 @@ void STKHost::updatePlayers(unsigned* ingame, unsigned* waiting, auto& stk_peer = p.second; if (!stk_peer->isValidated()) continue; + if (ServerConfig::m_ai_handling && stk_peer->isAIPeer()) + continue; if (stk_peer->isWaitingForGame()) waiting_players += (uint32_t)stk_peer->getPlayerProfiles().size(); else diff --git a/src/network/stk_peer.hpp b/src/network/stk_peer.hpp index f1208fc27..aa9132861 100644 --- a/src/network/stk_peer.hpp +++ b/src/network/stk_peer.hpp @@ -140,7 +140,7 @@ public: void addPlayer(std::shared_ptr p) { m_players.push_back(p); } // ------------------------------------------------------------------------ - void setValidated() { m_validated.store(true); } + void setValidated(bool val) { m_validated.store(val); } // ------------------------------------------------------------------------ /** Returns if the client is validated by server. */ bool isValidated() const { return m_validated.load(); }