1
0
mirror of https://github.com/OpenRCT2/OpenRCT2 synced 2026-01-23 06:44:38 +01:00

Refactor the network code also change protocol (#25575)

* Change the network protocol and refactor the code, still supports the old format for now

* Bump up network version

* Update changelog.txt
This commit is contained in:
Matt
2025-12-03 00:06:39 +02:00
committed by GitHub
parent 628b6af059
commit d8698726c9
16 changed files with 456 additions and 356 deletions

View File

@@ -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…

View File

@@ -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.

View File

@@ -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<uint32_t>(sleepTimeSec * 1000.f));
return;
}

View File

@@ -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

View File

@@ -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++;

View File

@@ -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,

View File

@@ -53,7 +53,8 @@ namespace OpenRCT2::Network
void SendTick();
bool GamestateSnapshotsEnabled();
void Update();
void ProcessPending();
void Tick();
void PostTick();
void Flush();
[[nodiscard]] Auth GetAuthstatus();

View File

@@ -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<uint32_t>(0) << static_cast<uint32_t>(objects.size());
Packet packet(Command::objectsList);
connection.QueuePacket(std::move(packet));
}
else
// Count.
packet << static_cast<uint32_t>(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<uint32_t>(i) << static_cast<uint32_t>(objects.size());
if (object->Identifier.empty())
{
// DAT
LOG_VERBOSE("Object %.8s (checksum %x)", object->ObjectEntry.name, object->ObjectEntry.checksum);
packet << static_cast<uint8_t>(0);
packet.Write(&object->ObjectEntry, sizeof(RCTObjectEntry));
}
else
{
// JSON
LOG_VERBOSE("Object %s", object->Identifier.c_str());
packet << static_cast<uint8_t>(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<uint8_t>(0);
packet.Write(&object->ObjectEntry, sizeof(RCTObjectEntry));
}
else
{
// JSON
LOG_VERBOSE("Object %s", object->Identifier.c_str());
packet << static_cast<uint8_t>(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<uint32_t>(remotePlugins.size());
for (auto& plugin : remotePlugins)
{
const auto& code = plugin->GetCode();
const auto codeSize = static_cast<uint32_t>(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<uint32_t>(remotePlugins.size());
packetScriptHeader << static_cast<uint32_t>(pluginData.GetLength());
connection.QueuePacket(std::move(packetScriptHeader));
// Segment the plugin data into chunks and send them.
const uint8_t* pluginDataBuffer = static_cast<const uint8_t*>(pluginData.GetData());
uint32_t dataOffset = 0;
while (dataOffset < pluginData.GetLength())
{
const uint32_t chunkSize = std::min<uint32_t>(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<uint32_t>(0u);
packetScriptHeader << static_cast<uint32_t>(0u);
packet << static_cast<uint32_t>(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<uint32_t>(header.size()) << static_cast<uint32_t>(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<uint32_t>(bytesReceived), static_cast<uint32_t>(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<ObjectEntryDescriptor> 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<uint32_t>();
const auto scriptData = scriptsData.ReadArray<char>(codeSize);
const uint8_t* scriptData = packet.Read(codeSize);
auto code = std::string_view(reinterpret_cast<const char*>(scriptData.get()), codeSize);
scriptEngine.AddNetworkPlugin(code);
}
Guard::Assert(scriptsData.GetPosition() == scriptsData.GetLength());
auto code = std::string_view(reinterpret_cast<const char*>(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<int32_t>(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<void*>(static_cast<const void*>(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)

View File

@@ -11,6 +11,7 @@
#include "NetworkTypes.h"
#include "NetworkUser.h"
#include <chrono>
#include <fstream>
#include <list>
#include <memory>
@@ -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>& connection);
void UpdateServer();
void TickServer();
void ServerClientDisconnected(std::unique_ptr<Connection>& connection);
bool SaveMap(IStream* stream, const std::vector<const ObjectRepositoryItem*>& objects) const;
std::vector<uint8_t> SaveForNetwork(const std::vector<const ObjectRepositoryItem*>& 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<uint8_t> _challenge;
std::map<uint32_t, GameActions::GameAction::Callback_t> _gameActionCallbacks;
Key _key;
@@ -190,7 +193,6 @@ namespace OpenRCT2::Network
private: // Common Data
using CommandHandler = void (NetworkBase::*)(Connection& connection, Packet& packet);
std::vector<uint8_t> 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<Command, CommandHandler> client_command_handlers;
std::unique_ptr<Connection> _serverConnection;
std::map<uint32_t, PlayerListUpdate> _pendingPlayerLists;
std::multimap<uint32_t, Player> _pendingPlayerInfo;
std::map<uint32_t, ServerTickData> _serverTickData;
std::vector<ObjectEntryDescriptor> _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

View File

@@ -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<uint16_t>::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<uint8_t*>(&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<uint16_t>(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<uint8_t, 512> serializePacket(const Packet& packet)
static sfl::small_vector<uint8_t, 512> 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<uint16_t>::max(), "Packet size too large");
auto header = packet.Header;
header.Size = static_cast<uint16_t>(bodyLength);
header.Size = Convert::HostToNetwork(header.Size);
header.Id = ByteSwapBE(header.Id);
sfl::small_vector<uint8_t, 512> buffer;
buffer.reserve(sizeof(header) + packet.Data.size());
buffer.insert(buffer.end(), reinterpret_cast<uint8_t*>(&header), reinterpret_cast<uint8_t*>(&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<uint16_t>::max(), "Packet size too large");
PacketLegacyHeader header{};
header.Size = static_cast<uint16_t>(bodyLength);
header.Size = Convert::HostToNetwork(header.Size);
header.Id = ByteSwapBE(packet.Header.id);
buffer.insert(
buffer.end(), reinterpret_cast<uint8_t*>(&header), reinterpret_cast<uint8_t*>(&header) + sizeof(header));
}
else
{
PacketHeader header{};
header.magic = Convert::HostToNetwork(PacketHeader::kMagic);
header.version = Convert::HostToNetwork(PacketHeader::kVersion);
header.size = Convert::HostToNetwork(static_cast<uint32_t>(packet.Data.size()));
header.id = Convert::HostToNetwork(packet.Header.id);
buffer.insert(
buffer.end(), reinterpret_cast<uint8_t*>(&header), reinterpret_cast<uint8_t*>(&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

View File

@@ -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<uint8_t> _inboundBuffer;
std::vector<uint8_t> _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

View File

@@ -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

View File

@@ -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<typename T>
Packet& operator>>(T& value)
{
if (BytesRead + sizeof(value) > Header.Size)
if (BytesRead + sizeof(value) > Header.size)
{
value = T{};
}

View File

@@ -82,9 +82,10 @@ namespace OpenRCT2::Network
playerInfo,
requestGameState,
gameState,
scriptsHeader,
scriptsHeader, // Deprecated.
scriptsData,
heartbeat,
beginMap,
max,
invalid = static_cast<uint32_t>(-1),
};

View File

@@ -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

View File

@@ -9,6 +9,8 @@
#pragma once
#include "../core/Endianness.h"
#include <memory>
#include <string>
#include <vector>
@@ -106,6 +108,24 @@ namespace OpenRCT2::Network
namespace OpenRCT2::Convert
{
uint16_t HostToNetwork(uint16_t value);
uint16_t NetworkToHost(uint16_t value);
template<typename T>
constexpr T HostToNetwork(T value)
{
if constexpr (std::endian::native == std::endian::big)
{
return value; // already network order
}
else
{
return ByteSwapBE(value);
}
}
template<typename T>
constexpr T NetworkToHost(T value)
{
// Conversion is symmetric
return HostToNetwork(value);
}
} // namespace OpenRCT2::Convert