From 418901b0def908efb29f59f3156093bd68bcff62 Mon Sep 17 00:00:00 2001 From: "auria.mg" Date: Sun, 26 Aug 2018 21:07:02 -0400 Subject: [PATCH 01/42] Fix List widget rendering glitch --- src/guiengine/widgets/CGUISTKListBox.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/guiengine/widgets/CGUISTKListBox.cpp b/src/guiengine/widgets/CGUISTKListBox.cpp index f9c56dc4d..6847cc16e 100644 --- a/src/guiengine/widgets/CGUISTKListBox.cpp +++ b/src/guiengine/widgets/CGUISTKListBox.cpp @@ -573,7 +573,10 @@ void CGUISTKListBox::draw() } //Position back to inital pos if (IconBank && (Items[i].m_contents[x].m_icon > -1)) - textRect.UpperLeftCorner.X -= ItemsIconWidth+6; + textRect.UpperLeftCorner.X -= ItemsIconWidth; + + textRect.UpperLeftCorner.X -= 6; + //Calculate new beginning textRect.UpperLeftCorner.X += Items[i].m_contents[x].m_proportion * part_size; } From 817b57639960948773887373e323bb0a131225e8 Mon Sep 17 00:00:00 2001 From: "auria.mg" Date: Sun, 26 Aug 2018 21:07:56 -0400 Subject: [PATCH 02/42] Fix crash when leaving ghost replay screen --- src/guiengine/widgets/list_widget.cpp | 10 +++++++--- src/states_screens/ghost_replay_selection.cpp | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/guiengine/widgets/list_widget.cpp b/src/guiengine/widgets/list_widget.cpp index c942fcff6..bb93a651c 100644 --- a/src/guiengine/widgets/list_widget.cpp +++ b/src/guiengine/widgets/list_widget.cpp @@ -54,11 +54,11 @@ void ListWidget::setIcons(STKModifiedSpriteBank* icons, int size) m_use_icons = (icons != NULL); m_icons = icons; + CGUISTKListBox* list = getIrrlichtElement(); + assert(list != NULL); + if (m_use_icons) { - CGUISTKListBox* list = getIrrlichtElement(); - assert(list != NULL); - list->setSpriteBank(m_icons); // determine needed height @@ -83,6 +83,10 @@ void ListWidget::setIcons(STKModifiedSpriteBank* icons, int size) list->setItemHeight( item_height ); } } + else + { + list->setSpriteBank(NULL); + } } diff --git a/src/states_screens/ghost_replay_selection.cpp b/src/states_screens/ghost_replay_selection.cpp index 29b8ab033..770081413 100644 --- a/src/states_screens/ghost_replay_selection.cpp +++ b/src/states_screens/ghost_replay_selection.cpp @@ -50,6 +50,7 @@ GhostReplaySelection::~GhostReplaySelection() // ---------------------------------------------------------------------------- void GhostReplaySelection::tearDown() { + m_replay_list_widget->setIcons(NULL); delete m_icon_bank; m_icon_bank = NULL; } From e5925a53b7624ce4c2cf23cae5bf655ed3df8712 Mon Sep 17 00:00:00 2001 From: Benau Date: Mon, 27 Aug 2018 09:16:35 +0800 Subject: [PATCH 03/42] Use the network timer synchronizer to start game --- src/main_loop.cpp | 16 --- src/main_loop.hpp | 14 --- src/modes/world_status.cpp | 42 +------ src/modes/world_status.hpp | 8 -- src/network/network_timer_synchronizer.hpp | 35 +++++- src/network/protocols/client_lobby.cpp | 59 +++++----- src/network/protocols/lobby_protocol.cpp | 1 + src/network/protocols/lobby_protocol.hpp | 12 +- src/network/protocols/server_lobby.cpp | 128 ++++++++------------- src/network/protocols/server_lobby.hpp | 10 +- src/network/stk_host.cpp | 6 +- src/network/stk_host.hpp | 10 +- src/network/stk_peer.cpp | 6 +- src/network/stk_peer.hpp | 4 +- src/states_screens/race_gui.cpp | 4 +- src/states_screens/race_gui_base.cpp | 15 +++ src/states_screens/race_gui_base.hpp | 3 +- 17 files changed, 165 insertions(+), 208 deletions(-) diff --git a/src/main_loop.cpp b/src/main_loop.cpp index eed391f7e..ce5220865 100644 --- a/src/main_loop.cpp +++ b/src/main_loop.cpp @@ -66,8 +66,6 @@ LRESULT CALLBACK separateProcessProc(_In_ HWND hwnd, _In_ UINT uMsg, MainLoop::MainLoop(unsigned parent_pid) : m_abort(false), m_ticks_adjustment(0), m_parent_pid(parent_pid) { - m_network_timer.store(StkTime::getRealTimeMs()); - m_start_game_ticks.store(0); m_curr_time = 0; m_prev_time = 0; m_throttle_fps = true; @@ -495,18 +493,4 @@ void MainLoop::abort() m_abort = true; } // abort -//----------------------------------------------------------------------------- -/** Set game start ticks told by server somewhere in the future. - */ -void MainLoop::setStartNetworkGameTimer(uint64_t ticks) -{ - uint64_t ticks_now = getNetworkTimer(); - if (ticks < ticks_now) - { - Log::warn("MainLoop", "Network timer is too slow to catch up"); - ticks = ticks_now; - } - m_start_game_ticks.store(ticks); -} // setStartNetworkGameTimer - /* EOF */ diff --git a/src/main_loop.hpp b/src/main_loop.hpp index 48eda0e4c..f1ab747b7 100644 --- a/src/main_loop.hpp +++ b/src/main_loop.hpp @@ -21,7 +21,6 @@ #define HEADER_MAIN_LOOP_HPP #include "utils/synchronised.hpp" -#include "utils/time.hpp" #include "utils/types.hpp" #include @@ -40,8 +39,6 @@ private: Synchronised m_ticks_adjustment; - std::atomic m_network_timer, m_start_game_ticks; - uint32_t m_curr_time; uint32_t m_prev_time; unsigned m_parent_pid; @@ -65,17 +62,6 @@ public: m_ticks_adjustment.getData() += ticks; m_ticks_adjustment.unlock(); } - // ------------------------------------------------------------------------ - uint64_t getNetworkTimer() const - { return StkTime::getRealTimeMs() - m_network_timer.load(); } - // ------------------------------------------------------------------------ - void setNetworkTimer(uint64_t ticks) - { m_network_timer.store(StkTime::getRealTimeMs() - ticks); } - // ------------------------------------------------------------------------ - void resetStartNetworkGameTimer() { m_start_game_ticks.store(0); } - // ------------------------------------------------------------------------ - void setStartNetworkGameTimer(uint64_t ticks); - }; // MainLoop extern MainLoop* main_loop; diff --git a/src/modes/world_status.cpp b/src/modes/world_status.cpp index c7d6c9bd6..4768a6891 100644 --- a/src/modes/world_status.cpp +++ b/src/modes/world_status.cpp @@ -49,7 +49,6 @@ WorldStatus::WorldStatus() m_play_track_intro_sound = UserConfigParams::m_music; m_play_ready_set_go_sounds = true; m_play_racestart_sounds = true; - m_server_is_ready.store(false); IrrlichtDevice *device = irr_driver->getDevice(); @@ -96,10 +95,6 @@ void WorldStatus::reset() // Set the right music Track::getCurrentTrack()->startMusic(); - // In case of a networked race the race can only start once - // all protocols are up. This flag is used to wait for - // a confirmation before starting the actual race. - m_server_is_ready.store(false); } // reset //----------------------------------------------------------------------------- @@ -271,25 +266,13 @@ void WorldStatus::updateTime(int ticks) return; // Don't increase time case WAIT_FOR_SERVER_PHASE: { - // Wait for all players to finish loading world auto lobby = LobbyProtocol::get(); - assert(lobby); - if (!lobby->allPlayersReady()) - return; - // This stage is only reached in case of a networked game. - // The server waits for a confirmation from - // each client that they have started (to guarantee that the - // server is running with a local time behind all clients). - if (m_play_ready_set_go_sounds) - m_prestart_sound->play(); - - if (NetworkConfig::get()->isServer() && - m_server_is_ready.load() == false) return; - - m_phase = READY_PHASE; - auto cl = LobbyProtocol::get(); - if (cl) - cl->startingRaceNow(); + if (lobby && lobby->isRacing()) + { + if (m_play_ready_set_go_sounds) + m_prestart_sound->play(); + m_phase = READY_PHASE; + } return; // Don't increase time } case READY_PHASE: @@ -463,19 +446,6 @@ void WorldStatus::updateTime(int ticks) } // switch m_phase } // update -//----------------------------------------------------------------------------- -/** Called on the client when it receives a notification from the server that - * all clients (and server) are ready to start the race. The server will - * then additionally wait for all clients to report back that they are - * starting, which guarantees that the server is running far enough behind - * clients time that at server time T all events from the clients at time - * T have arrived, minimising rollback impact. - */ -void WorldStatus::startReadySetGo() -{ - m_server_is_ready.store(true); -} // startReadySetGo - //----------------------------------------------------------------------------- /** Sets the time for the clock. * \param time New time to set. diff --git a/src/modes/world_status.hpp b/src/modes/world_status.hpp index 4da315508..0147b7474 100644 --- a/src/modes/world_status.hpp +++ b/src/modes/world_status.hpp @@ -19,7 +19,6 @@ #define HEADER_WORLD_STATUS_HPP #include "utils/cpp2011.hpp" -#include class SFXBase; @@ -132,11 +131,6 @@ private: int m_count_up_ticks; bool m_engines_started; - /** In networked game a client must wait for the server to start 'ready - * set go' to make sure all client are actually ready to start the game. - * A server on the other hand will run behind all clients, so it will - * wait for all clients to indicate that they have started the race. */ - std::atomic_bool m_server_is_ready; void startEngines(); @@ -207,8 +201,6 @@ public: // ------------------------------------------------------------------------ /** Get the ticks since start regardless of which way the clock counts */ int getTicksSinceStart() const { return m_count_up_ticks; } - // ------------------------------------------------------------------------ - void setReadyToRace() { m_server_is_ready.store(true); } }; // WorldStatus diff --git a/src/network/network_timer_synchronizer.hpp b/src/network/network_timer_synchronizer.hpp index 09d9026b8..a20ab54b2 100644 --- a/src/network/network_timer_synchronizer.hpp +++ b/src/network/network_timer_synchronizer.hpp @@ -20,11 +20,12 @@ #define HEADER_NETWORK_TIMER_SYNCHRONIZER_HPP #include "config/user_config.hpp" +#include "network/stk_host.hpp" #include "utils/log.hpp" #include "utils/time.hpp" #include "utils/types.hpp" -#include "main_loop.hpp" +#include #include #include #include @@ -35,16 +36,37 @@ class NetworkTimerSynchronizer private: std::deque > m_times; - bool m_synchronised = false; + std::atomic_bool m_synchronised, m_force_set_timer; + public: + NetworkTimerSynchronizer() + { + m_synchronised.store(false); + m_force_set_timer.store(false); + } // ------------------------------------------------------------------------ - bool isSynchronised() const { return m_synchronised; } + bool isSynchronised() const { return m_synchronised.load(); } + // ------------------------------------------------------------------------ + void enableForceSetTimer() + { + if (m_synchronised.load() == true) + return; + m_force_set_timer.store(true); + } // ------------------------------------------------------------------------ void addAndSetTime(uint32_t ping, uint64_t server_time) { - if (m_synchronised) + if (m_synchronised.load() == true) return; + if (m_force_set_timer.load() == true) + { + m_force_set_timer.store(false); + m_synchronised.store(true); + STKHost::get()->setNetworkTimer(server_time + (uint64_t)(ping / 2)); + return; + } + const uint64_t cur_time = StkTime::getRealTimeMs(); // Take max 20 averaged samples from m_times, the next addAndGetTime // is used to determine that server_time if it's correct, if not @@ -64,8 +86,9 @@ public: if (std::abs(averaged_time - server_time_now) < UserConfigParams::m_timer_sync_tolerance) { - main_loop->setNetworkTimer(averaged_time); - m_synchronised = true; + STKHost::get()->setNetworkTimer(averaged_time); + m_force_set_timer.store(false); + m_synchronised.store(true); Log::info("NetworkTimerSynchronizer", "Network " "timer synchronized, difference: %dms", difference); return; diff --git a/src/network/protocols/client_lobby.cpp b/src/network/protocols/client_lobby.cpp index 9c6983bd6..3bd2ad95d 100644 --- a/src/network/protocols/client_lobby.cpp +++ b/src/network/protocols/client_lobby.cpp @@ -33,6 +33,7 @@ #include "network/game_setup.hpp" #include "network/network_config.hpp" #include "network/network_player_profile.hpp" +#include "network/network_timer_synchronizer.hpp" #include "network/protocols/game_protocol.hpp" #include "network/protocols/game_events_protocol.hpp" #include "network/race_event_manager.hpp" @@ -211,6 +212,18 @@ void ClientLobby::addAllPlayers(Event* event) STKHost::get()->requestShutdown(); return; } + // Timeout is too slow to synchronize, force it to stop and set current + // time + if (!STKHost::get()->getNetworkTimerSynchronizer()->isSynchronised()) + { + core::stringw msg = _("Bad network connection is detected."); + MessageQueue::add(MessageQueue::MT_ERROR, msg); + Log::warn("ClientLobby", "Failed to synchronize timer before game " + "start, maybe you enter the game too quick? (at least 5 seconds " + "are required for synchronization."); + STKHost::get()->getNetworkTimerSynchronizer()->enableForceSetTimer(); + } + NetworkString& data = event->data(); std::string track_name; data.decodeString(&track_name); @@ -737,37 +750,31 @@ void ClientLobby::connectionRefused(Event* event) //----------------------------------------------------------------------------- /*! \brief Called when the server broadcasts to start the race to all clients. - * \param event : Event providing the information (no additional informati - * in this case). + * \param event : Event providing the time the client should start game. */ void ClientLobby::startGame(Event* event) { - m_state.store(RACING); - // Triggers the world finite state machine to go from WAIT_FOR_SERVER_PHASE - // to READY_PHASE. - World::getWorld()->setReadyToRace(); - Log::info("ClientLobby", "Starting new game at %lf", - StkTime::getRealTime()); + uint64_t start_time = event->data().getUInt64(); + joinStartGameThread(); + m_start_game_thread = std::thread([start_time, this]() + { + const uint64_t cur_time = STKHost::get()->getNetworkTimer(); + if (!(start_time > cur_time)) + { + Log::warn("ClientLobby", "Network timer is too slow to catch " + "up, you must have a poor network."); + STKHost::get()->setErrorMessage( + m_disconnected_msg.at(PDI_BAD_CONNECTION)); + STKHost::get()->requestShutdown(); + return; + } + int sleep_time = (int)(start_time - cur_time); + Log::info("ClientLobby", "Start game after %dms", sleep_time); + std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time)); + m_state.store(RACING); + }); } // startGame -//----------------------------------------------------------------------------- -/** Called from WorldStatus when reaching the READY phase, i.e. when the race - * was started. It is going to inform the server of the race start. This - * allows the server to wait for all clients to start, so the server will - * be running behind the client with the biggest latency, which should - * make it likely that at local time T on the server all messages from - * all clients at their local time T have arrived. - */ -void ClientLobby::startingRaceNow() -{ - NetworkString* ns = getNetworkString(2); - ns->addUInt8(LE_STARTED_RACE); - sendToServer(ns, /*reliable*/true); - delete ns; - Log::verbose("ClientLobby", "StartingRaceNow at %lf", - StkTime::getRealTime()); -} // startingRaceNow - //----------------------------------------------------------------------------- /*! \brief Called when the kart selection starts. * \param event : Event providing the information (no additional information diff --git a/src/network/protocols/lobby_protocol.cpp b/src/network/protocols/lobby_protocol.cpp index 5ba3c93e8..a837a9f3f 100644 --- a/src/network/protocols/lobby_protocol.cpp +++ b/src/network/protocols/lobby_protocol.cpp @@ -45,6 +45,7 @@ LobbyProtocol::~LobbyProtocol() if (RaceEventManager::getInstance()) RaceEventManager::getInstance()->stop(); delete m_game_setup; + joinStartGameThread(); } // ~LobbyProtocol //----------------------------------------------------------------------------- diff --git a/src/network/protocols/lobby_protocol.hpp b/src/network/protocols/lobby_protocol.hpp index b5cccb53b..7a3333b16 100644 --- a/src/network/protocols/lobby_protocol.hpp +++ b/src/network/protocols/lobby_protocol.hpp @@ -26,6 +26,7 @@ class NetworkPlayerProfile; #include #include +#include #include /*! @@ -51,7 +52,6 @@ public: LE_CLIENT_LOADED_WORLD, // Client finished loading world LE_LOAD_WORLD, // Clients should load world LE_START_RACE, // Server to client to start race - LE_STARTED_RACE, // Client to server that it has started race LE_START_SELECTION, // inform client to start selection LE_RACE_FINISHED, // race has finished, display result LE_RACE_FINISHED_ACK, // client went back to lobby @@ -76,14 +76,22 @@ public: }; protected: + std::thread m_start_game_thread; + static std::weak_ptr m_lobby; /** Stores data about the online game to play. */ GameSetup* m_game_setup; + // ------------------------------------------------------------------------ void configRemoteKart( const std::vector >& players) const; - + // ------------------------------------------------------------------------ + void joinStartGameThread() + { + if (m_start_game_thread.joinable()) + m_start_game_thread.join(); + } public: /** Creates either a client or server lobby protocol as a singleton. */ diff --git a/src/network/protocols/server_lobby.cpp b/src/network/protocols/server_lobby.cpp index 67510f555..bb9de28ad 100644 --- a/src/network/protocols/server_lobby.cpp +++ b/src/network/protocols/server_lobby.cpp @@ -212,8 +212,6 @@ void ServerLobby::setup() // the server are ready: resetPeersReady(); m_peers_votes.clear(); - m_server_delay = std::numeric_limits::max(); - m_server_max_ping = std::numeric_limits::max(); m_timeout.store(std::numeric_limits::max()); m_waiting_for_reset = false; @@ -307,7 +305,6 @@ bool ServerLobby::notifyEventAsynchronous(Event* event) case LE_CONNECTION_REQUESTED: connectionRequested(event); break; case LE_KART_SELECTION: kartSelectionRequested(event); break; case LE_CLIENT_LOADED_WORLD: finishedLoadingWorldClient(event); break; - case LE_STARTED_RACE: startedRaceOnClient(event); break; case LE_VOTE: playerVote(event); break; case LE_KICK_HOST: kickHost(event); break; case LE_CHANGE_TEAM: changeTeam(event); break; @@ -439,47 +436,11 @@ void ServerLobby::asynchronousUpdate() return; if (!checkPeersReady()) return; - m_state = WAIT_FOR_RACE_STARTED; // Reset for next state usage resetPeersReady(); - signalRaceStartToClients(); - m_server_max_ping = StkTime::getRealTime() + - ((double)UserConfigParams::m_max_ping / 1000.0); + configPeersStartTime(); break; } - case WAIT_FOR_RACE_STARTED: - { - const bool ping_timed_out = - m_server_max_ping < StkTime::getRealTime(); - if (checkPeersReady() || ping_timed_out) - { - for (auto p : m_peers_ready) - { - auto cur_peer = p.first.lock(); - if (!cur_peer) - continue; - if (ping_timed_out && p.second.second > m_server_max_ping) - sendBadConnectionMessageToPeer(cur_peer); - } - m_state = DELAY_SERVER; - const double jt = - (double)UserConfigParams::m_jitter_tolerance / 1000.0; - m_server_delay = StkTime::getRealTime() + jt; - Log::verbose("ServerLobby", - "Started delay at %lf to %lf with jitter tolerance %lf.", - StkTime::getRealTime(), m_server_delay, jt); - } - break; - } - case DELAY_SERVER: - if (m_server_delay < StkTime::getRealTime()) - { - Log::verbose("ServerLobby", "End delay at %lf", - StkTime::getRealTime()); - m_state = RACING; - World::getWorld()->setReadyToRace(); - } - break; case SELECTING: { auto result = handleVote(); @@ -634,7 +595,6 @@ void ServerLobby::update(int ticks) case ACCEPTING_CLIENTS: case WAIT_FOR_WORLD_LOADED: case WAIT_FOR_RACE_STARTED: - case DELAY_SERVER: { // Waiting for asynchronousUpdate break; @@ -779,21 +739,6 @@ void ServerLobby::unregisterServer(bool now) } // unregisterServer -//----------------------------------------------------------------------------- -/** This function is called when all clients have loaded the world and - * are therefore ready to start the race. It signals to all clients - * to start the race and then switches state to DELAY_SERVER. - */ -void ServerLobby::signalRaceStartToClients() -{ - Log::verbose("Server", "Signaling race start to clients at %lf", - StkTime::getRealTime()); - NetworkString *ns = getNetworkString(1); - ns->addUInt8(LE_START_RACE); - sendMessageToPeers(ns, /*reliable*/true); - delete ns; -} // startGame - //----------------------------------------------------------------------------- /** Instructs all clients to start the kart selection. If event is NULL, * the command comes from the owner less server. @@ -1907,28 +1852,6 @@ void ServerLobby::finishedLoadingWorldClient(Event *event) peer->getHostId(), StkTime::getRealTime()); } // finishedLoadingWorldClient -//----------------------------------------------------------------------------- -/** Called when a notification from a client is received that the client has - * started the race. Once all clients have informed the server that they - * have started the race, the server can start. This makes sure that the - * server's local time is behind all clients by at most max ping, - * which in turn means that when the server simulates local time T all - * messages from clients at their local time T should have arrived at - * the server, which creates smoother play experience. - */ -void ServerLobby::startedRaceOnClient(Event *event) -{ - if (m_state.load() != WAIT_FOR_RACE_STARTED) - { - sendBadConnectionMessageToPeer(event->getPeerSP()); - return; - } - std::shared_ptr peer = event->getPeerSP(); - m_peers_ready.at(peer) = std::make_pair(true, StkTime::getRealTime()); - Log::info("ServerLobby", "Peer %s has started race at %lf", - peer->getAddress().toString().c_str(), StkTime::getRealTime()); -} // startedRaceOnClient - //----------------------------------------------------------------------------- /** Called when a client clicks on 'ok' on the race result screen. * If all players have clicked on 'ok', go back to the lobby. @@ -2114,3 +2037,52 @@ void ServerLobby::submitRankingsToAddons() 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 = UserConfigParams::m_max_ping; + for (auto p : m_peers_ready) + { + auto peer = p.first.lock(); + if (!peer) + continue; + if (peer->getAveragePing() > max_ping_from_peers) + { + sendBadConnectionMessageToPeer(peer); + continue; + } + max_ping = std::max(peer->getAveragePing(), max_ping); + } + // Start up time will be after 2000ms, 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)2000; + NetworkString* ns = getNetworkString(10); + ns->addUInt8(LE_START_RACE).addUInt64(start_time); + sendMessageToPeers(ns, /*reliable*/true); + + const unsigned jitter_tolerance = UserConfigParams::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. + start_time += (uint64_t)(max_ping / 2) + (uint64_t)jitter_tolerance; + delete ns; + m_state = WAIT_FOR_RACE_STARTED; + + joinStartGameThread(); + m_start_game_thread = std::thread([start_time, this]() + { + const uint64_t cur_time = STKHost::get()->getNetworkTimer(); + assert(start_time > cur_time); + int sleep_time = (int)(start_time - cur_time); + Log::info("ServerLobby", "Start game after %dms", sleep_time); + std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time)); + m_state.store(RACING); + }); +} // configPeersStartTime diff --git a/src/network/protocols/server_lobby.hpp b/src/network/protocols/server_lobby.hpp index ca0f0318f..8c1827081 100644 --- a/src/network/protocols/server_lobby.hpp +++ b/src/network/protocols/server_lobby.hpp @@ -51,7 +51,6 @@ public: LOAD_WORLD, // Server starts loading world WAIT_FOR_WORLD_LOADED, // Wait for clients and server to load world WAIT_FOR_RACE_STARTED, // Wait for all clients to have started the race - DELAY_SERVER, // Additional server delay RACING, // racing WAIT_FOR_RACE_STOPPED, // Wait server for stopping all race protocols RESULT_DISPLAY, // Show result screen @@ -90,12 +89,6 @@ private: std::map, std::tuple, std::owner_less > > m_peers_votes; - /** Keeps track of an artificial server delay, which is used to compensate - * for network jitter. */ - double m_server_delay; - - double m_server_max_ping; - bool m_has_created_server_id_file; /** It indicates if this server is unregistered with the stk server. */ @@ -153,7 +146,6 @@ private: void playerFinishedResult(Event *event); bool registerServer(); void finishedLoadingWorldClient(Event *event); - void startedRaceOnClient(Event *event); void kickHost(Event* event); void changeTeam(Event* event); void handleChat(Event* event); @@ -220,6 +212,7 @@ private: void checkRaceFinished(); void sendBadConnectionMessageToPeer(std::shared_ptr p); std::pair getHitCaptureLimit(float num_karts); + void configPeersStartTime(); public: ServerLobby(); virtual ~ServerLobby(); @@ -230,7 +223,6 @@ public: virtual void update(int ticks) OVERRIDE; virtual void asynchronousUpdate() OVERRIDE; - void signalRaceStartToClients(); void startSelection(const Event *event=NULL); void checkIncomingConnectionRequests(); void finishedLoadingWorld() OVERRIDE; diff --git a/src/network/stk_host.cpp b/src/network/stk_host.cpp index d6582a236..3a29982ad 100644 --- a/src/network/stk_host.cpp +++ b/src/network/stk_host.cpp @@ -35,7 +35,6 @@ #include "utils/separate_process.hpp" #include "utils/time.hpp" #include "utils/vs.hpp" -#include "main_loop.hpp" #include #if defined(WIN32) @@ -304,12 +303,12 @@ STKHost::STKHost(bool server) */ void STKHost::init() { + m_network_timer.store(StkTime::getRealTimeMs()); m_shutdown = false; m_authorised = false; m_network = NULL; m_exit_timeout.store(std::numeric_limits::max()); m_client_ping.store(0); - main_loop->resetStartNetworkGameTimer(); // Start with initialising ENet // ============================ @@ -337,7 +336,6 @@ void STKHost::init() */ STKHost::~STKHost() { - main_loop->resetStartNetworkGameTimer(); requestShutdown(); if (m_network_console.joinable()) m_network_console.join(); @@ -779,7 +777,7 @@ void STKHost::mainLoop() if (need_ping) { BareNetworkString ping_packet; - uint64_t network_timer = main_loop->getNetworkTimer(); + uint64_t network_timer = getNetworkTimer(); ping_packet.addUInt64(network_timer); ping_packet.addUInt8((uint8_t)m_peer_pings.getData().size()); for (auto& p : m_peer_pings.getData()) diff --git a/src/network/stk_host.hpp b/src/network/stk_host.hpp index 65121d89a..5e06da5ee 100644 --- a/src/network/stk_host.hpp +++ b/src/network/stk_host.hpp @@ -26,6 +26,7 @@ #include "network/network_string.hpp" #include "network/transport_address.hpp" #include "utils/synchronised.hpp" +#include "utils/time.hpp" #include "irrString.h" @@ -138,6 +139,8 @@ private: std::atomic m_client_ping; + std::atomic m_network_timer; + std::unique_ptr m_nts; // ------------------------------------------------------------------------ @@ -318,7 +321,12 @@ public: // ------------------------------------------------------------------------ NetworkTimerSynchronizer* getNetworkTimerSynchronizer() const { return m_nts.get(); } - + // ------------------------------------------------------------------------ + uint64_t getNetworkTimer() const + { return StkTime::getRealTimeMs() - m_network_timer.load(); } + // ------------------------------------------------------------------------ + void setNetworkTimer(uint64_t ticks) + { m_network_timer.store(StkTime::getRealTimeMs() - ticks); } }; // class STKHost #endif // STK_HOST_HPP diff --git a/src/network/stk_peer.cpp b/src/network/stk_peer.cpp index 03e0df822..fbc9b96ac 100644 --- a/src/network/stk_peer.cpp +++ b/src/network/stk_peer.cpp @@ -39,7 +39,7 @@ STKPeer::STKPeer(ENetPeer *enet_peer, STKHost* host, uint32_t host_id) m_host_id = host_id; m_connected_time = (float)StkTime::getRealTime(); m_validated.store(false); - m_average_ping = 0; + m_average_ping.store(0); } // STKPeer //----------------------------------------------------------------------------- @@ -165,9 +165,9 @@ uint32_t STKPeer::getPing() while (m_previous_pings.size() > ap) { m_previous_pings.pop_front(); - m_average_ping = + m_average_ping.store( (uint32_t)(std::accumulate(m_previous_pings.begin(), - m_previous_pings.end(), 0) / m_previous_pings.size()); + m_previous_pings.end(), 0) / m_previous_pings.size())); } } return m_enet_peer->roundTripTime; diff --git a/src/network/stk_peer.hpp b/src/network/stk_peer.hpp index 2dfd32564..ed327694f 100644 --- a/src/network/stk_peer.hpp +++ b/src/network/stk_peer.hpp @@ -81,7 +81,7 @@ protected: std::deque m_previous_pings; - uint32_t m_average_ping; + std::atomic m_average_ping; public: STKPeer(ENetPeer *enet_peer, STKHost* host, uint32_t host_id); @@ -165,7 +165,7 @@ public: // ------------------------------------------------------------------------ void setCrypto(std::unique_ptr&& c); // ------------------------------------------------------------------------ - uint32_t getAveragePing() const { return m_average_ping; } + uint32_t getAveragePing() const { return m_average_ping.load(); } // ------------------------------------------------------------------------ ENetPeer* getENetPeer() const { return m_enet_peer; } }; // STKPeer diff --git a/src/states_screens/race_gui.cpp b/src/states_screens/race_gui.cpp index 7e5147663..96ad3bb5e 100644 --- a/src/states_screens/race_gui.cpp +++ b/src/states_screens/race_gui.cpp @@ -230,13 +230,13 @@ void RaceGUI::renderGlobal(float dt) World *world = World::getWorld(); assert(world != NULL); - if(world->getPhase() >= WorldStatus::READY_PHASE && + if(world->getPhase() >= WorldStatus::WAIT_FOR_SERVER_PHASE && world->getPhase() <= WorldStatus::GO_PHASE ) { drawGlobalReadySetGo(); } if(world->getPhase() == World::GOAL_PHASE) - drawGlobalGoal(); + drawGlobalGoal(); // MiniMap is drawn when the players wait for the start countdown to end drawGlobalMiniMap(); diff --git a/src/states_screens/race_gui_base.cpp b/src/states_screens/race_gui_base.cpp index 7df7b783d..4a2823763 100644 --- a/src/states_screens/race_gui_base.cpp +++ b/src/states_screens/race_gui_base.cpp @@ -70,6 +70,9 @@ RaceGUIBase::RaceGUIBase() m_string_go = _("Go!"); //I18N: Shown when a goal is scored m_string_goal = _("GOAL!"); + // I18N: Shown waiting for other players in network to finish loading or + // waiting + m_string_waiting_for_others = _("Waiting for others"); m_music_icon = irr_driver->getTexture("notes.png"); if (!m_music_icon) @@ -626,6 +629,18 @@ void RaceGUIBase::drawGlobalReadySetGo() { switch (World::getWorld()->getPhase()) { + case WorldStatus::WAIT_FOR_SERVER_PHASE: + { + static video::SColor color = video::SColor(255, 255, 255, 255); + core::rect pos(irr_driver->getActualScreenSize().Width>>1, + irr_driver->getActualScreenSize().Height>>1, + irr_driver->getActualScreenSize().Width>>1, + irr_driver->getActualScreenSize().Height>>1); + gui::IGUIFont* font = GUIEngine::getTitleFont(); + font->draw(StringUtils::loadingDots( + m_string_waiting_for_others.c_str()), pos, color, true, true); + } + break; case WorldStatus::READY_PHASE: { static video::SColor color = video::SColor(255, 255, 255, 255); diff --git a/src/states_screens/race_gui_base.hpp b/src/states_screens/race_gui_base.hpp index 95b1dfbd2..1a09ebdeb 100644 --- a/src/states_screens/race_gui_base.hpp +++ b/src/states_screens/race_gui_base.hpp @@ -133,7 +133,8 @@ private: video::ITexture* m_plunger_face; /** Translated strings 'ready', 'set', 'go'. */ - core::stringw m_string_ready, m_string_set, m_string_go, m_string_goal; + core::stringw m_string_ready, m_string_set, m_string_go, m_string_goal, + m_string_waiting_for_others; /** The position of the referee for all karts. */ std::vector m_referee_pos; From 0f39add43262cfd701bc9edf162d3c54d2daf22a Mon Sep 17 00:00:00 2001 From: Benau Date: Mon, 27 Aug 2018 13:49:11 +0800 Subject: [PATCH 04/42] Convert time-related code in network to 64bit to avoid overflow --- src/main_loop.cpp | 11 ++- src/main_loop.hpp | 4 +- src/network/protocols/client_lobby.cpp | 1 + src/network/protocols/connect_to_peer.cpp | 4 +- src/network/protocols/connect_to_peer.hpp | 2 +- src/network/protocols/connect_to_server.cpp | 14 ++-- src/network/protocols/connect_to_server.hpp | 2 +- src/network/protocols/request_connection.cpp | 4 +- src/network/protocols/server_lobby.cpp | 70 +++++++++++-------- src/network/protocols/server_lobby.hpp | 9 ++- src/network/servers_manager.cpp | 14 ++-- src/network/servers_manager.hpp | 2 +- src/network/stk_host.cpp | 41 ++++++----- src/network/stk_host.hpp | 2 +- src/network/stk_peer.cpp | 6 +- src/network/stk_peer.hpp | 6 +- .../dialogs/player_rankings_dialog.cpp | 6 +- src/states_screens/tracks_screen.cpp | 10 +-- src/states_screens/tracks_screen.hpp | 5 +- 19 files changed, 110 insertions(+), 103 deletions(-) diff --git a/src/main_loop.cpp b/src/main_loop.cpp index ce5220865..73238eaa2 100644 --- a/src/main_loop.cpp +++ b/src/main_loop.cpp @@ -42,6 +42,7 @@ #include "race/race_manager.hpp" #include "states_screens/state_manager.hpp" #include "utils/profiler.hpp" +#include "utils/time.hpp" #ifndef WIN32 #include @@ -111,11 +112,9 @@ float MainLoop::getLimitedDt() return 1.0f/60.0f; } - IrrlichtDevice* device = irr_driver->getDevice(); - while( 1 ) { - m_curr_time = device->getTimer()->getRealTime(); + m_curr_time = StkTime::getRealTimeMs(); dt = (float)(m_curr_time - m_prev_time); // On a server (i.e. without graphics) the frame rate can be under // 1 ms, i.e. dt = 0. Additionally, the resolution of a sleep @@ -131,7 +130,7 @@ float MainLoop::getLimitedDt() while (dt <= 0 && !ProfileWorld::isProfileMode()) { StkTime::sleep(1); - m_curr_time = device->getTimer()->getRealTime(); + m_curr_time = StkTime::getRealTimeMs(); dt = (float)(m_curr_time - m_prev_time); } @@ -279,9 +278,7 @@ void MainLoop::updateRace(int ticks) */ void MainLoop::run() { - IrrlichtDevice* device = irr_driver->getDevice(); - - m_curr_time = device->getTimer()->getRealTime(); + m_curr_time = StkTime::getRealTimeMs(); // DT keeps track of the leftover time, since the race update // happens in fixed timesteps float left_over_time = 0; diff --git a/src/main_loop.hpp b/src/main_loop.hpp index f1ab747b7..efa4c332e 100644 --- a/src/main_loop.hpp +++ b/src/main_loop.hpp @@ -39,8 +39,8 @@ private: Synchronised m_ticks_adjustment; - uint32_t m_curr_time; - uint32_t m_prev_time; + uint64_t m_curr_time; + uint64_t m_prev_time; unsigned m_parent_pid; float getLimitedDt(); void updateRace(int ticks); diff --git a/src/network/protocols/client_lobby.cpp b/src/network/protocols/client_lobby.cpp index 3bd2ad95d..94d4f7dad 100644 --- a/src/network/protocols/client_lobby.cpp +++ b/src/network/protocols/client_lobby.cpp @@ -771,6 +771,7 @@ void ClientLobby::startGame(Event* event) int sleep_time = (int)(start_time - cur_time); Log::info("ClientLobby", "Start game after %dms", sleep_time); std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time)); + Log::info("ClientLobby", "Started at %lf", StkTime::getRealTime()); m_state.store(RACING); }); } // startGame diff --git a/src/network/protocols/connect_to_peer.cpp b/src/network/protocols/connect_to_peer.cpp index 9e0ccd206..d4dbe72c0 100644 --- a/src/network/protocols/connect_to_peer.cpp +++ b/src/network/protocols/connect_to_peer.cpp @@ -55,9 +55,9 @@ void ConnectToPeer::asynchronousUpdate() break; } // Each 2 second for a ping or broadcast - if (StkTime::getRealTime() > m_timer + 2.0) + if (StkTime::getRealTimeMs() > m_timer + 2000) { - m_timer = StkTime::getRealTime(); + m_timer = StkTime::getRealTimeMs(); // Send a broadcast packet with the string aloha_stk inside, // the client will know our ip address and will connect // The wan remote should already start its ping message to us now diff --git a/src/network/protocols/connect_to_peer.hpp b/src/network/protocols/connect_to_peer.hpp index d9318f681..48b3c514b 100644 --- a/src/network/protocols/connect_to_peer.hpp +++ b/src/network/protocols/connect_to_peer.hpp @@ -33,7 +33,7 @@ protected: TransportAddress m_peer_address; /** Timer use for tracking broadcast. */ - double m_timer = 0.0; + uint64_t m_timer = 0; /** If greater than a certain value, terminate this protocol. */ unsigned m_tried_connection = 0; diff --git a/src/network/protocols/connect_to_server.cpp b/src/network/protocols/connect_to_server.cpp index 50fcfe59b..643ca02ec 100644 --- a/src/network/protocols/connect_to_server.cpp +++ b/src/network/protocols/connect_to_server.cpp @@ -141,7 +141,7 @@ void ConnectToServer::asynchronousUpdate() request_connection->requestStart(); m_current_protocol = request_connection; // Reset timer for next usage - m_timer = 0.0; + m_timer = 0; break; } case REQUESTING_CONNECTION: @@ -169,7 +169,7 @@ void ConnectToServer::asynchronousUpdate() " aloha, trying to connect anyway."); m_state = CONNECTING; // Reset timer for next usage - m_timer = 0.0; + m_timer = 0; m_tried_connection = 0; } else @@ -190,9 +190,9 @@ void ConnectToServer::asynchronousUpdate() { // Send a 1-byte datagram, the remote host can simply ignore // this datagram, to keep the port open (2 second each) - if (StkTime::getRealTime() > m_timer + 2.0) + if (StkTime::getRealTimeMs() > m_timer + 2000) { - m_timer = StkTime::getRealTime(); + m_timer = StkTime::getRealTimeMs(); BareNetworkString data; data.addUInt8(0); STKHost::get()->sendRawPacket(data, m_server_address); @@ -204,9 +204,9 @@ void ConnectToServer::asynchronousUpdate() case CONNECTING: // waiting the server to answer our connection { // Every 5 seconds - if (StkTime::getRealTime() > m_timer + 5.0) + if (StkTime::getRealTimeMs() > m_timer + 5000) { - m_timer = StkTime::getRealTime(); + m_timer = StkTime::getRealTimeMs(); STKHost::get()->stopListening(); STKHost::get()->connect(m_server_address); STKHost::get()->startListening(); @@ -420,7 +420,7 @@ void ConnectToServer::waitingAloha(bool is_wan) m_server_address = sender; m_state = CONNECTING; // Reset timer for next usage - m_timer = 0.0; + m_timer = 0; m_tried_connection = 0; } } // waitingAloha diff --git a/src/network/protocols/connect_to_server.hpp b/src/network/protocols/connect_to_server.hpp index 05b52d24d..c4b5e63f6 100644 --- a/src/network/protocols/connect_to_server.hpp +++ b/src/network/protocols/connect_to_server.hpp @@ -31,7 +31,7 @@ class Server; class ConnectToServer : public Protocol { private: - double m_timer = 0.0; + uint64_t m_timer = 0; TransportAddress m_server_address; std::shared_ptr m_server; unsigned m_tried_connection = 0; diff --git a/src/network/protocols/request_connection.cpp b/src/network/protocols/request_connection.cpp index 00c88d16d..5d4f96814 100644 --- a/src/network/protocols/request_connection.cpp +++ b/src/network/protocols/request_connection.cpp @@ -77,8 +77,8 @@ void RequestConnection::asynchronousUpdate() { // Allow up to 10 seconds for the separate process to // fully start-up - double timeout = StkTime::getRealTime() + 10.; - while (StkTime::getRealTime() < timeout) + uint64_t timeout = StkTime::getRealTimeMs() + 10000; + while (StkTime::getRealTimeMs() < timeout) { const std::string& sid = NetworkConfig::get() ->getServerIdFile(); diff --git a/src/network/protocols/server_lobby.cpp b/src/network/protocols/server_lobby.cpp index bb9de28ad..8c45a8623 100644 --- a/src/network/protocols/server_lobby.cpp +++ b/src/network/protocols/server_lobby.cpp @@ -212,7 +212,7 @@ void ServerLobby::setup() // the server are ready: resetPeersReady(); m_peers_votes.clear(); - m_timeout.store(std::numeric_limits::max()); + m_timeout.store(std::numeric_limits::max()); m_waiting_for_reset = false; Log::info("ServerLobby", "Reset server to initial state."); @@ -397,19 +397,20 @@ void ServerLobby::asynchronousUpdate() (float)NetworkConfig::get()->getMaxPlayers() * UserConfigParams::m_start_game_threshold || m_game_setup->isGrandPrixStarted()) && - m_timeout.load() == std::numeric_limits::max()) + m_timeout.load() == std::numeric_limits::max()) { - m_timeout.store((float)StkTime::getRealTime() + - UserConfigParams::m_start_game_counter); + m_timeout.store((int64_t)StkTime::getRealTimeMs() + + (int64_t) + (UserConfigParams::m_start_game_counter * 1000.0f)); } else if ((float)players.size() < (float)NetworkConfig::get()->getMaxPlayers() * UserConfigParams::m_start_game_threshold && !m_game_setup->isGrandPrixStarted()) { - m_timeout.store(std::numeric_limits::max()); + m_timeout.store(std::numeric_limits::max()); } - if (m_timeout.load() < (float)StkTime::getRealTime()) + if (m_timeout.load() < (int64_t)StkTime::getRealTimeMs()) { startSelection(); return; @@ -444,10 +445,11 @@ void ServerLobby::asynchronousUpdate() case SELECTING: { auto result = handleVote(); - if (m_timeout.load() < (float)StkTime::getRealTime() || + if (m_timeout.load() < (int64_t)StkTime::getRealTimeMs() || (std::get<3>(result) && - m_timeout.load() - (UserConfigParams::m_voting_timeout / 2.0f) < - (float)StkTime::getRealTime())) + m_timeout.load() - + (int64_t)(UserConfigParams::m_voting_timeout / 2.0f * 1000.0f) < + (int64_t)StkTime::getRealTimeMs())) { m_game_setup->setRace(std::get<0>(result), std::get<1>(result), std::get<2>(result)); @@ -509,9 +511,8 @@ void ServerLobby::asynchronousUpdate() void ServerLobby::sendBadConnectionMessageToPeer(std::shared_ptr p) { const unsigned max_ping = UserConfigParams::m_max_ping; - Log::warn("ServerLobby", "Peer %s cannot catch up with max ping %d, it" - " started at %lf.", p->getAddress().toString().c_str(), max_ping, - StkTime::getRealTime()); + Log::warn("ServerLobby", "Peer %s cannot catch up with max ping %d.", + p->getAddress().toString().c_str(), max_ping); NetworkString* msg = getNetworkString(); msg->setSynchronous(true); msg->addUInt8(LE_BAD_CONNECTION); @@ -627,14 +628,14 @@ void ServerLobby::update(int ticks) 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((float)StkTime::getRealTime() + 15.0f); + m_timeout.store((int64_t)StkTime::getRealTimeMs() + 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() || - StkTime::getRealTime() > m_timeout.load()) + (int64_t)StkTime::getRealTimeMs() > m_timeout.load()) { // Send a notification to all clients to exit // the race result screen @@ -866,7 +867,7 @@ void ServerLobby::startSelection(const Event *event) ul.unlock(); // Will be changed after the first vote received - m_timeout.store(std::numeric_limits::max()); + m_timeout.store(std::numeric_limits::max()); } // startSelection //----------------------------------------------------------------------------- @@ -876,9 +877,9 @@ void ServerLobby::startSelection(const Event *event) void ServerLobby::checkIncomingConnectionRequests() { // First poll every 5 seconds. Return if no polling needs to be done. - const float POLL_INTERVAL = 5.0f; - static double last_poll_time = 0; - if (StkTime::getRealTime() < last_poll_time + POLL_INTERVAL) + const uint64_t POLL_INTERVAL = 5000; + static uint64_t last_poll_time = 0; + if (StkTime::getRealTimeMs() < last_poll_time + POLL_INTERVAL) return; // Keep the port open, it can be sent to anywhere as we will send to the @@ -891,7 +892,7 @@ void ServerLobby::checkIncomingConnectionRequests() } // Now poll the stk server - last_poll_time = StkTime::getRealTime(); + last_poll_time = StkTime::getRealTimeMs(); // ======================================================================== class PollServerRequest : public Online::XMLRequest @@ -1509,15 +1510,20 @@ void ServerLobby::handleUnencryptedConnection(std::shared_ptr peer, NetworkString* message_ack = getNetworkString(4); message_ack->setSynchronous(true); // connection success -- return the host id of peer - float auto_start_timer = m_timeout.load(); + 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::getRealTimeMs()) / 1000.0f; + } message_ack->addUInt8(LE_CONNECTION_ACCEPTED).addUInt32(peer->getHostId()) - .addUInt32(NetworkConfig::m_server_version) - .addFloat(auto_start_timer == std::numeric_limits::max() ? - auto_start_timer : auto_start_timer - (float)StkTime::getRealTime()); + .addUInt32(NetworkConfig::m_server_version).addFloat(auto_start_timer); peer->sendPacket(message_ack); delete message_ack; - m_peers_ready[peer] = std::make_pair(false, 0.0); + m_peers_ready[peer] = false; for (std::shared_ptr npp : peer->getPlayerProfiles()) { m_game_setup->addPlayer(npp); @@ -1658,13 +1664,14 @@ void ServerLobby::playerVote(Event* event) return; // Check if first vote, if so start counter - if (m_timeout.load() == std::numeric_limits::max()) + if (m_timeout.load() == std::numeric_limits::max()) { - m_timeout.store((float)StkTime::getRealTime() + - UserConfigParams::m_voting_timeout); + m_timeout.store((int64_t)StkTime::getRealTimeMs() + + (int64_t)(UserConfigParams::m_voting_timeout * 1000.0f)); } - float remaining_time = m_timeout.load() - (float)StkTime::getRealTime(); - if (remaining_time < 0.0f) + int64_t remaining_time = + m_timeout.load() - (int64_t)StkTime::getRealTimeMs(); + if (remaining_time < 0) { return; } @@ -1847,7 +1854,7 @@ void ServerLobby::finishedLoadingWorld() void ServerLobby::finishedLoadingWorldClient(Event *event) { std::shared_ptr peer = event->getPeerSP(); - m_peers_ready.at(peer) = std::make_pair(true, StkTime::getRealTime()); + m_peers_ready.at(peer) = true; Log::info("ServerLobby", "Peer %d has finished loading world at %lf", peer->getHostId(), StkTime::getRealTime()); } // finishedLoadingWorldClient @@ -1861,7 +1868,7 @@ void ServerLobby::playerFinishedResult(Event *event) if (m_state.load() != RESULT_DISPLAY) return; std::shared_ptr peer = event->getPeerSP(); - m_peers_ready.at(peer) = std::make_pair(true, StkTime::getRealTime()); + m_peers_ready.at(peer) = true; } // playerFinishedResult //----------------------------------------------------------------------------- @@ -2083,6 +2090,7 @@ void ServerLobby::configPeersStartTime() int sleep_time = (int)(start_time - cur_time); Log::info("ServerLobby", "Start game after %dms", sleep_time); std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time)); + Log::info("ServerLobby", "Started at %lf", StkTime::getRealTime()); m_state.store(RACING); }); } // configPeersStartTime diff --git a/src/network/protocols/server_lobby.hpp b/src/network/protocols/server_lobby.hpp index 8c1827081..7eec95e6a 100644 --- a/src/network/protocols/server_lobby.hpp +++ b/src/network/protocols/server_lobby.hpp @@ -82,7 +82,7 @@ private: std::atomic_bool m_server_has_loaded_world; /** Counts how many peers have finished loading the world. */ - std::map, std::pair, + std::map, bool, std::owner_less > > m_peers_ready; /** Vote from each peer. */ @@ -95,7 +95,7 @@ private: std::weak_ptr m_server_unregistered; /** Timeout counter for various state. */ - std::atomic m_timeout; + std::atomic m_timeout; /** Lock this mutex whenever a client is connect / disconnect or * starting race. */ @@ -160,7 +160,7 @@ private: { if (p.first.expired()) continue; - all_ready = all_ready && p.second.first; + all_ready = all_ready && p.second; if (!all_ready) return false; } @@ -176,8 +176,7 @@ private: } else { - it->second.first = false; - it->second.second = 0.0; + it->second = false; it++; } } diff --git a/src/network/servers_manager.cpp b/src/network/servers_manager.cpp index e4e2ff51e..0078e9a2e 100644 --- a/src/network/servers_manager.cpp +++ b/src/network/servers_manager.cpp @@ -41,7 +41,7 @@ # include #endif -#define SERVER_REFRESH_INTERVAL 5.0f +const uint64_t SERVER_REFRESH_INTERVAL = 5000; static ServersManager* g_manager_singleton(NULL); @@ -64,7 +64,7 @@ void ServersManager::deallocate() // ---------------------------------------------------------------------------- ServersManager::ServersManager() { - m_last_load_time.store(0.0f); + m_last_load_time.store(0); m_list_updated = false; } // ServersManager @@ -158,8 +158,8 @@ Online::XMLRequest* ServersManager::getLANRefreshRequest() const char buffer[LEN]; // Wait for up to 0.5 seconds to receive an answer from // any local servers. - double start_time = StkTime::getRealTime(); - const double DURATION = 1.0; + uint64_t start_time = StkTime::getRealTimeMs(); + const uint64_t DURATION = 1000; const auto& servers = ServersManager::get()->getServers(); int cur_server_id = (int)servers.size(); assert(cur_server_id == 0); @@ -169,7 +169,7 @@ Online::XMLRequest* ServersManager::getLANRefreshRequest() const // because e.g. a local client would answer as 127.0.0.1 and // 192.168.**. std::map > servers_now; - while (StkTime::getRealTime() - start_time < DURATION) + while (StkTime::getRealTimeMs() - start_time < DURATION) { TransportAddress sender; int len = broadcast->receiveRawPacket(buffer, LEN, &sender, 1); @@ -236,7 +236,7 @@ void ServersManager::setLanServers(const std::map(*servers_xml->getNode(i))); } - m_last_load_time.store((float)StkTime::getRealTime()); + m_last_load_time.store(StkTime::getRealTimeMs()); m_list_updated = true; } // refresh diff --git a/src/network/servers_manager.hpp b/src/network/servers_manager.hpp index 40f4a7179..fec48ee9e 100644 --- a/src/network/servers_manager.hpp +++ b/src/network/servers_manager.hpp @@ -45,7 +45,7 @@ private: /** List of broadcast addresses to use. */ std::vector m_broadcast_address; - std::atomic m_last_load_time; + std::atomic m_last_load_time; std::atomic_bool m_list_updated; // ------------------------------------------------------------------------ diff --git a/src/network/stk_host.cpp b/src/network/stk_host.cpp index 3a29982ad..29111f7bd 100644 --- a/src/network/stk_host.cpp +++ b/src/network/stk_host.cpp @@ -307,7 +307,7 @@ void STKHost::init() m_shutdown = false; m_authorised = false; m_network = NULL; - m_exit_timeout.store(std::numeric_limits::max()); + m_exit_timeout.store(std::numeric_limits::max()); m_client_ping.store(0); // Start with initialising ENet @@ -440,7 +440,7 @@ void STKHost::setPublicAddress() } m_network->sendRawPacket(s, m_stun_address); - double ping = StkTime::getRealTime(); + uint64_t ping = StkTime::getRealTimeMs(); freeaddrinfo(res); // Recieve now @@ -448,7 +448,7 @@ void STKHost::setPublicAddress() const int LEN = 2048; char buffer[LEN]; int len = m_network->receiveRawPacket(buffer, LEN, &sender, 2000); - ping = StkTime::getRealTime() - ping; + ping = StkTime::getRealTimeMs() - ping; if (sender.getIP() != m_stun_address.getIP()) { @@ -587,8 +587,7 @@ void STKHost::setPublicAddress() m_public_address = non_xor_addr; } // Succeed, save ping - UserConfigParams::m_stun_list[server_name] = - (uint32_t)(ping * 1000.0); + UserConfigParams::m_stun_list[server_name] = (uint32_t)(ping); untried_server.clear(); } } @@ -620,7 +619,7 @@ void STKHost::disconnectAllPeers(bool timeout_waiting) for (auto peer : m_peers) peer.second->disconnect(); // Wait for at most 2 seconds for disconnect event to be generated - m_exit_timeout.store(StkTime::getRealTime() + 2.0); + m_exit_timeout.store(StkTime::getRealTimeMs() + 2000); } m_peers.clear(); } // disconnectAllPeers @@ -667,7 +666,7 @@ bool STKHost::connect(const TransportAddress& address) */ void STKHost::startListening() { - m_exit_timeout.store(std::numeric_limits::max()); + m_exit_timeout.store(std::numeric_limits::max()); m_listening_thread = std::thread(std::bind(&STKHost::mainLoop, this)); } // startListening @@ -677,7 +676,7 @@ void STKHost::startListening() */ void STKHost::stopListening() { - if (m_exit_timeout.load() == std::numeric_limits::max()) + if (m_exit_timeout.load() == std::numeric_limits::max()) m_exit_timeout.store(0.0); if (m_listening_thread.joinable()) m_listening_thread.join(); @@ -716,9 +715,9 @@ void STKHost::mainLoop() } } - double last_ping_time = StkTime::getRealTime(); - double last_ping_time_update_for_client = StkTime::getRealTime(); - while (m_exit_timeout.load() > StkTime::getRealTime()) + uint64_t last_ping_time = StkTime::getRealTimeMs(); + uint64_t last_ping_time_update_for_client = StkTime::getRealTimeMs(); + while (m_exit_timeout.load() > StkTime::getRealTimeMs()) { auto sl = LobbyProtocol::get(); if (direct_socket && sl && sl->waitingForPlayers()) @@ -740,12 +739,13 @@ void STKHost::mainLoop() const float timeout = UserConfigParams::m_validation_timeout; bool need_ping = false; if (sl && !sl->isRacing() && - last_ping_time < StkTime::getRealTime()) + last_ping_time < StkTime::getRealTimeMs()) { // If not racing, send an reliable packet at the same rate with // state exchange to keep enet ping accurate - last_ping_time = StkTime::getRealTime() + - 1.0 / double(stk_config->m_network_state_frequeny); + last_ping_time = StkTime::getRealTimeMs() + + (uint64_t)((1.0f / + (float)(stk_config->m_network_state_frequeny)) * 1000.0f); need_ping = true; } @@ -793,8 +793,7 @@ 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() && - (float)StkTime::getRealTime() > - it->second->getConnectedTime() + timeout) + it->second->getConnectedTime() > timeout) { Log::info("STKHost", "%s has not been validated for more" " than %f seconds, disconnect it by force.", @@ -843,10 +842,10 @@ void STKHost::mainLoop() while (enet_host_service(host, &event, 10) != 0) { if (!is_server && - last_ping_time_update_for_client < StkTime::getRealTime()) + last_ping_time_update_for_client < StkTime::getRealTimeMs()) { last_ping_time_update_for_client = - StkTime::getRealTime() + 2.0; + StkTime::getRealTimeMs() + 2000; auto lp = LobbyProtocol::get(); if (lp && lp->isRacing()) { @@ -886,9 +885,9 @@ void STKHost::mainLoop() // If used a timeout waiting disconnect, exit now if (m_exit_timeout.load() != - std::numeric_limits::max()) + std::numeric_limits::max()) { - m_exit_timeout.store(0.0); + m_exit_timeout.store(0); break; } // Use the previous stk peer so protocol can see the network @@ -981,7 +980,7 @@ void STKHost::mainLoop() else delete stk_event; } // while enet_host_service - } // while m_exit_timeout.load() > StkTime::getRealTime() + } // while m_exit_timeout.load() > StkTime::getRealTimeMs() delete direct_socket; Log::info("STKHost", "Listening has been stopped."); } // mainLoop diff --git a/src/network/stk_host.hpp b/src/network/stk_host.hpp index 5e06da5ee..1237833a1 100644 --- a/src/network/stk_host.hpp +++ b/src/network/stk_host.hpp @@ -120,7 +120,7 @@ private: std::atomic_bool m_authorised; /** Use as a timeout to waiting a disconnect event when exiting. */ - std::atomic m_exit_timeout; + std::atomic m_exit_timeout; /** An error message, which is set by a protocol to be displayed * in the GUI. */ diff --git a/src/network/stk_peer.cpp b/src/network/stk_peer.cpp index fbc9b96ac..a38a6a853 100644 --- a/src/network/stk_peer.cpp +++ b/src/network/stk_peer.cpp @@ -37,7 +37,7 @@ STKPeer::STKPeer(ENetPeer *enet_peer, STKHost* host, uint32_t host_id) { m_enet_peer = enet_peer; m_host_id = host_id; - m_connected_time = (float)StkTime::getRealTime(); + m_connected_time = StkTime::getRealTimeMs(); m_validated.store(false); m_average_ping.store(0); } // STKPeer @@ -114,7 +114,7 @@ void STKPeer::sendPacket(NetworkString *data, bool reliable, bool encrypted) { if (Network::m_connection_debug) { - Log::verbose("STKPeer", "sending packet of size %d to %s at %f", + Log::verbose("STKPeer", "sending packet of size %d to %s at %lf", packet->dataLength, a.toString().c_str(), StkTime::getRealTime()); } @@ -155,7 +155,7 @@ bool STKPeer::isSamePeer(const ENetPeer* peer) const */ uint32_t STKPeer::getPing() { - if ((float)StkTime::getRealTime() - m_connected_time < 3.0f) + if (getConnectedTime() < 3.0f) return 0; if (NetworkConfig::get()->isServer()) { diff --git a/src/network/stk_peer.hpp b/src/network/stk_peer.hpp index ed327694f..a16c3db23 100644 --- a/src/network/stk_peer.hpp +++ b/src/network/stk_peer.hpp @@ -25,6 +25,7 @@ #include "network/transport_address.hpp" #include "utils/no_copy.hpp" +#include "utils/time.hpp" #include "utils/types.hpp" #include @@ -72,7 +73,7 @@ protected: std::vector > m_players; - float m_connected_time; + uint64_t m_connected_time; /** Available karts and tracks from this peer */ std::pair, std::set > m_available_kts; @@ -120,7 +121,8 @@ public: /** Returns the host id of this peer. */ uint32_t getHostId() const { return m_host_id; } // ------------------------------------------------------------------------ - float getConnectedTime() const { return m_connected_time; } + float getConnectedTime() const + { return float(StkTime::getRealTimeMs() - m_connected_time) / 1000.0f; } // ------------------------------------------------------------------------ void setAvailableKartsTracks(std::set& k, std::set& t) diff --git a/src/states_screens/dialogs/player_rankings_dialog.cpp b/src/states_screens/dialogs/player_rankings_dialog.cpp index 3d36d53eb..5774340e4 100644 --- a/src/states_screens/dialogs/player_rankings_dialog.cpp +++ b/src/states_screens/dialogs/player_rankings_dialog.cpp @@ -165,12 +165,12 @@ GUIEngine::EventPropagation } else if (selection == m_refresh_widget->m_properties[PROP_ID]) { - static double timer = StkTime::getRealTime(); + static uint64_t timer = StkTime::getRealTimeMs(); // 1 minute per refresh - if (StkTime::getRealTime() < timer + 60.0) + if (StkTime::getRealTimeMs() < timer + 60000) return GUIEngine::EVENT_BLOCK; - timer = StkTime::getRealTime(); + timer = StkTime::getRealTimeMs(); *m_fetched_ranking = false; updatePlayerRanking(m_name, m_online_id, m_ranking_info, m_fetched_ranking); diff --git a/src/states_screens/tracks_screen.cpp b/src/states_screens/tracks_screen.cpp index 6bccd95a9..dcfdb4fcc 100644 --- a/src/states_screens/tracks_screen.cpp +++ b/src/states_screens/tracks_screen.cpp @@ -135,7 +135,7 @@ bool TracksScreen::onEscapePressed() void TracksScreen::tearDown() { m_network_tracks = false; - m_vote_timeout = -1.0f; + m_vote_timeout = std::numeric_limits::max(); m_selected_track = NULL; } // tearDown @@ -418,9 +418,9 @@ void TracksScreen::setFocusOnTrack(const std::string& trackName) // ----------------------------------------------------------------------------- void TracksScreen::setVoteTimeout(float timeout) { - if (m_vote_timeout != -1.0f) + if (m_vote_timeout != std::numeric_limits::max()) return; - m_vote_timeout = (float)StkTime::getRealTime() + timeout; + m_vote_timeout = StkTime::getRealTimeMs() + (uint64_t)(timeout * 1000.0f); } // setVoteTimeout // ----------------------------------------------------------------------------- @@ -451,14 +451,14 @@ void TracksScreen::voteForPlayer() void TracksScreen::onUpdate(float dt) { assert(m_votes); - if (m_vote_timeout == -1.0f) + if (m_vote_timeout == std::numeric_limits::max()) { m_votes->setText(L"", false); return; } m_votes->setVisible(true); - int remaining_time = (int)(m_vote_timeout - StkTime::getRealTime()); + int remaining_time = (m_vote_timeout - StkTime::getRealTimeMs()) / 1000; if (remaining_time < 0) remaining_time = 0; //I18N: In tracks screen, about voting of tracks in network diff --git a/src/states_screens/tracks_screen.hpp b/src/states_screens/tracks_screen.hpp index 9a95716af..293428b77 100644 --- a/src/states_screens/tracks_screen.hpp +++ b/src/states_screens/tracks_screen.hpp @@ -21,6 +21,7 @@ #include "guiengine/screen.hpp" #include "utils/synchronised.hpp" #include +#include #include #include @@ -58,7 +59,7 @@ private: int m_bottom_box_height = -1; - float m_vote_timeout = -1.0f; + uint64_t m_vote_timeout = std::numeric_limits::max(); std::map m_vote_messages; @@ -101,7 +102,7 @@ public: void resetVote() { m_vote_messages.clear(); - m_vote_timeout = -1.0f; + m_vote_timeout = std::numeric_limits::max(); } void setVoteTimeout(float timeout); From 732fd7a4c90236ec85ee44f2a43014ab6bcec56f Mon Sep 17 00:00:00 2001 From: Benau Date: Mon, 27 Aug 2018 13:49:52 +0800 Subject: [PATCH 05/42] Don't show timer warning if voting timeout is not default --- src/network/protocols/client_lobby.cpp | 13 ++++++++----- src/network/stk_host.cpp | 3 ++- src/network/stk_peer.cpp | 3 +++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/network/protocols/client_lobby.cpp b/src/network/protocols/client_lobby.cpp index 94d4f7dad..89e611be3 100644 --- a/src/network/protocols/client_lobby.cpp +++ b/src/network/protocols/client_lobby.cpp @@ -216,11 +216,14 @@ void ClientLobby::addAllPlayers(Event* event) // time if (!STKHost::get()->getNetworkTimerSynchronizer()->isSynchronised()) { - core::stringw msg = _("Bad network connection is detected."); - MessageQueue::add(MessageQueue::MT_ERROR, msg); - Log::warn("ClientLobby", "Failed to synchronize timer before game " - "start, maybe you enter the game too quick? (at least 5 seconds " - "are required for synchronization."); + if (UserConfigParams::m_voting_timeout >= 10.0f) + { + core::stringw msg = _("Bad network connection is detected."); + MessageQueue::add(MessageQueue::MT_ERROR, msg); + Log::warn("ClientLobby", "Failed to synchronize timer before game " + "start, maybe you enter the game too quick? (at least 5 " + "seconds are required for synchronization."); + } STKHost::get()->getNetworkTimerSynchronizer()->enableForceSetTimer(); } diff --git a/src/network/stk_host.cpp b/src/network/stk_host.cpp index 29111f7bd..e3ebc9868 100644 --- a/src/network/stk_host.cpp +++ b/src/network/stk_host.cpp @@ -759,7 +759,8 @@ void STKHost::mainLoop() const unsigned ap = p.second->getAveragePing(); const unsigned max_ping = UserConfigParams::m_max_ping; if (UserConfigParams::m_kick_high_ping_players && - p.second->isValidated() && ap > max_ping) + p.second->isValidated() && + p.second->getConnectedTime() > 5.0f && ap > max_ping) { Log::info("STKHost", "%s with ping %d is higher than" " %d ms, kick.", diff --git a/src/network/stk_peer.cpp b/src/network/stk_peer.cpp index a38a6a853..03d50e50f 100644 --- a/src/network/stk_peer.cpp +++ b/src/network/stk_peer.cpp @@ -156,7 +156,10 @@ bool STKPeer::isSamePeer(const ENetPeer* peer) const uint32_t STKPeer::getPing() { if (getConnectedTime() < 3.0f) + { + m_average_ping.store(m_enet_peer->roundTripTime); return 0; + } if (NetworkConfig::get()->isServer()) { // Average ping in 5 seconds From cbaa06d952c15f7144c83a6713b2ae83d4a9b471 Mon Sep 17 00:00:00 2001 From: Benau Date: Mon, 27 Aug 2018 14:32:05 +0800 Subject: [PATCH 06/42] Discard resend packet due to packet loss and adjust tolerance --- src/config/user_config.hpp | 4 ++-- src/network/network_timer_synchronizer.hpp | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/config/user_config.hpp b/src/config/user_config.hpp index 49ab1aab9..15b379313 100644 --- a/src/config/user_config.hpp +++ b/src/config/user_config.hpp @@ -774,8 +774,8 @@ namespace UserConfigParams PARAM_PREFIX IntUserConfigParam m_jitter_tolerance PARAM_DEFAULT(IntUserConfigParam(100, "jitter-tolerance", &m_network_group, "Tolerance of jitter in network allowed (in ms).")); - PARAM_PREFIX IntUserConfigParam m_timer_sync_tolerance - PARAM_DEFAULT(IntUserConfigParam(50, "timer-sync-tolerance", + PARAM_PREFIX IntUserConfigParam m_timer_sync_difference_tolerance + PARAM_DEFAULT(IntUserConfigParam(5, "timer-sync-difference-tolerance", &m_network_group, "Max time difference tolerance (in ms) to synchronize timer with server.")); PARAM_PREFIX BoolUserConfigParam m_kick_high_ping_players PARAM_DEFAULT(BoolUserConfigParam(false, "kick-high-ping-players", diff --git a/src/network/network_timer_synchronizer.hpp b/src/network/network_timer_synchronizer.hpp index a20ab54b2..bfc94a68b 100644 --- a/src/network/network_timer_synchronizer.hpp +++ b/src/network/network_timer_synchronizer.hpp @@ -68,6 +68,14 @@ public: } const uint64_t cur_time = StkTime::getRealTimeMs(); + // Discard too close time compared to last ping + // (due to resend when packet loss) + const uint64_t frequency = (uint64_t)((1.0f / + (float)(stk_config->m_network_state_frequeny)) * 1000.0f) / 2; + if (!m_times.empty() && + cur_time - std::get<2>(m_times.back()) < frequency) + return; + // Take max 20 averaged samples from m_times, the next addAndGetTime // is used to determine that server_time if it's correct, if not // clear half in m_times until it's correct @@ -84,7 +92,7 @@ public: const int64_t server_time_now = server_time + (uint64_t)(ping / 2); int difference = (int)std::abs(averaged_time - server_time_now); if (std::abs(averaged_time - server_time_now) < - UserConfigParams::m_timer_sync_tolerance) + UserConfigParams::m_timer_sync_difference_tolerance) { STKHost::get()->setNetworkTimer(averaged_time); m_force_set_timer.store(false); From 61e37bc60f4e6dec76b9a426b60b2cfb1dacc996 Mon Sep 17 00:00:00 2001 From: Benau Date: Mon, 27 Aug 2018 15:41:15 +0800 Subject: [PATCH 07/42] Use zero for unused values in vote for FFA and CTF --- src/states_screens/tracks_screen.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/states_screens/tracks_screen.cpp b/src/states_screens/tracks_screen.cpp index dcfdb4fcc..9456eb789 100644 --- a/src/states_screens/tracks_screen.cpp +++ b/src/states_screens/tracks_screen.cpp @@ -442,8 +442,23 @@ void TracksScreen::voteForPlayer() NetworkString vote(PROTOCOL_LOBBY_ROOM); vote.addUInt8(LobbyProtocol::LE_VOTE); - vote.encodeString(m_selected_track->getIdent()) - .addUInt8(m_laps->getValue()).addUInt8(m_reversed->getState()); + if (race_manager->getMajorMode() == RaceManager::MAJOR_MODE_FREE_FOR_ALL) + { + vote.encodeString(m_selected_track->getIdent()) + .addUInt8(0).addUInt8(m_reversed->getState() ? 1 : 0); + } + else if (race_manager->getMajorMode() == + RaceManager::MAJOR_MODE_CAPTURE_THE_FLAG) + { + vote.encodeString(m_selected_track->getIdent()) + .addUInt8(0).addUInt8(0); + } + else + { + vote.encodeString(m_selected_track->getIdent()) + .addUInt8(m_laps->getValue()) + .addUInt8(m_reversed->getState() ? 1 : 0); + } STKHost::get()->sendToServer(&vote, true); } // voteForPlayer From 54bac1bf83118a64e5164be41cf44522fa5c9908 Mon Sep 17 00:00:00 2001 From: Benau Date: Tue, 28 Aug 2018 01:54:25 +0800 Subject: [PATCH 08/42] Fix compiler warnings --- src/network/protocols/server_lobby.cpp | 2 +- src/network/stk_host.cpp | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/network/protocols/server_lobby.cpp b/src/network/protocols/server_lobby.cpp index 8c45a8623..3b8bfdf7f 100644 --- a/src/network/protocols/server_lobby.cpp +++ b/src/network/protocols/server_lobby.cpp @@ -765,7 +765,7 @@ void ServerLobby::startSelection(const Event *event) } auto players = m_game_setup->getConnectedPlayers(); - const unsigned player_count = players.size(); + const unsigned player_count = (unsigned)players.size(); if (NetworkConfig::get()->hasTeamChoosing() && race_manager->teamEnabled()) { int red_count = 0; diff --git a/src/network/stk_host.cpp b/src/network/stk_host.cpp index e3ebc9868..d75523f02 100644 --- a/src/network/stk_host.cpp +++ b/src/network/stk_host.cpp @@ -677,7 +677,7 @@ void STKHost::startListening() void STKHost::stopListening() { if (m_exit_timeout.load() == std::numeric_limits::max()) - m_exit_timeout.store(0.0); + m_exit_timeout.store(0); if (m_listening_thread.joinable()) m_listening_thread.join(); } // stopListening @@ -912,9 +912,9 @@ void STKHost::mainLoop() if (!is_server) { BareNetworkString ping_packet((char*)event.packet->data, - event.packet->dataLength); + (int)event.packet->dataLength); std::map peer_pings; - ping_packet.skip(g_ping_packet.size()); + ping_packet.skip((int)g_ping_packet.size()); uint64_t server_time = ping_packet.getUInt64(); unsigned peer_size = ping_packet.getUInt8(); for (unsigned i = 0; i < peer_size; i++) From 658e091ea3a0cb3110fc79b0f2565ca837717694 Mon Sep 17 00:00:00 2001 From: Alayan-stk-2 Date: Tue, 28 Aug 2018 00:41:36 +0200 Subject: [PATCH 09/42] Fix list headers not being aligned with cells (#3400) --- src/guiengine/widgets/list_widget.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/guiengine/widgets/list_widget.cpp b/src/guiengine/widgets/list_widget.cpp index bb93a651c..0da6409ac 100644 --- a/src/guiengine/widgets/list_widget.cpp +++ b/src/guiengine/widgets/list_widget.cpp @@ -149,7 +149,8 @@ void ListWidget::createHeader() } int x = m_x; - for (unsigned int n=0; ngetSize(EGDS_SCROLLBAR_SIZE); + for (unsigned int n=0; nm_h = header_height; header->m_x = x; - header->m_w = (int)(m_w * float(m_header[n].m_proportion) - /float(proportion_total)); + if (n == m_header.size()) + { + header->m_w = scrollbar_width; + header->setActive(false); + } + else + { + int header_width = m_w - scrollbar_width; + header->m_w = (int)(header_width * float(m_header[n].m_proportion) + /float(proportion_total)); + } x += header->m_w; - header->setText( m_header[n].m_text ); + if (n < m_header.size()) + header->setText( m_header[n].m_text ); header->m_properties[PROP_ID] = name.str(); header->add(); From d94383e307d13a23af78a2b42d0f904aa6f7d565 Mon Sep 17 00:00:00 2001 From: scootergrisen Date: Tue, 28 Aug 2018 00:42:07 +0200 Subject: [PATCH 10/42] Change multiplayer race to multiplayer (#3401) I think its called multiplayer in the main menu without race. --- data/gui/help7.stkgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/gui/help7.stkgui b/data/gui/help7.stkgui index 8c8dfb2b6..42376bd48 100644 --- a/data/gui/help7.stkgui +++ b/data/gui/help7.stkgui @@ -59,7 +59,7 @@ + text="When input devices are configured, select the 'multiplayer' icon in the main menu. Each player can press the 'fire' key of their gamepad or keyboard to join the game, and use their input device to select their kart. The game continues when everyone selected their kart. Note that the mouse may not be used for this operation."/> From ed8b1fc185f77d1df9d6f45ba2b1c516f6d41650 Mon Sep 17 00:00:00 2001 From: Alayan-stk-2 Date: Tue, 28 Aug 2018 00:44:40 +0200 Subject: [PATCH 11/42] Fix egg hunt finish time (#3402) * Fix #3264 * Fix the fix --- src/modes/easter_egg_hunt.cpp | 7 ++++++- src/modes/easter_egg_hunt.hpp | 2 ++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/modes/easter_egg_hunt.cpp b/src/modes/easter_egg_hunt.cpp index 8e5f1e3b4..ec74d98ca 100644 --- a/src/modes/easter_egg_hunt.cpp +++ b/src/modes/easter_egg_hunt.cpp @@ -33,6 +33,7 @@ EasterEggHunt::EasterEggHunt() : LinearWorld() m_use_highscores = true; m_eggs_found = 0; m_only_ghosts = false; + m_finish_time = 0; } // EasterEggHunt //----------------------------------------------------------------------------- @@ -191,7 +192,11 @@ void EasterEggHunt::update(int ticks) bool EasterEggHunt::isRaceOver() { if(!m_only_ghosts && m_eggs_found == m_number_of_eggs) + { + if (m_finish_time == 0) + m_finish_time = getTime(); return true; + } else if (m_only_ghosts) { for (unsigned int i=0 ; igetGhostFinishTime(); } - return getTime(); + return m_finish_time; } // estimateFinishTimeForKart diff --git a/src/modes/easter_egg_hunt.hpp b/src/modes/easter_egg_hunt.hpp index f7a19aebe..ba5e5eaf9 100644 --- a/src/modes/easter_egg_hunt.hpp +++ b/src/modes/easter_egg_hunt.hpp @@ -45,6 +45,8 @@ private: int m_eggs_found; bool m_only_ghosts; + + float m_finish_time; public: EasterEggHunt(); virtual ~EasterEggHunt(); From 8e17965465ee9bd3ebe2fbb30fbe1dbbd46d19e6 Mon Sep 17 00:00:00 2001 From: Alayan-stk-2 Date: Tue, 28 Aug 2018 01:31:21 +0200 Subject: [PATCH 12/42] Add default 1280x720 resolution in case irrlicht don't report it (#3405) --- src/states_screens/options_screen_video.cpp | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/states_screens/options_screen_video.cpp b/src/states_screens/options_screen_video.cpp index a4c3fa923..efa62f25c 100644 --- a/src/states_screens/options_screen_video.cpp +++ b/src/states_screens/options_screen_video.cpp @@ -250,6 +250,7 @@ void OptionsScreenVideo::init() // old standard resolutions // those are always useful for windowed mode bool found_1024_768 = false; + bool found_1280_720 = false; for (int n=0; n Date: Tue, 28 Aug 2018 02:50:47 +0200 Subject: [PATCH 13/42] Multiple AI levels when view is hidden by a plunger (#3407) * Disable AI item avoidance when blocked by a plunger * Differentiate steering under plunger by item AI level * Variable AI slowdown under plunger --- src/karts/controller/skidding_ai.cpp | 45 ++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/karts/controller/skidding_ai.cpp b/src/karts/controller/skidding_ai.cpp index cfdf53f36..76432ca4c 100644 --- a/src/karts/controller/skidding_ai.cpp +++ b/src/karts/controller/skidding_ai.cpp @@ -562,7 +562,7 @@ void SkiddingAI::handleSteering(float dt) // Potentially adjust the point to aim for in order to either // aim to collect item, or steer to avoid a bad item. - if(m_ai_properties->m_collect_avoid_items) + if(m_ai_properties->m_collect_avoid_items && m_kart->getBlockedByPlungerTicks()<=0) handleItemCollectionAndAvoidance(&aim_point, last_node); steer_angle = steerToPoint(aim_point); @@ -2054,9 +2054,24 @@ void SkiddingAI::handleAcceleration(int ticks) if(m_kart->getBlockedByPlungerTicks()>0) { - if(m_kart->getSpeed() < m_kart->getCurrentMaxSpeed() / 2) - m_controls->setAccel(0.05f); - else + int item_skill = computeSkill(ITEM_SKILL); + float accel_threshold = 0.5f; + + if (item_skill == 0) + accel_threshold = 0.3f; + else if (item_skill == 1) + accel_threshold = 0.5f; + else if (item_skill == 2) + accel_threshold = 0.6f; + else if (item_skill == 3) + accel_threshold = 0.7f; + else if (item_skill == 4) + accel_threshold = 0.8f; + // The best players, knowing the track, don't slow down with a plunger + else if (item_skill == 5) + accel_threshold = 1.0f; + + if(m_kart->getSpeed() > m_kart->getCurrentMaxSpeed() * accel_threshold) m_controls->setAccel(0.0f); return; } @@ -3071,10 +3086,28 @@ void SkiddingAI::setSteering(float angle, float dt) else if(steer_fraction < -1.0f) steer_fraction = -1.0f; // Restrict steering when a plunger is in the face + // The degree of restriction depends on item_skill + + //FIXME : the AI speed estimate in curves don't account for this restriction if(m_kart->getBlockedByPlungerTicks()>0) { - if (steer_fraction > 0.5f) steer_fraction = 0.5f; - else if(steer_fraction < -0.5f) steer_fraction = -0.5f; + int item_skill = computeSkill(ITEM_SKILL); + float steering_limit = 0.5f; + if (item_skill == 0) + steering_limit = 0.35f; + else if (item_skill == 1) + steering_limit = 0.45f; + else if (item_skill == 2) + steering_limit = 0.55f; + else if (item_skill == 3) + steering_limit = 0.65f; + else if (item_skill == 4) + steering_limit = 0.75f; + else if (item_skill == 5) + steering_limit = 0.9f; + + if (steer_fraction > steering_limit) steer_fraction = steering_limit; + else if(steer_fraction < -steering_limit) steer_fraction = -steering_limit; } const Skidding *skidding = m_kart->getSkidding(); From ea25d6b7d4c709859b9d9eadd61583822832b3b2 Mon Sep 17 00:00:00 2001 From: Alayan-stk-2 Date: Tue, 28 Aug 2018 03:18:49 +0200 Subject: [PATCH 14/42] Fix #3404 --- src/items/attachment.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/items/attachment.cpp b/src/items/attachment.cpp index 282cfe6cb..2314a5eb7 100644 --- a/src/items/attachment.cpp +++ b/src/items/attachment.cpp @@ -607,7 +607,8 @@ void Attachment::update(int ticks) m_bubble_explode_sound->play(); } - ItemManager::get()->dropNewItem(Item::ITEM_BUBBLEGUM, m_kart); + if (!m_kart->isGhostKart()) + ItemManager::get()->dropNewItem(Item::ITEM_BUBBLEGUM, m_kart); } break; } // switch From ca9f66a8a8174f480f8acd34e4915f217a9ccec1 Mon Sep 17 00:00:00 2001 From: Benau Date: Tue, 28 Aug 2018 10:17:59 +0800 Subject: [PATCH 15/42] Add explanation for smooth network body class --- src/network/smooth_network_body.hpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/network/smooth_network_body.hpp b/src/network/smooth_network_body.hpp index d6835945d..ac3e30989 100644 --- a/src/network/smooth_network_body.hpp +++ b/src/network/smooth_network_body.hpp @@ -16,6 +16,15 @@ // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +/*! \file smooth_network_body.hpp + * \brief This class help to smooth the graphicial transformation of network + * controlled object. In case there is any difference between server and + * client predicted values, instead of showing the server one immediately, + * it will interpolate between them with an extrapolated value from the old + * predicted values stored in m_adjust_control_point estimated by current + * speed of object. + */ + #ifndef HEADER_SMOOTH_NETWORK_BODY_HPP #define HEADER_SMOOTH_NETWORK_BODY_HPP From 8fc2bdcc3f9e801d44420ea1c439f4eab8550eaa Mon Sep 17 00:00:00 2001 From: Benau Date: Tue, 28 Aug 2018 14:43:48 +0800 Subject: [PATCH 16/42] Show game started info in server screen with icon --- data/gui/blue_flag.png | Bin 11461 -> 4354 bytes data/gui/hourglass.png | Bin 2530 -> 3782 bytes data/gui/online/server_selection.stkgui | 4 ++ data/gui/red_flag.png | Bin 9241 -> 4417 bytes src/main.cpp | 2 +- src/network/protocols/server_lobby.cpp | 2 +- src/network/server.cpp | 6 ++- src/network/server.hpp | 7 ++- src/network/servers_manager.cpp | 3 +- src/network/stk_host.cpp | 1 + src/states_screens/create_server_screen.cpp | 2 +- src/states_screens/online_screen.cpp | 2 +- src/states_screens/server_selection.cpp | 45 +++++++++++++++++--- src/states_screens/server_selection.hpp | 13 ++++++ 14 files changed, 73 insertions(+), 14 deletions(-) diff --git a/data/gui/blue_flag.png b/data/gui/blue_flag.png index bb62e53c34989f960e2ab9a9fb71133612ea7fdb..4b23dbe3075aee72bc0fdcd315bd841cdee983ff 100644 GIT binary patch delta 4316 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 Vw&7lF&Gt_XaCi1`y6+Iq{10Q=WL^LO literal 11461 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 Kl)fq;g8v^^R#VIX diff --git a/data/gui/hourglass.png b/data/gui/hourglass.png index fa7a2f62b592329d4f90689735f979e48ef49940..3a775b0c7b311118053572af8b62f41837da691e 100644 GIT binary patch literal 3782 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 literal 2530 zcmZ8ic|25m8=u88mP;sGAs5+t13S}u>a<5dfWU_QAhO&kv%32h~l{-jp z*)myjZ5hl^*=Y=#F=jI}=bU+ez5aWj&*yx9&-48~=X=ic{GRhUsi#gJ`$=9~9)&{v zmrem$z*Pj z8_45w5m>w-0vJA@zexFWb#--QWQ4=v0H&v>hrwU~P2iiFn(}zOjT_emY-MF-;5Rij zX=`f(M@>y_U|=94J>A~kevQ3~!C)*cEddW4@K!Y17!HTKbt^6}FOSV;x3#qx85z~r zHxP*l%gf6fnps)dRaMm!6B8XB9ka8u3kwS*Qe8?)N>x>rxw$#WSy}nfED6FxU`VhI zf#(8cz(`3+B_$=v%ga|(RM6>kz@(+6<>%+CtE+RlyyD^_z<~0Tk`i-ra;B!HNOxxs8 z36BrmxN##kHnzEi3}joI>tk;vm6w+n78WKDQ*!U$ZEbD+&Cl;?$pbHMZyOt%%E}k( z0dDZ zbCyODmPUvY1lkynrO^b&aGXdaf*=TFGKOOphoTrG@eII^5&_K^zJnBVWe$sJqQfH z1YQ;M?;%KYVLc*2#0W zhz~FDW8sUP9}vNI!f}p*@F0XIIwUaO&lP<_`d2_3X3Ek9 z=3`s|S6q(ZXuWI>B4$9nNE1^G!^;T!9kRqinDE*fvrqD+!oS)~0(&UQE$=BB)QCN=aJwl^I+q2woDr&G z#7n!W%G@2PmoTX?2k_)Nk;m)71|)o$9V=&(n7^GPRTz zN(5A;px8HY) zy{zZ(mxBii!fq$6iu>{Jd(6Kqi#?4Dr6VxDR=;Q5S0AaJvzoTvS`v|RQ1^Df%eBR6 z(oT!AyH-KwIyRvPOVbaZ-&H!e@QcBogz0u;@AB7hb)q*8WBs6t@u}Rq&+U+|PXVLJ zt|2t9(fOB*y_$E^dJQ8Q#Rix_yzZ}^ckn)Xs!5)?Cgtrx>Y0+wJKtbdzk*$L?elcr=lD);(J#FC*{(Kn>EK73=ckjqVv+kp zB<^MQ?~yS-RhqQK@9M9$m5_vP7)L}|jxQ0WzilQe6XF6kB2HxMRL$d?W+yncDdz)|k?+spM1_^&L%vw^?kBdBF?$wb$C%r!OkFvNcs}K8~2BQT^p$lXe)^(sugWTus+?p?c%?$eP4= z{h$A6M@vl-AoAq<)SMrp(E`lsD*B?m@1Vn~XBvNTR-Z{n zGGP}H*JpN8T@mYhZAdo#X1H5m+Ogty6e-)`sT_~a#tWNVirGfiP<{B^Ve4R5^Ob(@ zVjVRjQ)^Y-3$bpGb{=O)+YM}Q z(Qf3q`VMpJ5LJ1Pk+`|WsKaL-zFmTd`c{)czR2T=_$eX)goCoX1vcZqpfgAMUh##E}Qpx^i9Kf5B_$KNz& zy3+jF-WG!=U{(FMrRwVxJU;Dy>>_$uPwEkt!bnHy%4DOA!xX#2@k)bRwpD8CokG<^ zk2QVpd80}H5cCb>+sX?}iDVP`&}lAGBt}UeufV9fM=FB$8gU|CcfBqJNuXlU_}PCn>2|1t%5Ro+z`$8q?kRm1q@WrtW1W zhd=9U_-DVzd)l#|2Bqp$4!i2nsgTv-SJ diff --git a/data/gui/online/server_selection.stkgui b/data/gui/online/server_selection.stkgui index 5c19d2eb0..c2f7f4b1a 100644 --- a/data/gui/online/server_selection.stkgui +++ b/data/gui/online/server_selection.stkgui @@ -17,6 +17,10 @@