diff --git a/data/language/en-GB.txt b/data/language/en-GB.txt index 0102bca7d7..0eaa1171dd 100644 --- a/data/language/en-GB.txt +++ b/data/language/en-GB.txt @@ -3838,3 +3838,4 @@ STR_7005 :Drag an area of footpath STR_7006 :Draw border around image buttons STR_7007 :Ride Type STR_7008 :Unknown ride type ({INT32}) +STR_7009 :Receiving scripts… diff --git a/distribution/changelog.txt b/distribution/changelog.txt index a3c5e18f5b..e4fc56fadc 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -2,6 +2,7 @@ ------------------------------------------------------------------------ - Improved: [#25529] The map selection grid no longer redraws every frame if it has not changed. - Improved: [#25530] Wall dragging can now be cancelled without closing the Scenery window. +- Improved: [#25575] Updated the network protocol to a new format that supports larger packets, allowing clients to connect reliably to servers with many objects or large maps. - Change: [#25485] Make the enlarged pressed swatch sprite more pronounced. - Fix: [#22484] Lingering ghost entrance after placing park entrance. - Fix: [#24952] Duplicate track designs when running via Steam without having RCT1 linked. diff --git a/src/openrct2/Context.cpp b/src/openrct2/Context.cpp index 78867e2f83..7df72e4dfa 100644 --- a/src/openrct2/Context.cpp +++ b/src/openrct2/Context.cpp @@ -1299,6 +1299,8 @@ namespace OpenRCT2 UpdateTimeAccumulators(deltaTime); + Network::Update(); + if (useVariableFrame) { RunVariableFrame(deltaTime); @@ -1307,6 +1309,8 @@ namespace OpenRCT2 { RunFixedFrame(deltaTime); } + + Network::Flush(); } void UpdateTimeAccumulators(float deltaTime) @@ -1332,7 +1336,7 @@ namespace OpenRCT2 if (_ticksAccumulator < kGameUpdateTimeMS) { - const auto sleepTimeSec = (kGameUpdateTimeMS - _ticksAccumulator); + const auto sleepTimeSec = std::min(kNetworkUpdateTimeMS, kGameUpdateTimeMS - _ticksAccumulator); Platform::Sleep(static_cast(sleepTimeSec * 1000.f)); return; } diff --git a/src/openrct2/Game.h b/src/openrct2/Game.h index 9b08cf16c3..308b0e2247 100644 --- a/src/openrct2/Game.h +++ b/src/openrct2/Game.h @@ -26,6 +26,11 @@ namespace OpenRCT2 // The maximum threshold to advance. constexpr float kGameUpdateMaxThreshold = kGameUpdateTimeMS * kGameMaxUpdates; + // The network update runs at a different rate to the game update. + constexpr uint32_t kNetworkUpdateFPS = 140; + // The network update interval in milliseconds, (1000 / 140fps) = ~7.14ms + constexpr float kNetworkUpdateTimeMS = 1.0f / kNetworkUpdateFPS; + constexpr float kGameMinTimeScale = 0.1f; constexpr float kGameMaxTimeScale = 5.0f; } // namespace OpenRCT2 diff --git a/src/openrct2/GameState.cpp b/src/openrct2/GameState.cpp index d8ec67127b..3de0e5b5a1 100644 --- a/src/openrct2/GameState.cpp +++ b/src/openrct2/GameState.cpp @@ -173,7 +173,7 @@ namespace OpenRCT2 getGameState().entities.UpdateMoneyEffect(); // Post-tick network update - Network::ProcessPending(); + Network::PostTick(); // Post-tick game actions. GameActions::ProcessQueue(); @@ -181,6 +181,12 @@ namespace OpenRCT2 } } + // Network has to always tick. + if (numUpdates == 0) + { + Network::Tick(); + } + // Update the game one or more times for (uint32_t i = 0; i < numUpdates; i++) { @@ -251,7 +257,7 @@ namespace OpenRCT2 GetContext()->GetReplayManager()->Update(); - Network::Update(); + Network::Tick(); auto& gameState = getGameState(); @@ -344,7 +350,7 @@ namespace OpenRCT2 GameActions::ProcessQueue(); - Network::ProcessPending(); + Network::PostTick(); Network::Flush(); gameState.currentTicks++; diff --git a/src/openrct2/localisation/StringIds.h b/src/openrct2/localisation/StringIds.h index a9276c20f3..b1fedb852c 100644 --- a/src/openrct2/localisation/StringIds.h +++ b/src/openrct2/localisation/StringIds.h @@ -1575,6 +1575,7 @@ enum : StringId STR_UNKNOWN_RIDE = 6375, STR_MULTIPLAYER_RECEIVING_OBJECTS_LIST = 6378, + STR_MULTIPLAYER_RECEIVING_SCRIPTS = 7009, STR_MULTIPLAYER_RECEIVED_INVALID_DATA = 6379, diff --git a/src/openrct2/network/Network.h b/src/openrct2/network/Network.h index 3f620669b0..3f4fd9d5e0 100644 --- a/src/openrct2/network/Network.h +++ b/src/openrct2/network/Network.h @@ -53,7 +53,8 @@ namespace OpenRCT2::Network void SendTick(); bool GamestateSnapshotsEnabled(); void Update(); - void ProcessPending(); + void Tick(); + void PostTick(); void Flush(); [[nodiscard]] Auth GetAuthstatus(); diff --git a/src/openrct2/network/NetworkBase.cpp b/src/openrct2/network/NetworkBase.cpp index 4b4b963c2d..415a5ca3e5 100644 --- a/src/openrct2/network/NetworkBase.cpp +++ b/src/openrct2/network/NetworkBase.cpp @@ -47,7 +47,7 @@ // It is used for making sure only compatible builds get connected, even within // single OpenRCT2 version. -constexpr uint8_t kStreamVersion = 1; +constexpr uint8_t kStreamVersion = 2; const std::string kStreamID = std::string(kOpenRCT2Version) + "-" + std::to_string(kStreamVersion); @@ -62,7 +62,7 @@ static constexpr uint32_t kChunkSize = 1024 * 63; // If data is sent fast enough it would halt the entire server, process only a maximum amount. // This limit is per connection, the current value was determined by tests with fuzzing. -static constexpr uint32_t kMaxPacketsPerUpdate = 100; +static constexpr uint32_t kMaxPacketsPerTick = 100; #include "../Cheats.h" #include "../ParkImporter.h" @@ -119,6 +119,7 @@ namespace OpenRCT2::Network _actionId = 0; client_command_handlers[Command::auth] = &NetworkBase::Client_Handle_AUTH; + client_command_handlers[Command::beginMap] = &NetworkBase::Client_Handle_BEGINMAP; client_command_handlers[Command::map] = &NetworkBase::Client_Handle_MAP; client_command_handlers[Command::chat] = &NetworkBase::Client_Handle_CHAT; client_command_handlers[Command::gameAction] = &NetworkBase::Client_Handle_GAME_ACTION; @@ -134,7 +135,6 @@ namespace OpenRCT2::Network client_command_handlers[Command::gameInfo] = &NetworkBase::Client_Handle_GAMEINFO; client_command_handlers[Command::token] = &NetworkBase::Client_Handle_TOKEN; client_command_handlers[Command::objectsList] = &NetworkBase::Client_Handle_OBJECTS_LIST; - client_command_handlers[Command::scriptsHeader] = &NetworkBase::Client_Handle_SCRIPTS_HEADER; client_command_handlers[Command::scriptsData] = &NetworkBase::Client_Handle_SCRIPTS_DATA; client_command_handlers[Command::gameState] = &NetworkBase::Client_Handle_GAMESTATE; @@ -463,6 +463,21 @@ namespace OpenRCT2::Network } void NetworkBase::Update() + { + switch (GetMode()) + { + case Mode::server: + UpdateServer(); + break; + case Mode::client: + UpdateClient(); + break; + default: + break; + } + } + + void NetworkBase::Tick() { _closeLock = true; @@ -474,10 +489,10 @@ namespace OpenRCT2::Network switch (GetMode()) { case Mode::server: - UpdateServer(); + TickServer(); break; case Mode::client: - UpdateClient(); + TickClient(); break; default: break; @@ -511,6 +526,18 @@ namespace OpenRCT2::Network } void NetworkBase::UpdateServer() + { + for (auto& connection : client_connection_list) + { + // This can be called multiple times before the connection is removed. + if (!connection->IsValid()) + continue; + + connection->update(); + } + } + + void NetworkBase::TickServer() { for (auto& connection : client_connection_list) { @@ -520,6 +547,7 @@ namespace OpenRCT2::Network if (!ProcessConnection(*connection)) { + LOG_INFO("Disconnecting player %s", connection->player->Name.c_str()); connection->Disconnect(); } else @@ -548,6 +576,11 @@ namespace OpenRCT2::Network } void NetworkBase::UpdateClient() + { + _serverConnection->update(); + } + + void NetworkBase::TickClient() { assert(_serverConnection != nullptr); @@ -594,7 +627,6 @@ namespace OpenRCT2::Network case SocketStatus::connected: { status = Status::connected; - _serverConnection->ResetLastPacketTime(); Client_Send_TOKEN(); char str_authenticating[256]; FormatStringLegacy(str_authenticating, 256, STR_MULTIPLAYER_AUTHENTICATING, nullptr); @@ -1319,44 +1351,39 @@ namespace OpenRCT2::Network { LOG_VERBOSE("Server sends objects list with %u items", objects.size()); - if (objects.empty()) - { - Packet packet(Command::objectsList); - packet << static_cast(0) << static_cast(objects.size()); + Packet packet(Command::objectsList); - connection.QueuePacket(std::move(packet)); - } - else + // Count. + packet << static_cast(objects.size()); + + // List + for (size_t i = 0; i < objects.size(); ++i) { - for (size_t i = 0; i < objects.size(); ++i) + const auto* object = objects[i]; + + if (object->Identifier.empty()) { - const auto* object = objects[i]; - - Packet packet(Command::objectsList); - packet << static_cast(i) << static_cast(objects.size()); - - if (object->Identifier.empty()) - { - // DAT - LOG_VERBOSE("Object %.8s (checksum %x)", object->ObjectEntry.name, object->ObjectEntry.checksum); - packet << static_cast(0); - packet.Write(&object->ObjectEntry, sizeof(RCTObjectEntry)); - } - else - { - // JSON - LOG_VERBOSE("Object %s", object->Identifier.c_str()); - packet << static_cast(1); - packet.WriteString(object->Identifier); - } - - connection.QueuePacket(std::move(packet)); + // DAT + LOG_VERBOSE("Object %.8s (checksum %x)", object->ObjectEntry.name, object->ObjectEntry.checksum); + packet << static_cast(0); + packet.Write(&object->ObjectEntry, sizeof(RCTObjectEntry)); + } + else + { + // JSON + LOG_VERBOSE("Object %s", object->Identifier.c_str()); + packet << static_cast(1); + packet.WriteString(object->Identifier); } } + + connection.QueuePacket(std::move(packet)); } void NetworkBase::ServerSendScripts(Connection& connection) { + Packet packet(Command::scriptsData); + #ifdef ENABLE_SCRIPTING using namespace OpenRCT2::Scripting; @@ -1366,45 +1393,21 @@ namespace OpenRCT2::Network const auto remotePlugins = scriptEngine.GetRemotePlugins(); LOG_VERBOSE("Server sends %zu scripts", remotePlugins.size()); - // Build the data contents for each plugin. - MemoryStream pluginData; + packet << static_cast(remotePlugins.size()); + for (auto& plugin : remotePlugins) { const auto& code = plugin->GetCode(); - const auto codeSize = static_cast(code.size()); - pluginData.WriteValue(codeSize); - pluginData.WriteArray(code.c_str(), code.size()); + + packet << codeSize; + packet.Write(code.c_str(), code.size()); } - - // Send the header packet. - Packet packetScriptHeader(Command::scriptsHeader); - packetScriptHeader << static_cast(remotePlugins.size()); - packetScriptHeader << static_cast(pluginData.GetLength()); - connection.QueuePacket(std::move(packetScriptHeader)); - - // Segment the plugin data into chunks and send them. - const uint8_t* pluginDataBuffer = static_cast(pluginData.GetData()); - uint32_t dataOffset = 0; - while (dataOffset < pluginData.GetLength()) - { - const uint32_t chunkSize = std::min(pluginData.GetLength() - dataOffset, kChunkSize); - - Packet packet(Command::scriptsData); - packet << chunkSize; - packet.Write(pluginDataBuffer + dataOffset, chunkSize); - - connection.QueuePacket(std::move(packet)); - - dataOffset += chunkSize; - } - Guard::Assert(dataOffset == pluginData.GetLength()); - #else - Packet packetScriptHeader(Command::scriptsHeader); - packetScriptHeader << static_cast(0u); - packetScriptHeader << static_cast(0u); + packet << static_cast(0); #endif + + connection.QueuePacket(std::move(packet)); } void NetworkBase::Client_Send_HEARTBEAT(Connection& connection) const @@ -1472,8 +1475,8 @@ namespace OpenRCT2::Network objects = objManager.GetPackableObjects(); } - auto header = SaveForNetwork(objects); - if (header.empty()) + auto mapContent = SaveForNetwork(objects); + if (mapContent.empty()) { if (connection != nullptr) { @@ -1482,21 +1485,21 @@ namespace OpenRCT2::Network } return; } - size_t chunksize = kChunkSize; - for (size_t i = 0; i < header.size(); i += chunksize) + + Packet packetBeginMap(Command::beginMap); + + Packet packetMap(Command::map); + packetMap.Write(mapContent.data(), mapContent.size()); + + if (connection != nullptr) { - size_t datasize = std::min(chunksize, header.size() - i); - Packet packet(Command::map); - packet << static_cast(header.size()) << static_cast(i); - packet.Write(&header[i], datasize); - if (connection != nullptr) - { - connection->QueuePacket(std::move(packet)); - } - else - { - SendPacketToClients(packet); - } + connection->QueuePacket(std::move(packetBeginMap)); + connection->QueuePacket(std::move(packetMap)); + } + else + { + SendPacketToClients(packetBeginMap); + SendPacketToClients(packetMap); } } @@ -1740,6 +1743,56 @@ namespace OpenRCT2::Network SendPacketToClients(packet); } + bool NetworkBase::UpdateConnection(Connection& connection) + { + connection.update(); + + return connection.IsValid(); + } + + static void displayNetworkProgress(StringId captionStringId) + { + auto captionString = GetContext()->GetLocalisationService().GetString(captionStringId); + auto intent = Intent(INTENT_ACTION_PROGRESS_OPEN); + intent.PutExtra(INTENT_EXTRA_MESSAGE, captionString); + intent.PutExtra(INTENT_EXTRA_CALLBACK, []() -> void { + LOG_INFO("User aborted network operation"); + OpenRCT2::GetContext()->GetNetwork().Close(); + }); + ContextOpenIntent(&intent); + } + + static void reportPacketProgress(NetworkBase& network, Connection& connection) + { + if (network.GetMode() != Mode::client) + { + return; + } + + const auto nextPacketCommand = connection.getPendingPacketCommand(); + const auto bytesReceived = connection.getPendingPacketAvailable(); + const auto bytesTotal = connection.getPendingPacketSize(); + + switch (nextPacketCommand) + { + case Command::objectsList: + displayNetworkProgress(STR_MULTIPLAYER_RECEIVING_OBJECTS_LIST); + break; + case Command::map: + displayNetworkProgress(STR_MULTIPLAYER_DOWNLOADING_MAP); + break; + case Command::scriptsData: + displayNetworkProgress(STR_MULTIPLAYER_RECEIVING_SCRIPTS); + break; + default: + // Nothing to report. + return; + } + + network.GetContext().SetProgress( + static_cast(bytesReceived), static_cast(bytesTotal), STR_STRING_M_OF_N_KIB); + } + bool NetworkBase::ProcessConnection(Connection& connection) { ReadPacket packetStatus; @@ -1760,6 +1813,7 @@ namespace OpenRCT2::Network return false; case ReadPacket::success: // done reading in packet + reportPacketProgress(*this, connection); ProcessPacket(connection, connection.InboundPacket); if (!connection.IsValid()) { @@ -1768,15 +1822,20 @@ namespace OpenRCT2::Network break; case ReadPacket::moreData: // more data required to be read + reportPacketProgress(*this, connection); break; case ReadPacket::noData: // could not read anything from socket break; } - } while (packetStatus == ReadPacket::success && countProcessed < kMaxPacketsPerUpdate); + } while (packetStatus == ReadPacket::success && countProcessed < kMaxPacketsPerTick); - if (!connection.ReceivedPacketRecently()) + if (!connection.ReceivedDataRecently()) { + LOG_INFO( + "No data received recently from connection %s, disconnecting connection.", + connection.Socket->GetIpAddress().c_str()); + if (!connection.GetLastDisconnectReason()) { connection.SetLastDisconnectReason(STR_MULTIPLAYER_NO_DATA); @@ -1812,7 +1871,7 @@ namespace OpenRCT2::Network } // This is called at the end of each game tick, this where things should be processed that affects the game state. - void NetworkBase::ProcessPending() + void NetworkBase::PostTick() { if (GetMode() == Mode::server) { @@ -2302,8 +2361,7 @@ namespace OpenRCT2::Network void NetworkBase::ServerHandleHeartbeat(Connection& connection, Packet& packet) { - LOG_VERBOSE("Client %s heartbeat", connection.Socket->GetHostName()); - connection.ResetLastPacketTime(); + LOG_VERBOSE("Client %s heartbeat", connection.Socket->GetIpAddress().c_str()); } void NetworkBase::Client_Handle_AUTH(Connection& connection, Packet& packet) @@ -2392,34 +2450,17 @@ namespace OpenRCT2::Network ServerSendToken(connection); } - static void OpenNetworkProgress(StringId captionStringId) - { - auto captionString = GetContext()->GetLocalisationService().GetString(captionStringId); - auto intent = Intent(INTENT_ACTION_PROGRESS_OPEN); - intent.PutExtra(INTENT_EXTRA_MESSAGE, captionString); - intent.PutExtra(INTENT_EXTRA_CALLBACK, []() -> void { OpenRCT2::GetContext()->GetNetwork().Close(); }); - ContextOpenIntent(&intent); - } - void NetworkBase::Client_Handle_OBJECTS_LIST(Connection& connection, Packet& packet) { auto& repo = GetContext().GetObjectRepository(); - uint32_t index = 0; - uint32_t totalObjects = 0; - packet >> index >> totalObjects; + uint32_t objectCount{}; + packet >> objectCount; - static constexpr uint32_t kObjectStartIndex = 0; - if (index == kObjectStartIndex) + std::vector missingObjects; + + for (uint32_t i = 0; i < objectCount; ++i) { - _missingObjects.clear(); - } - - if (totalObjects > 0) - { - OpenNetworkProgress(STR_MULTIPLAYER_RECEIVING_OBJECTS_LIST); - GetContext().SetProgress(index + 1, totalObjects); - uint8_t objectType{}; packet >> objectType; @@ -2434,7 +2475,7 @@ namespace OpenRCT2::Network { auto objectName = std::string(entry->GetName()); LOG_VERBOSE("Requesting object %s with checksum %x from server", objectName.c_str(), entry->checksum); - _missingObjects.push_back(ObjectEntryDescriptor(*entry)); + missingObjects.push_back(ObjectEntryDescriptor(*entry)); } else if (object->ObjectEntry.checksum != entry->checksum || object->ObjectEntry.flags != entry->flags) { @@ -2456,69 +2497,35 @@ namespace OpenRCT2::Network { auto objectName = std::string(identifier); LOG_VERBOSE("Requesting object %s from server", objectName.c_str()); - _missingObjects.push_back(ObjectEntryDescriptor(objectName)); + missingObjects.push_back(ObjectEntryDescriptor(objectName)); } } } } - if (index + 1 >= totalObjects) - { - LOG_VERBOSE("client received object list, it has %u entries", totalObjects); - Client_Send_MAPREQUEST(_missingObjects); - _missingObjects.clear(); - } - } - - void NetworkBase::Client_Handle_SCRIPTS_HEADER(Connection& connection, Packet& packet) - { - uint32_t numScripts{}; - uint32_t dataSize{}; - packet >> numScripts >> dataSize; - - #ifdef ENABLE_SCRIPTING - _serverScriptsData.data.Clear(); - _serverScriptsData.pluginCount = numScripts; - _serverScriptsData.dataSize = dataSize; - #else - if (numScripts > 0) - { - connection.SetLastDisconnectReason("The client requires plugin support."); - Close(); - } - #endif + LOG_VERBOSE("client received object list, it has %u entries, %zu missing", objectCount, missingObjects.size()); + Client_Send_MAPREQUEST(missingObjects); } void NetworkBase::Client_Handle_SCRIPTS_DATA(Connection& connection, Packet& packet) { #ifdef ENABLE_SCRIPTING - uint32_t dataSize{}; - packet >> dataSize; - Guard::Assert(dataSize > 0); + auto& scriptEngine = GetContext().GetScriptEngine(); - const auto* data = packet.Read(dataSize); - Guard::Assert(data != nullptr); + uint32_t count{}; + packet >> count; - auto& scriptsData = _serverScriptsData.data; - scriptsData.Write(data, dataSize); - - if (scriptsData.GetLength() == _serverScriptsData.dataSize) + for (uint32_t i = 0; i < count; ++i) { - auto& scriptEngine = GetContext().GetScriptEngine(); + uint32_t codeSize{}; + packet >> codeSize; - scriptsData.SetPosition(0); - for (uint32_t i = 0; i < _serverScriptsData.pluginCount; ++i) - { - const auto codeSize = scriptsData.ReadValue(); - const auto scriptData = scriptsData.ReadArray(codeSize); + const uint8_t* scriptData = packet.Read(codeSize); - auto code = std::string_view(reinterpret_cast(scriptData.get()), codeSize); - scriptEngine.AddNetworkPlugin(code); - } - Guard::Assert(scriptsData.GetPosition() == scriptsData.GetLength()); + auto code = std::string_view(reinterpret_cast(scriptData), codeSize); + scriptEngine.AddNetworkPlugin(code); - // Empty the current buffer. - _serverScriptsData = {}; + LOG_VERBOSE("Received and loaded network script plugin %u/%u", i + 1, count); } #else connection.SetLastDisconnectReason("The client requires plugin support."); @@ -2768,84 +2775,59 @@ namespace OpenRCT2::Network } } + void NetworkBase::Client_Handle_BEGINMAP([[maybe_unused]] Connection& connection, Packet& packet) + { + // Start of a new map load, clear the queue now as we have to buffer them + // until the map is fully loaded. + GameActions::ClearQueue(); + GameActions::SuspendQueue(); + + displayNetworkProgress(STR_LOADING_SAVED_GAME); + } + void NetworkBase::Client_Handle_MAP([[maybe_unused]] Connection& connection, Packet& packet) { - uint32_t size, offset; - packet >> size >> offset; - int32_t chunksize = static_cast(packet.Header.Size - packet.BytesRead); - if (chunksize <= 0) + // Allow queue processing of game actions again. + GameActions::ResumeQueue(); + + // This prevents invoking the callback for when the window closes which would close the connection. + GetContext().CloseProgress(); + + GameUnloadScripts(); + GameNotifyMapChange(); + + auto ms = MemoryStream(packet.Data.data(), packet.Data.size()); + if (LoadMap(&ms)) { - return; + GameLoadInit(); + GameLoadScripts(); + GameNotifyMapChanged(); + + // This seems wrong, we want to catch up to that tick so we shouldn't mess with this. + //_serverState.tick = getGameState().currentTicks; + + _serverState.state = ServerStatus::ok; + _clientMapLoaded = true; + gFirstTimeSaving = true; + + // Notify user he is now online and which shortcut key enables chat + ChatShowConnectedMessage(); + + // Fix invalid vehicle sprite sizes, thus preventing visual corruption of sprites + FixInvalidVehicleSpriteSizes(); + + // NOTE: Game actions are normally processed before processing the player list. + // Given that during map load game actions are buffered we have to process the + // player list first to have valid players for the queued game actions. + ProcessPlayerList(); } - if (offset == 0) + else { - // Start of a new map load, clear the queue now as we have to buffer them - // until the map is fully loaded. - GameActions::ClearQueue(); - GameActions::SuspendQueue(); + // Something went wrong, game is not loaded. Return to main screen. + auto loadOrQuitAction = GameActions::LoadOrQuitAction( + GameActions::LoadOrQuitModes::OpenSavePrompt, PromptMode::saveBeforeQuit); - _serverTickData.clear(); - _clientMapLoaded = false; - - OpenNetworkProgress(STR_MULTIPLAYER_DOWNLOADING_MAP); - } - if (size > chunk_buffer.size()) - { - chunk_buffer.resize(size); - } - - const auto currentProgressKiB = (offset + chunksize) / 1024; - const auto totalSizeKiB = size / 1024; - - GetContext().SetProgress(currentProgressKiB, totalSizeKiB, STR_STRING_M_OF_N_KIB); - - std::memcpy(&chunk_buffer[offset], const_cast(static_cast(packet.Read(chunksize))), chunksize); - if (offset + chunksize == size) - { - // Allow queue processing of game actions again. - GameActions::ResumeQueue(); - - ContextForceCloseWindowByClass(WindowClass::progressWindow); - GameUnloadScripts(); - GameNotifyMapChange(); - - bool has_to_free = false; - uint8_t* data = &chunk_buffer[0]; - size_t data_size = size; - auto ms = MemoryStream(data, data_size); - if (LoadMap(&ms)) - { - GameLoadInit(); - GameLoadScripts(); - GameNotifyMapChanged(); - _serverState.tick = getGameState().currentTicks; - // NetworkStatusOpen("Loaded new map from network"); - _serverState.state = ServerStatus::ok; - _clientMapLoaded = true; - gFirstTimeSaving = true; - - // Notify user he is now online and which shortcut key enables chat - ChatShowConnectedMessage(); - - // Fix invalid vehicle sprite sizes, thus preventing visual corruption of sprites - FixInvalidVehicleSpriteSizes(); - - // NOTE: Game actions are normally processed before processing the player list. - // Given that during map load game actions are buffered we have to process the - // player list first to have valid players for the queued game actions. - ProcessPlayerList(); - } - else - { - // Something went wrong, game is not loaded. Return to main screen. - auto loadOrQuitAction = GameActions::LoadOrQuitAction( - GameActions::LoadOrQuitModes::OpenSavePrompt, PromptMode::saveBeforeQuit); - GameActions::Execute(&loadOrQuitAction, getGameState()); - } - if (has_to_free) - { - free(data); - } + loadOrQuitAction.Execute(getGameState()); } } @@ -2980,7 +2962,7 @@ namespace OpenRCT2::Network packet >> tick >> actionType; MemoryStream stream; - const size_t size = packet.Header.Size - packet.BytesRead; + const size_t size = packet.Header.size - packet.BytesRead; stream.WriteArray(packet.Read(size), size); stream.SetPosition(0); @@ -3070,7 +3052,7 @@ namespace OpenRCT2::Network } DataSerialiser stream(false); - const size_t size = packet.Header.Size - packet.BytesRead; + const size_t size = packet.Header.size - packet.BytesRead; stream.GetStream().WriteArray(packet.Read(size), size); stream.GetStream().SetPosition(0); @@ -3306,9 +3288,14 @@ namespace OpenRCT2::Network GetContext()->GetNetwork().Update(); } - void ProcessPending() + void Tick() { - GetContext()->GetNetwork().ProcessPending(); + GetContext()->GetNetwork().Tick(); + } + + void PostTick() + { + GetContext()->GetNetwork().PostTick(); } void Flush() @@ -4147,10 +4134,13 @@ namespace OpenRCT2::Network void SendGameAction(const GameActions::GameAction* action) { } + void Tick() + { + } void Update() { } - void ProcessPending() + void PostTick() { } int32_t BeginClient(const std::string& host, int32_t port) diff --git a/src/openrct2/network/NetworkBase.h b/src/openrct2/network/NetworkBase.h index fdf9b08f9a..006ab80a7e 100644 --- a/src/openrct2/network/NetworkBase.h +++ b/src/openrct2/network/NetworkBase.h @@ -11,6 +11,7 @@ #include "NetworkTypes.h" #include "NetworkUser.h" +#include #include #include #include @@ -37,10 +38,10 @@ namespace OpenRCT2::Network bool Init(); void Close(); uint32_t GetServerTick() const noexcept; - // FIXME: This is currently the wrong function to override in System, will be refactored later. - void Update() override final; + void Update() final; + void Tick() final; void Flush(); - void ProcessPending(); + void PostTick() final; void ProcessPlayerList(); auto GetPlayerIteratorByID(uint8_t id) const; auto GetGroupIteratorByID(uint8_t id) const; @@ -61,6 +62,7 @@ namespace OpenRCT2::Network void CloseConnection(); Player* AddPlayer(const std::string& name, const std::string& keyhash); void ProcessPacket(Connection& connection, Packet& packet); + bool UpdateConnection(Connection& connection); public: // Server Connection* GetPlayerConnection(uint8_t id) const; @@ -81,6 +83,7 @@ namespace OpenRCT2::Network void SetupDefaultGroups(); void RemovePlayer(std::unique_ptr& connection); void UpdateServer(); + void TickServer(); void ServerClientDisconnected(std::unique_ptr& connection); bool SaveMap(IStream* stream, const std::vector& objects) const; std::vector SaveForNetwork(const std::vector& objects) const; @@ -136,6 +139,7 @@ namespace OpenRCT2::Network void ServerClientDisconnected(); bool LoadMap(IStream* stream); void UpdateClient(); + void TickClient(); // Packet dispatchers. void Client_Send_RequestGameState(uint32_t tick); @@ -152,6 +156,7 @@ namespace OpenRCT2::Network // Handlers. void Client_Handle_AUTH(Connection& connection, Packet& packet); + void Client_Handle_BEGINMAP([[maybe_unused]] Connection& connection, Packet& packet); void Client_Handle_MAP(Connection& connection, Packet& packet); void Client_Handle_CHAT(Connection& connection, Packet& packet); void Client_Handle_GAME_ACTION(Connection& connection, Packet& packet); @@ -167,10 +172,8 @@ namespace OpenRCT2::Network void Client_Handle_EVENT(Connection& connection, Packet& packet); void Client_Handle_TOKEN(Connection& connection, Packet& packet); void Client_Handle_OBJECTS_LIST(Connection& connection, Packet& packet); - void Client_Handle_SCRIPTS_HEADER(Connection& connection, Packet& packet); void Client_Handle_SCRIPTS_DATA(Connection& connection, Packet& packet); void Client_Handle_GAMESTATE(Connection& connection, Packet& packet); - std::vector _challenge; std::map _gameActionCallbacks; Key _key; @@ -190,7 +193,6 @@ namespace OpenRCT2::Network private: // Common Data using CommandHandler = void (NetworkBase::*)(Connection& connection, Packet& packet); - std::vector chunk_buffer; std::ofstream _chat_log_fs; uint32_t _lastUpdateTime = 0; uint32_t _currentDeltaTime = 0; @@ -223,19 +225,11 @@ namespace OpenRCT2::Network std::string spriteHash; }; - struct ServerScriptsData - { - uint32_t pluginCount{}; - uint32_t dataSize{}; - MemoryStream data; - }; - std::unordered_map client_command_handlers; std::unique_ptr _serverConnection; std::map _pendingPlayerLists; std::multimap _pendingPlayerInfo; std::map _serverTickData; - std::vector _missingObjects; std::string _host; std::string _chatLogPath; std::string _chatLogFilenameFormat = "%Y%m%d-%H%M%S.txt"; @@ -252,7 +246,6 @@ namespace OpenRCT2::Network SocketStatus _lastConnectStatus = SocketStatus::closed; bool _requireReconnect = false; bool _clientMapLoaded = false; - ServerScriptsData _serverScriptsData{}; }; } // namespace OpenRCT2::Network diff --git a/src/openrct2/network/NetworkConnection.cpp b/src/openrct2/network/NetworkConnection.cpp index 2e86d59a5c..36fd6866d7 100644 --- a/src/openrct2/network/NetworkConnection.cpp +++ b/src/openrct2/network/NetworkConnection.cpp @@ -11,6 +11,8 @@ #include "NetworkConnection.h" + #include "../Diagnostic.h" + #include "../core/Diagnostics.hpp" #include "../core/String.hpp" #include "../localisation/Formatting.h" #include "../platform/Platform.h" @@ -22,42 +24,80 @@ namespace OpenRCT2::Network { static constexpr size_t kDisconnectReasonBufSize = 256; - static constexpr size_t kBufferSize = (1024 * 64) - 1; // 64 KiB, maximum packet size. - #ifndef DEBUG - static constexpr size_t kNoDataTimeout = 20; // Seconds. - #endif - - static_assert(kBufferSize <= std::numeric_limits::max(), "kBufferSize too big, uint16_t is max."); + static constexpr size_t kBufferSize = 1024 * 128; // 128 KiB. + static constexpr size_t kNoDataTimeout = 40; // Seconds. Connection::Connection() noexcept { - ResetLastPacketTime(); + _lastReceiveTime = Platform::GetTicks(); + } + + void Connection::update() + { + if (!IsValid()) + { + return; + } + + receiveData(); + SendQueuedData(); + } + + void Connection::receiveData() + { + uint8_t buffer[kBufferSize]; + size_t bytesRead = 0; + + ReadPacket status = Socket->ReceiveData(buffer, sizeof(buffer), &bytesRead); + if (status == ReadPacket::disconnected) + { + Disconnect(); + return; + } + + if (status == ReadPacket::success) + { + _lastReceiveTime = Platform::GetTicks(); + _inboundBuffer.insert(_inboundBuffer.end(), buffer, buffer + bytesRead); + } } ReadPacket Connection::readPacket() { - size_t bytesRead = 0; + uint32_t magic = 0; - // Read packet header. - auto& header = InboundPacket.Header; - if (InboundPacket.BytesTransferred < sizeof(InboundPacket.Header)) + // Check if we have enough data for the magic. + if (_inboundBuffer.size() < sizeof(magic)) { - const size_t missingLength = sizeof(header) - InboundPacket.BytesTransferred; + return ReadPacket::moreData; + } - uint8_t* buffer = reinterpret_cast(&InboundPacket.Header); + // Read magic. + std::memcpy(&magic, _inboundBuffer.data(), sizeof(magic)); - ReadPacket status = Socket->ReceiveData(buffer, missingLength, &bytesRead); - if (status != ReadPacket::success) - { - return status; - } + size_t totalPacketLength = 0; + size_t headerSize = 0; - InboundPacket.BytesTransferred += bytesRead; - if (InboundPacket.BytesTransferred < sizeof(InboundPacket.Header)) - { - // If still not enough data for header, keep waiting. - return ReadPacket::moreData; - } + magic = Convert::NetworkToHost(magic); + if (magic == PacketHeader::kMagic) + { + // New format. + auto& header = InboundPacket.Header; + std::memcpy(&header, _inboundBuffer.data(), sizeof(header)); + + header.magic = magic; + header.version = Convert::NetworkToHost(header.version); + header.size = Convert::NetworkToHost(header.size); + header.id = Convert::NetworkToHost(header.id); + + headerSize = sizeof(header); + totalPacketLength = sizeof(header) + header.size; + } + else + { + // Legacy format. + PacketLegacyHeader header{}; + std::memcpy(&header, _inboundBuffer.data(), sizeof(header)); // Normalise values. header.Size = Convert::NetworkToHost(header.Size); @@ -65,61 +105,81 @@ namespace OpenRCT2::Network // NOTE: For compatibility reasons for the master server we need to remove sizeof(Header.Id) from the size. // Previously the Id field was not part of the header rather part of the body. - header.Size -= std::min(header.Size, sizeof(header.Id)); + // We correct the size to have only the length of the body. + if (header.Size < sizeof(header.Id)) + { + // This is a malformed packet, disconnect. + LOG_INFO( + "Received malformed packet (size: %u) from {%s}, disconnecting.", header.Size, + Socket->GetIpAddress().c_str()); - // Fall-through: Read rest of packet. + Disconnect(); + return ReadPacket::disconnected; + } + + header.Size -= sizeof(header.Id); + + // Fill in new header format. + InboundPacket.Header.magic = PacketHeader::kMagic; + InboundPacket.Header.size = header.Size; + InboundPacket.Header.id = header.Id; + + headerSize = sizeof(header); + totalPacketLength = sizeof(header) + header.Size; + + _isLegacyProtocol = true; + } + + if (_inboundBuffer.size() < totalPacketLength) + { + InboundPacket.BytesTransferred = _inboundBuffer.size(); + return ReadPacket::moreData; } // Read packet body. - { - // NOTE: BytesTransfered includes the header length, this will not underflow. - const size_t missingLength = header.Size - (InboundPacket.BytesTransferred - sizeof(header)); + InboundPacket.BytesTransferred = totalPacketLength; + InboundPacket.Write(_inboundBuffer.data() + headerSize, totalPacketLength - headerSize); - uint8_t buffer[kBufferSize]; + // Remove read data from buffer. + _inboundBuffer.erase(_inboundBuffer.begin(), _inboundBuffer.begin() + totalPacketLength); - if (missingLength > 0) - { - ReadPacket status = Socket->ReceiveData(buffer, std::min(missingLength, kBufferSize), &bytesRead); - if (status != ReadPacket::success) - { - return status; - } + RecordPacketStats(InboundPacket, false); - InboundPacket.BytesTransferred += bytesRead; - InboundPacket.Write(buffer, bytesRead); - } - - if (InboundPacket.Data.size() == header.Size) - { - // Received complete packet. - _lastPacketTime = Platform::GetTicks(); - - RecordPacketStats(InboundPacket, false); - - return ReadPacket::success; - } - } - - return ReadPacket::moreData; + return ReadPacket::success; } - static sfl::small_vector serializePacket(const Packet& packet) + static sfl::small_vector serializePacket(bool legacyProtocol, const Packet& packet) { - // NOTE: For compatibility reasons for the master server we need to add sizeof(Header.Id) to the size. - // Previously the Id field was not part of the header rather part of the body. - const auto bodyLength = packet.Data.size() + sizeof(packet.Header.Id); - - Guard::Assert(bodyLength <= std::numeric_limits::max(), "Packet size too large"); - - auto header = packet.Header; - header.Size = static_cast(bodyLength); - header.Size = Convert::HostToNetwork(header.Size); - header.Id = ByteSwapBE(header.Id); - sfl::small_vector buffer; - buffer.reserve(sizeof(header) + packet.Data.size()); - buffer.insert(buffer.end(), reinterpret_cast(&header), reinterpret_cast(&header) + sizeof(header)); + if (legacyProtocol) + { + // NOTE: For compatibility reasons for the master server we need to add sizeof(Header.Id) to the size. + // Previously the Id field was not part of the header rather part of the body. + const auto bodyLength = packet.Data.size() + sizeof(PacketLegacyHeader::Id); + + Guard::Assert(bodyLength <= std::numeric_limits::max(), "Packet size too large"); + + PacketLegacyHeader header{}; + header.Size = static_cast(bodyLength); + header.Size = Convert::HostToNetwork(header.Size); + header.Id = ByteSwapBE(packet.Header.id); + + buffer.insert( + buffer.end(), reinterpret_cast(&header), reinterpret_cast(&header) + sizeof(header)); + } + else + { + PacketHeader header{}; + header.magic = Convert::HostToNetwork(PacketHeader::kMagic); + header.version = Convert::HostToNetwork(PacketHeader::kVersion); + header.size = Convert::HostToNetwork(static_cast(packet.Data.size())); + header.id = Convert::HostToNetwork(packet.Header.id); + + buffer.insert( + buffer.end(), reinterpret_cast(&header), reinterpret_cast(&header) + sizeof(header)); + } + buffer.insert(buffer.end(), packet.Data.begin(), packet.Data.end()); return buffer; @@ -129,7 +189,7 @@ namespace OpenRCT2::Network { if (AuthStatus == Auth::ok || !packet.CommandRequiresAuth()) { - const auto payload = serializePacket(packet); + const auto payload = serializePacket(_isLegacyProtocol, packet); if (front) { _outboundBuffer.insert(_outboundBuffer.begin(), payload.begin(), payload.end()); @@ -168,20 +228,16 @@ namespace OpenRCT2::Network } } - void Connection::ResetLastPacketTime() noexcept + bool Connection::ReceivedDataRecently() const noexcept { - _lastPacketTime = Platform::GetTicks(); - } - - bool Connection::ReceivedPacketRecently() const noexcept - { - #ifndef DEBUG constexpr auto kTimeoutMs = kNoDataTimeout * 1000; - if (Platform::GetTicks() > _lastPacketTime + kTimeoutMs) + + const auto timeSinceLastRecv = Platform::GetTicks() - _lastReceiveTime; + if (timeSinceLastRecv > kTimeoutMs) { return false; } - #endif + return true; } @@ -231,6 +287,22 @@ namespace OpenRCT2::Network stats.bytesReceived[EnumValue(StatisticsGroup::Total)] += packetSize; } } + + Command Connection::getPendingPacketCommand() const noexcept + { + return InboundPacket.GetCommand(); + } + + size_t Connection::getPendingPacketSize() const noexcept + { + return InboundPacket.Header.size; + } + + size_t Connection::getPendingPacketAvailable() const noexcept + { + return InboundPacket.BytesTransferred; + } + } // namespace OpenRCT2::Network #endif diff --git a/src/openrct2/network/NetworkConnection.h b/src/openrct2/network/NetworkConnection.h index e06333a6fa..6ed1045858 100644 --- a/src/openrct2/network/NetworkConnection.h +++ b/src/openrct2/network/NetworkConnection.h @@ -45,28 +45,35 @@ namespace OpenRCT2::Network Connection() noexcept; + void update(); ReadPacket readPacket(); void QueuePacket(const Packet& packet, bool front = false); + Command getPendingPacketCommand() const noexcept; + size_t getPendingPacketSize() const noexcept; + size_t getPendingPacketAvailable() const noexcept; + // This will not immediately disconnect the client. The disconnect // will happen post-tick. void Disconnect() noexcept; bool IsValid() const; void SendQueuedData(); - void ResetLastPacketTime() noexcept; - bool ReceivedPacketRecently() const noexcept; + bool ReceivedDataRecently() const noexcept; const utf8* GetLastDisconnectReason() const noexcept; void SetLastDisconnectReason(std::string_view src); void SetLastDisconnectReason(const StringId string_id, void* args = nullptr); private: + std::vector _inboundBuffer; std::vector _outboundBuffer; - uint32_t _lastPacketTime = 0; + uint32_t _lastReceiveTime = 0; std::string _lastDisconnectReason; + bool _isLegacyProtocol = false; void RecordPacketStats(const Packet& packet, bool sending); + void receiveData(); }; } // namespace OpenRCT2::Network diff --git a/src/openrct2/network/NetworkPacket.cpp b/src/openrct2/network/NetworkPacket.cpp index 87ef06317c..82a87808ba 100644 --- a/src/openrct2/network/NetworkPacket.cpp +++ b/src/openrct2/network/NetworkPacket.cpp @@ -18,7 +18,7 @@ namespace OpenRCT2::Network { Packet::Packet(Command id) noexcept - : Header{ 0, id } + : Header{ PacketHeader::kMagic, PacketHeader::kVersion, 0, id } { } @@ -34,7 +34,7 @@ namespace OpenRCT2::Network Command Packet::GetCommand() const noexcept { - return Header.Id; + return Header.id; } void Packet::Clear() noexcept diff --git a/src/openrct2/network/NetworkPacket.h b/src/openrct2/network/NetworkPacket.h index ce69859e3f..72688485aa 100644 --- a/src/openrct2/network/NetworkPacket.h +++ b/src/openrct2/network/NetworkPacket.h @@ -19,12 +19,23 @@ namespace OpenRCT2::Network { #pragma pack(push, 1) - struct PacketHeader + struct PacketLegacyHeader { uint16_t Size = 0; Command Id = Command::invalid; }; - static_assert(sizeof(PacketHeader) == 6); + static_assert(sizeof(PacketLegacyHeader) == 6); + + struct PacketHeader + { + static constexpr uint32_t kMagic = 0x3254524F; // 'ORT2' + static constexpr uint16_t kVersion = 2; + + uint32_t magic{}; + uint16_t version{}; + uint32_t size{}; + Command id{}; + }; #pragma pack(pop) struct Packet final @@ -49,7 +60,7 @@ namespace OpenRCT2::Network template Packet& operator>>(T& value) { - if (BytesRead + sizeof(value) > Header.Size) + if (BytesRead + sizeof(value) > Header.size) { value = T{}; } diff --git a/src/openrct2/network/NetworkTypes.h b/src/openrct2/network/NetworkTypes.h index 255d03eab0..fc027f478d 100644 --- a/src/openrct2/network/NetworkTypes.h +++ b/src/openrct2/network/NetworkTypes.h @@ -82,9 +82,10 @@ namespace OpenRCT2::Network playerInfo, requestGameState, gameState, - scriptsHeader, + scriptsHeader, // Deprecated. scriptsData, heartbeat, + beginMap, max, invalid = static_cast(-1), }; diff --git a/src/openrct2/network/Socket.cpp b/src/openrct2/network/Socket.cpp index 73e2447cd7..795b96bcda 100644 --- a/src/openrct2/network/Socket.cpp +++ b/src/openrct2/network/Socket.cpp @@ -982,17 +982,4 @@ namespace OpenRCT2::Network } // namespace OpenRCT2::Network -namespace OpenRCT2::Convert -{ - uint16_t HostToNetwork(uint16_t value) - { - return htons(value); - } - - uint16_t NetworkToHost(uint16_t value) - { - return ntohs(value); - } -} // namespace OpenRCT2::Convert - #endif diff --git a/src/openrct2/network/Socket.h b/src/openrct2/network/Socket.h index e5e9ac0771..034e615d7e 100644 --- a/src/openrct2/network/Socket.h +++ b/src/openrct2/network/Socket.h @@ -9,6 +9,8 @@ #pragma once +#include "../core/Endianness.h" + #include #include #include @@ -106,6 +108,24 @@ namespace OpenRCT2::Network namespace OpenRCT2::Convert { - uint16_t HostToNetwork(uint16_t value); - uint16_t NetworkToHost(uint16_t value); + template + constexpr T HostToNetwork(T value) + { + if constexpr (std::endian::native == std::endian::big) + { + return value; // already network order + } + else + { + return ByteSwapBE(value); + } + } + + template + constexpr T NetworkToHost(T value) + { + // Conversion is symmetric + return HostToNetwork(value); + } + } // namespace OpenRCT2::Convert