diff --git a/binaries/data/mods/public/gui/common/network.js b/binaries/data/mods/public/gui/common/network.js index c0bb5b377e..b629082d54 100644 --- a/binaries/data/mods/public/gui/common/network.js +++ b/binaries/data/mods/public/gui/common/network.js @@ -79,12 +79,31 @@ function getDisconnectReason(id, wasConnected) case 13: return translate("Password is invalid."); case 14: return translate("Could not find an unused port for the enet STUN client."); case 15: return translate("Could not find the STUN endpoint."); + case 16: return translate("Different game engine versions or different mods loaded."); default: warn("Unknown disconnect-reason ID received: " + id); return sprintf(translate("\\[Invalid value %(id)s]"), { "id": id }); } } +function getHandshakeDisconnectMessage(mismatchType, clientMismatchInfo, serverMismatchInfo) +{ + switch (mismatchType) + { + case "engine": return sprintf(translate("Different engine versions detected client version: %(clientMismatch)s server version: %(serverMismatch)s"), { + "clientMismatch": clientMismatchInfo, + "serverMismatch": serverMismatchInfo + }); + case "mod": return sprintf(translate("Different mods enabled, or mods enabled in a different order. Client mod: '%(clientMismatch)s' Server mod: '%(serverMismatch)s'"), { + "clientMismatch": clientMismatchInfo, + "serverMismatch": serverMismatchInfo + }); + default: return sprintf(translate("Unrecognised handshake mismatch type: %(mismatchType)s"), { + "mismatchType": mismatchType + }); + } +} + /** * Show the disconnect reason in a message box. * @@ -102,6 +121,16 @@ function reportDisconnect(reason, wasConnected) ); } +function reportHandshakeDisconnect(mismatchType, clientMismatch, serverMismatch) +{ + messageBox( + 400, 200, + translate("Failed to connect to the server.") + + "\n\n" + getHandshakeDisconnectMessage(mismatchType, clientMismatch, serverMismatch), + translate("Disconnected") + ); +} + function kickError() { addChatMessage({ diff --git a/binaries/data/mods/public/gui/credits/texts/programming.json b/binaries/data/mods/public/gui/credits/texts/programming.json index 257d150a77..5dee992cd4 100644 --- a/binaries/data/mods/public/gui/credits/texts/programming.json +++ b/binaries/data/mods/public/gui/credits/texts/programming.json @@ -252,6 +252,7 @@ { "nick": "RefinedCode" }, { "nick": "Riemer" }, { "nick": "Riesi" }, + { "nick": "roflson", "name": "Paul Robinson" }, { "name": "Rolf Sievers" }, { "nick": "s0600204", "name": "Matthew Norwood" }, { "nick": "sacha_vrand", "name": "Sacha Vrand" }, diff --git a/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js b/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js index 347c37acc9..3a73993658 100644 --- a/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js +++ b/binaries/data/mods/public/gui/gamesetup_mp/gamesetup_mp.js @@ -220,7 +220,10 @@ function pollAndHandleNetworkClient(loadSavedGame) { case "disconnected": cancelSetup(); - reportDisconnect(message.reason, false); + if (message.reason === 16) + reportHandshakeDisconnect(message.mismatch_type, message.client_mismatch, message.server_mismatch); + else + reportDisconnect(message.reason, false); return; default: @@ -285,7 +288,10 @@ function pollAndHandleNetworkClient(loadSavedGame) case "disconnected": cancelSetup(); - reportDisconnect(message.reason, false); + if (message.reason === 16) + reportHandshakeDisconnect(message.mismatch_type, message.client_mismatch_component, message.server_mismatch_component); + else + reportDisconnect(message.reason, false); return; default: diff --git a/source/network/NetClient.cpp b/source/network/NetClient.cpp index 484a7bb4d1..76acb7ada5 100644 --- a/source/network/NetClient.cpp +++ b/source/network/NetClient.cpp @@ -22,6 +22,7 @@ #include "NetClientTurnManager.h" #include "NetEnet.h" #include "NetMessage.h" +#include "NetProtocol.h" #include "NetSession.h" #include "lib/byte_order.h" @@ -439,10 +440,21 @@ void CNetClient::HandleConnect() void CNetClient::HandleDisconnect(u32 reason) { - PushGuiMessage( - "type", "netstatus", - "status", "disconnected", - "reason", reason); + if (reason == NDR_INCORRECT_SOFTWARE_VERSION) { + const auto& mismatch = CheckHandshake(m_ServerHandshake, CreateHandshake()); + ENSURE(mismatch.has_value()); + PushGuiMessage( + "type", "netstatus", + "status", "disconnected", + "reason", reason, + "mismatch_type", mismatch->componentType, + "client_mismatch_component", mismatch->clientComponent, + "server_mismatch_component", mismatch->serverComponent); + } else + PushGuiMessage( + "type", "netstatus", + "status", "disconnected", + "reason", reason); DestroyConnection(); @@ -665,13 +677,11 @@ bool CNetClient::OnConnect(CNetClient* client, CFsmEvent* event) bool CNetClient::OnHandshake(CNetClient* client, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE); + client->m_ServerHandshake = *static_cast(event->GetParamRef()); + + CCliHandshakeMessage handshake(CreateHandshake()); - CCliHandshakeMessage handshake; - handshake.m_MagicResponse = PS_PROTOCOL_MAGIC_RESPONSE; - handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION; - handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION; client->SendMessage(&handshake); - return true; } diff --git a/source/network/NetClient.h b/source/network/NetClient.h index 2f56bf321b..892af41d04 100644 --- a/source/network/NetClient.h +++ b/source/network/NetClient.h @@ -22,6 +22,7 @@ #include "network/FSM.h" #include "network/NetFileTransfer.h" #include "network/NetHost.h" +#include "network/NetMessage.h" #include "scriptinterface/Object.h" #include "ps/CStr.h" @@ -367,6 +368,9 @@ private: /// Time when the server was last checked for timeouts and bad latency std::time_t m_LastConnectionCheck; + + /// Record of the server engine version and loaded mods + CSrvHandshakeMessage m_ServerHandshake; }; /// Global network client for the standard game diff --git a/source/network/NetHost.h b/source/network/NetHost.h index e842a36d0f..d2cdd0a573 100644 --- a/source/network/NetHost.h +++ b/source/network/NetHost.h @@ -76,7 +76,8 @@ enum NetDisconnectReason NDR_INCORRECT_READY_TURN_SIMULATED, NDR_SERVER_REFUSED, NDR_STUN_PORT_FAILED, - NDR_STUN_ENDPOINT_FAILED + NDR_STUN_ENDPOINT_FAILED, + NDR_INCORRECT_SOFTWARE_VERSION }; class CNetHost diff --git a/source/network/NetMessages.h b/source/network/NetMessages.h index aa6220a2fc..f1c50ad152 100644 --- a/source/network/NetMessages.h +++ b/source/network/NetMessages.h @@ -28,7 +28,7 @@ #define PS_PROTOCOL_MAGIC 0x5073013f // 'P', 's', 0x01, '?' #define PS_PROTOCOL_MAGIC_RESPONSE 0x50630121 // 'P', 'c', 0x01, '!' -#define PS_PROTOCOL_VERSION 0x01010018 // Arbitrary protocol +#define PS_PROTOCOL_VERSION 0x01010019 // Arbitrary protocol #define PS_DEFAULT_PORT 0x5073 // 'P', 's' // Set when lobby authentication is required. Used in the SrvHandshakeResponseMessage. @@ -107,13 +107,21 @@ START_NMTS() START_NMT_CLASS_(SrvHandshake, NMT_SERVER_HANDSHAKE) NMT_FIELD_INT(m_Magic, u32, 4) NMT_FIELD_INT(m_ProtocolVersion, u32, 4) - NMT_FIELD_INT(m_SoftwareVersion, u32, 4) + NMT_FIELD(CStr, m_EngineVersion) + NMT_START_ARRAY(m_EnabledMods) + NMT_FIELD(CStr, m_Name) + NMT_FIELD(CStr, m_Version) + NMT_END_ARRAY() END_NMT_CLASS() START_NMT_CLASS_(CliHandshake, NMT_CLIENT_HANDSHAKE) NMT_FIELD_INT(m_MagicResponse, u32, 4) NMT_FIELD_INT(m_ProtocolVersion, u32, 4) - NMT_FIELD_INT(m_SoftwareVersion, u32, 4) + NMT_FIELD(CStr, m_EngineVersion) + NMT_START_ARRAY(m_EnabledMods) + NMT_FIELD(CStr, m_Name) + NMT_FIELD(CStr, m_Version) + NMT_END_ARRAY() END_NMT_CLASS() START_NMT_CLASS_(SrvHandshakeResponse, NMT_SERVER_HANDSHAKE_RESPONSE) diff --git a/source/network/NetProtocol.cpp b/source/network/NetProtocol.cpp new file mode 100644 index 0000000000..ec1b2dfb73 --- /dev/null +++ b/source/network/NetProtocol.cpp @@ -0,0 +1,56 @@ +/* Copyright (C) 2025 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#include "precompiled.h" + +#include "NetProtocol.h" + +namespace +{ +template +std::string GetModName(const ModType& mod) +{ + return mod.m_Name + "-" + mod.m_Version; +} +} + +std::optional CheckHandshake(const CSrvHandshakeMessage& serverMessage, const CCliHandshakeMessage& clientMessage) +{ + if (serverMessage.m_EngineVersion != clientMessage.m_EngineVersion) + return HandshakeError{"engine", clientMessage.m_EngineVersion, serverMessage.m_EngineVersion}; + + const auto& serverMods = serverMessage.m_EnabledMods; + const auto& clientMods = clientMessage.m_EnabledMods; + + for (uint32_t i = 0; i < std::max(clientMods.size(), serverMods.size()); ++i) + { + if (i >= serverMods.size()) + return HandshakeError{"mod", GetModName(clientMods[i]), ""}; + + if (i >= clientMods.size()) + return HandshakeError{"mod", "", GetModName(serverMods[i])}; + + const std::string serverMod = GetModName(serverMods[i]); + const std::string clientMod = GetModName(clientMods[i]); + + // Client and server have different mods enabled, or mods enabled in a different order + if (clientMod != serverMod) + return HandshakeError{"mod", clientMod, serverMod}; + } + + return {}; +} diff --git a/source/network/NetProtocol.h b/source/network/NetProtocol.h new file mode 100644 index 0000000000..83b8709d24 --- /dev/null +++ b/source/network/NetProtocol.h @@ -0,0 +1,65 @@ +/* Copyright (C) 2025 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef NETPROTOCOL_H +#define NETPROTOCOL_H + +#include "NetMessage.h" +#include "network/NetMessage.h" +#include "ps/Pyrogenesis.h" +#include "ps/Mod.h" + +#include +#include +#include + +struct HandshakeError +{ + std::string componentType; + std::string clientComponent; + std::string serverComponent; +}; + +template +Message CreateHandshake() { + Message handshake; + + if constexpr (std::is_same_v) + handshake.m_MagicResponse = PS_PROTOCOL_MAGIC_RESPONSE; + else + handshake.m_Magic = PS_PROTOCOL_MAGIC; + + handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION; + handshake.m_EngineVersion = engine_version; + + for (const Mod::ModData* mod : Mod::Instance().GetEnabledModsData()) + { + if (mod->m_IgnoreInCompatibilityChecks) + continue; + + typename Message::S_m_EnabledMods enabledMod; + enabledMod.m_Name = mod->m_Name; + enabledMod.m_Version = mod->m_Version; + handshake.m_EnabledMods.push_back(enabledMod); + } + + return handshake; +} + +std::optional CheckHandshake(const CSrvHandshakeMessage& serverMessage, const CCliHandshakeMessage& clientMessage); + +#endif \ No newline at end of file diff --git a/source/network/NetServer.cpp b/source/network/NetServer.cpp index 37fe2c96f3..ff6b40e469 100644 --- a/source/network/NetServer.cpp +++ b/source/network/NetServer.cpp @@ -25,6 +25,7 @@ #include "NetSession.h" #include "NetServerTurnManager.h" #include "NetStats.h" +#include "NetProtocol.h" #include "lib/external_libraries/enet.h" #include "lib/types.h" @@ -708,11 +709,8 @@ bool CNetServerWorker::HandleConnect(CNetServerSession* session) return false; } - CSrvHandshakeMessage handshake; - handshake.m_Magic = PS_PROTOCOL_MAGIC; - handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION; - handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION; - return session->SendMessage(&handshake); + const CSrvHandshakeMessage handshake(CreateHandshake()); + return session->SendMessage(&handshake); } void CNetServerWorker::OnUserJoin(CNetServerSession* session) @@ -920,6 +918,12 @@ bool CNetServerWorker::OnClientHandshake(CNetServerSession* session, CFsmEvent* return false; } + if (CheckHandshake(CreateHandshake(), *message)) + { + session->Disconnect(NDR_INCORRECT_SOFTWARE_VERSION); + return false; + } + CStr guid = ps_generate_guid(); int count = 0; // Ensure unique GUID