From e4d216e44b4a5d87094b4478ea4cf18109f99a35 Mon Sep 17 00:00:00 2001 From: Patric Stout Date: Thu, 29 Apr 2021 15:37:02 +0200 Subject: [PATCH] Feature: join servers based on their invite code This removes the need to know a server IP to join it. Invite codes are small (~7 characters) indentifiers for servers, which can be exchanged with other players to join the servers. --- src/console_cmds.cpp | 1 + src/lang/english.txt | 6 +- src/network/core/config.h | 7 +- src/network/core/game_info.cpp | 6 +- src/network/core/tcp_connect.cpp | 4 +- src/network/core/tcp_coordinator.cpp | 24 +- src/network/core/tcp_coordinator.h | 99 +++++++- src/network/network.cpp | 10 +- src/network/network_coordinator.cpp | 239 +++++++++++++++++- src/network/network_coordinator.h | 24 +- src/network/network_gui.cpp | 13 +- src/network/network_internal.h | 1 + src/settings_type.h | 2 + .../settings/network_secrets_settings.ini | 14 + src/widgets/network_widget.h | 1 + 15 files changed, 422 insertions(+), 29 deletions(-) diff --git a/src/console_cmds.cpp b/src/console_cmds.cpp index 9d03f3deba..0fcac44880 100644 --- a/src/console_cmds.cpp +++ b/src/console_cmds.cpp @@ -702,6 +702,7 @@ DEF_CONSOLE_CMD(ConServerInfo) return true; } + IConsolePrint(CC_DEFAULT, "Invite code: {}", _network_server_invite_code); IConsolePrint(CC_DEFAULT, "Current/maximum clients: {:3d}/{:3d}", _network_game_info.clients_on, _settings_client.network.max_clients); IConsolePrint(CC_DEFAULT, "Current/maximum companies: {:3d}/{:3d}", Company::GetNumItems(), _settings_client.network.max_companies); IConsolePrint(CC_DEFAULT, "Current/maximum spectators: {:3d}/{:3d}", NetworkSpectatorCount(), _settings_client.network.max_spectators); diff --git a/src/lang/english.txt b/src/lang/english.txt index 2e84d4b3a1..813705958b 100644 --- a/src/lang/english.txt +++ b/src/lang/english.txt @@ -2045,12 +2045,12 @@ STR_NETWORK_SERVER_LIST_SEARCH_SERVER_INTERNET_TOOLTIP :{BLACK}Search i STR_NETWORK_SERVER_LIST_SEARCH_SERVER_LAN :{BLACK}Search LAN STR_NETWORK_SERVER_LIST_SEARCH_SERVER_LAN_TOOLTIP :{BLACK}Search local area network for servers STR_NETWORK_SERVER_LIST_ADD_SERVER :{BLACK}Add server -STR_NETWORK_SERVER_LIST_ADD_SERVER_TOOLTIP :{BLACK}Adds a server to the list which will always be checked for running games +STR_NETWORK_SERVER_LIST_ADD_SERVER_TOOLTIP :{BLACK}Adds a server to the list. This can either be a server address or an invite code STR_NETWORK_SERVER_LIST_START_SERVER :{BLACK}Start server STR_NETWORK_SERVER_LIST_START_SERVER_TOOLTIP :{BLACK}Start your own server STR_NETWORK_SERVER_LIST_PLAYER_NAME_OSKTITLE :{BLACK}Enter your name -STR_NETWORK_SERVER_LIST_ENTER_IP :{BLACK}Enter the address of the host +STR_NETWORK_SERVER_LIST_ENTER_SERVER_ADDRESS :{BLACK}Enter server address or invite code # Start new multiplayer server STR_NETWORK_START_SERVER_CAPTION :{WHITE}Start new multiplayer game @@ -2136,6 +2136,8 @@ STR_NETWORK_CLIENT_LIST_SERVER_NAME_EDIT_TOOLTIP :{BLACK}Edit the STR_NETWORK_CLIENT_LIST_SERVER_NAME_QUERY_CAPTION :Name of the server STR_NETWORK_CLIENT_LIST_SERVER_VISIBILITY :{BLACK}Visibility STR_NETWORK_CLIENT_LIST_SERVER_VISIBILITY_TOOLTIP :{BLACK}Whether other people can see your server in the public listing +STR_NETWORK_CLIENT_LIST_SERVER_INVITE_CODE :{BLACK}Invite code +STR_NETWORK_CLIENT_LIST_SERVER_INVITE_CODE_TOOLTIP :{BLACK}Invite code other players can use to join this server STR_NETWORK_CLIENT_LIST_SERVER_CONNECTION_TYPE :{BLACK}Connection type STR_NETWORK_CLIENT_LIST_SERVER_CONNECTION_TYPE_TOOLTIP :{BLACK}Whether and how your server can be reached by others STR_NETWORK_CLIENT_LIST_PLAYER :{BLACK}Player diff --git a/src/network/core/config.h b/src/network/core/config.h index d37210e197..06df93140c 100644 --- a/src/network/core/config.h +++ b/src/network/core/config.h @@ -47,7 +47,7 @@ static const uint16 COMPAT_MTU = 1460; ///< Numbe static const byte NETWORK_GAME_ADMIN_VERSION = 1; ///< What version of the admin network do we use? static const byte NETWORK_GAME_INFO_VERSION = 4; ///< What version of game-info do we use? static const byte NETWORK_COMPANY_INFO_VERSION = 6; ///< What version of company info is this? -static const byte NETWORK_COORDINATOR_VERSION = 1; ///< What version of game-coordinator-protocol do we use? +static const byte NETWORK_COORDINATOR_VERSION = 2; ///< What version of game-coordinator-protocol do we use? static const uint NETWORK_NAME_LENGTH = 80; ///< The maximum length of the server name and map name, in bytes including '\0' static const uint NETWORK_COMPANY_NAME_LENGTH = 128; ///< The maximum length of the company name, in bytes including '\0' @@ -67,7 +67,10 @@ static const uint NETWORK_CONTENT_VERSION_LENGTH = 16; ///< The m static const uint NETWORK_CONTENT_URL_LENGTH = 96; ///< The maximum length of a content's url, in bytes including '\0'. static const uint NETWORK_CONTENT_DESC_LENGTH = 512; ///< The maximum length of a content's description, in bytes including '\0'. static const uint NETWORK_CONTENT_TAG_LENGTH = 32; ///< The maximum length of a content's tag, in bytes including '\0'. -static const uint NETWORK_ERROR_DETAIL_LENGTH = 100; ///< The maximum length of the error detail, in bytes including '\0' +static const uint NETWORK_ERROR_DETAIL_LENGTH = 100; ///< The maximum length of the error detail, in bytes including '\0'. +static const uint NETWORK_INVITE_CODE_LENGTH = 64; ///< The maximum length of the invite code, in bytes including '\0'. +static const uint NETWORK_INVITE_CODE_SECRET_LENGTH = 80; ///< The maximum length of the invite code secret, in bytes including '\0'. +static const uint NETWORK_TOKEN_LENGTH = 64; ///< The maximum length of a token, in bytes including '\0'. static const uint NETWORK_GRF_NAME_LENGTH = 80; ///< Maximum length of the name of a GRF diff --git a/src/network/core/game_info.cpp b/src/network/core/game_info.cpp index b4c487fba8..17f00c5f57 100644 --- a/src/network/core/game_info.cpp +++ b/src/network/core/game_info.cpp @@ -141,7 +141,11 @@ void FillStaticNetworkServerGameInfo() */ const NetworkServerGameInfo *GetCurrentNetworkServerGameInfo() { - /* Client_on is used as global variable to keep track on the number of clients. */ + /* These variables are updated inside _network_game_info as if they are global variables: + * - clients_on + * - invite_code + * These don't need to be updated manually here. + */ _network_game_info.companies_on = (byte)Company::GetNumItems(); _network_game_info.spectators_on = NetworkSpectatorCount(); _network_game_info.game_date = _date; diff --git a/src/network/core/tcp_connect.cpp b/src/network/core/tcp_connect.cpp index d9b6bb7818..0e8e2e1251 100644 --- a/src/network/core/tcp_connect.cpp +++ b/src/network/core/tcp_connect.cpp @@ -13,6 +13,7 @@ #include "../../thread.h" #include "tcp.h" +#include "../network_coordinator.h" #include "../network_internal.h" #include @@ -48,8 +49,7 @@ TCPServerConnecter::TCPServerConnecter(const std::string &connection_string, uin case SERVER_ADDRESS_INVITE_CODE: this->status = Status::CONNECTING; - - // TODO -- The next commit will add this functionality. + _network_coordinator_client.ConnectToServer(this->server_address.connection_string, this); break; default: diff --git a/src/network/core/tcp_coordinator.cpp b/src/network/core/tcp_coordinator.cpp index bbcb59b14d..dfd73147e1 100644 --- a/src/network/core/tcp_coordinator.cpp +++ b/src/network/core/tcp_coordinator.cpp @@ -27,12 +27,18 @@ bool NetworkCoordinatorSocketHandler::HandlePacket(Packet *p) PacketCoordinatorType type = (PacketCoordinatorType)p->Recv_uint8(); switch (type) { - case PACKET_COORDINATOR_GC_ERROR: return this->Receive_GC_ERROR(p); - case PACKET_COORDINATOR_SERVER_REGISTER: return this->Receive_SERVER_REGISTER(p); - case PACKET_COORDINATOR_GC_REGISTER_ACK: return this->Receive_GC_REGISTER_ACK(p); - case PACKET_COORDINATOR_SERVER_UPDATE: return this->Receive_SERVER_UPDATE(p); - case PACKET_COORDINATOR_CLIENT_LISTING: return this->Receive_CLIENT_LISTING(p); - case PACKET_COORDINATOR_GC_LISTING: return this->Receive_GC_LISTING(p); + case PACKET_COORDINATOR_GC_ERROR: return this->Receive_GC_ERROR(p); + case PACKET_COORDINATOR_SERVER_REGISTER: return this->Receive_SERVER_REGISTER(p); + case PACKET_COORDINATOR_GC_REGISTER_ACK: return this->Receive_GC_REGISTER_ACK(p); + case PACKET_COORDINATOR_SERVER_UPDATE: return this->Receive_SERVER_UPDATE(p); + case PACKET_COORDINATOR_CLIENT_LISTING: return this->Receive_CLIENT_LISTING(p); + case PACKET_COORDINATOR_GC_LISTING: return this->Receive_GC_LISTING(p); + case PACKET_COORDINATOR_CLIENT_CONNECT: return this->Receive_CLIENT_CONNECT(p); + case PACKET_COORDINATOR_GC_CONNECTING: return this->Receive_GC_CONNECTING(p); + case PACKET_COORDINATOR_SERCLI_CONNECT_FAILED: return this->Receive_SERCLI_CONNECT_FAILED(p); + case PACKET_COORDINATOR_GC_CONNECT_FAILED: return this->Receive_GC_CONNECT_FAILED(p); + case PACKET_COORDINATOR_CLIENT_CONNECTED: return this->Receive_CLIENT_CONNECTED(p); + case PACKET_COORDINATOR_GC_DIRECT_CONNECT: return this->Receive_GC_DIRECT_CONNECT(p); default: Debug(net, 0, "[tcp/coordinator] Received invalid packet type {}", type); @@ -82,3 +88,9 @@ bool NetworkCoordinatorSocketHandler::Receive_GC_REGISTER_ACK(Packet *p) { retur bool NetworkCoordinatorSocketHandler::Receive_SERVER_UPDATE(Packet *p) { return this->ReceiveInvalidPacket(PACKET_COORDINATOR_SERVER_UPDATE); } bool NetworkCoordinatorSocketHandler::Receive_CLIENT_LISTING(Packet *p) { return this->ReceiveInvalidPacket(PACKET_COORDINATOR_CLIENT_LISTING); } bool NetworkCoordinatorSocketHandler::Receive_GC_LISTING(Packet *p) { return this->ReceiveInvalidPacket(PACKET_COORDINATOR_GC_LISTING); } +bool NetworkCoordinatorSocketHandler::Receive_CLIENT_CONNECT(Packet *p) { return this->ReceiveInvalidPacket(PACKET_COORDINATOR_CLIENT_CONNECT); } +bool NetworkCoordinatorSocketHandler::Receive_GC_CONNECTING(Packet *p) { return this->ReceiveInvalidPacket(PACKET_COORDINATOR_GC_CONNECTING); } +bool NetworkCoordinatorSocketHandler::Receive_SERCLI_CONNECT_FAILED(Packet *p) { return this->ReceiveInvalidPacket(PACKET_COORDINATOR_SERCLI_CONNECT_FAILED); } +bool NetworkCoordinatorSocketHandler::Receive_GC_CONNECT_FAILED(Packet *p) { return this->ReceiveInvalidPacket(PACKET_COORDINATOR_GC_CONNECT_FAILED); } +bool NetworkCoordinatorSocketHandler::Receive_CLIENT_CONNECTED(Packet *p) { return this->ReceiveInvalidPacket(PACKET_COORDINATOR_CLIENT_CONNECTED); } +bool NetworkCoordinatorSocketHandler::Receive_GC_DIRECT_CONNECT(Packet *p) { return this->ReceiveInvalidPacket(PACKET_COORDINATOR_GC_DIRECT_CONNECT); } diff --git a/src/network/core/tcp_coordinator.h b/src/network/core/tcp_coordinator.h index e95916816f..2d793b1b68 100644 --- a/src/network/core/tcp_coordinator.h +++ b/src/network/core/tcp_coordinator.h @@ -23,15 +23,22 @@ * GC -> packets from Game Coordinator to either Client or Server. * SERVER -> packets from Server to Game Coordinator. * CLIENT -> packets from Client to Game Coordinator. + * SERCLI -> packets from either the Server or Client to Game Coordinator. **/ enum PacketCoordinatorType { - PACKET_COORDINATOR_GC_ERROR, ///< Game Coordinator indicates there was an error. - PACKET_COORDINATOR_SERVER_REGISTER, ///< Server registration. - PACKET_COORDINATOR_GC_REGISTER_ACK, ///< Game Coordinator accepts the registration. - PACKET_COORDINATOR_SERVER_UPDATE, ///< Server sends an set intervals an update of the server. - PACKET_COORDINATOR_CLIENT_LISTING, ///< Client is requesting a listing of all public servers. - PACKET_COORDINATOR_GC_LISTING, ///< Game Coordinator returns a listing of all public servers. - PACKET_COORDINATOR_END, ///< Must ALWAYS be on the end of this list!! (period). + PACKET_COORDINATOR_GC_ERROR, ///< Game Coordinator indicates there was an error. + PACKET_COORDINATOR_SERVER_REGISTER, ///< Server registration. + PACKET_COORDINATOR_GC_REGISTER_ACK, ///< Game Coordinator accepts the registration. + PACKET_COORDINATOR_SERVER_UPDATE, ///< Server sends an set intervals an update of the server. + PACKET_COORDINATOR_CLIENT_LISTING, ///< Client is requesting a listing of all public servers. + PACKET_COORDINATOR_GC_LISTING, ///< Game Coordinator returns a listing of all public servers. + PACKET_COORDINATOR_CLIENT_CONNECT, ///< Client wants to connect to a server based on an invite code. + PACKET_COORDINATOR_GC_CONNECTING, ///< Game Coordinator informs the client of the token assigned to the connection attempt. + PACKET_COORDINATOR_SERCLI_CONNECT_FAILED, ///< Client/server tells the Game Coordinator the current connection attempt failed. + PACKET_COORDINATOR_GC_CONNECT_FAILED, ///< Game Coordinator informs client/server it has given up on the connection attempt. + PACKET_COORDINATOR_CLIENT_CONNECTED, ///< Client informs the Game Coordinator the connection with the server is established. + PACKET_COORDINATOR_GC_DIRECT_CONNECT, ///< Game Coordinator tells client to directly connect to the hostname:port of the server. + PACKET_COORDINATOR_END, ///< Must ALWAYS be on the end of this list!! (period) }; /** @@ -49,6 +56,7 @@ enum ConnectionType { enum NetworkCoordinatorErrorType { NETWORK_COORDINATOR_ERROR_UNKNOWN, ///< There was an unknown error. NETWORK_COORDINATOR_ERROR_REGISTRATION_FAILED, ///< Your request for registration failed. + NETWORK_COORDINATOR_ERROR_INVALID_INVITE_CODE, ///< The invite code given is invalid. }; /** Base socket handler for all Game Coordinator TCP sockets. */ @@ -76,6 +84,8 @@ protected: * uint8 Game Coordinator protocol version. * uint8 Type of game (see ServerGameType). * uint16 Local port of the server. + * string Invite code the server wants to use (can be empty; coordinator will assign a new invite code). + * string Secret that belongs to the invite code (empty if invite code is empty). * * @param p The packet that was just received. * @return True upon success, otherwise false. @@ -85,6 +95,8 @@ protected: /** * Game Coordinator acknowledges the registration. * + * string Invite code that can be used to join this server. + * string Secret that belongs to the invite code (only needed if reusing the invite code on next SERVER_REGISTER). * uint8 Type of connection was detected (see ConnectionType). * * @param p The packet that was just received. @@ -130,6 +142,79 @@ protected: */ virtual bool Receive_GC_LISTING(Packet *p); + /** + * Client wants to connect to a Server. + * + * uint8 Game Coordinator protocol version. + * string Invite code of the Server to join. + * + * @param p The packet that was just received. + * @return True upon success, otherwise false. + */ + virtual bool Receive_CLIENT_CONNECT(Packet *p); + + /** + * Game Coordinator informs the Client under what token it will start the + * attempt to connect the Server and Client together. + * + * string Token to track the current connect request. + * string Invite code of the Server to join. + * + * @param p The packet that was just received. + * @return True upon success, otherwise false. + */ + virtual bool Receive_GC_CONNECTING(Packet *p); + + /** + * Client or Server failed to connect to the remote side. + * + * uint8 Game Coordinator protocol version. + * string Token to track the current connect request. + * uint8 Tracking number to track current connect request. + * + * @param p The packet that was just received. + * @return True upon success, otherwise false. + */ + virtual bool Receive_SERCLI_CONNECT_FAILED(Packet *p); + + /** + * Game Coordinator informs the Client that it failed to find a way to + * connect the Client to the Server. Any open connections for this token + * should be closed now. + * + * string Token to track the current connect request. + * + * @param p The packet that was just received. + * @return True upon success, otherwise false. + */ + virtual bool Receive_GC_CONNECT_FAILED(Packet *p); + + /** + * Client informs the Game Coordinator the connection with the Server is + * established. The Client will disconnect from the Game Coordinator next. + * + * uint8 Game Coordinator protocol version. + * string Token to track the current connect request. + * + * @param p The packet that was just received. + * @return True upon success, otherwise false. + */ + virtual bool Receive_CLIENT_CONNECTED(Packet *p); + + /** + * Game Coordinator requests that the Client makes a direct connection to + * the indicated peer, which is a Server. + * + * string Token to track the current connect request. + * uint8 Tracking number to track current connect request. + * string Hostname of the peer. + * uint16 Port of the peer. + * + * @param p The packet that was just received. + * @return True upon success, otherwise false. + */ + virtual bool Receive_GC_DIRECT_CONNECT(Packet *p); + bool HandlePacket(Packet *p); public: /** diff --git a/src/network/network.cpp b/src/network/network.cpp index 3a33e50962..f8138bbbc1 100644 --- a/src/network/network.cpp +++ b/src/network/network.cpp @@ -589,9 +589,13 @@ void NetworkClose(bool close_admins) ServerNetworkAdminSocketHandler::CloseListeners(); _network_coordinator_client.CloseConnection(); - } else if (MyClient::my_client != nullptr) { - MyClient::SendQuit(); - MyClient::my_client->CloseConnection(NETWORK_RECV_STATUS_CLIENT_QUIT); + } else { + if (MyClient::my_client != nullptr) { + MyClient::SendQuit(); + MyClient::my_client->CloseConnection(NETWORK_RECV_STATUS_CLIENT_QUIT); + } + + _network_coordinator_client.CloseAllTokens(); } TCPConnecter::KillAll(); diff --git a/src/network/network_coordinator.cpp b/src/network/network_coordinator.cpp index b3d3049524..8bd81b6f62 100644 --- a/src/network/network_coordinator.cpp +++ b/src/network/network_coordinator.cpp @@ -1,4 +1,3 @@ - /* * This file is part of OpenTTD. * OpenTTD 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, version 2. @@ -27,13 +26,41 @@ static const auto NETWORK_COORDINATOR_DELAY_BETWEEN_UPDATES = std::chrono::seconds(30); ///< How many time between updates the server sends to the Game Coordinator. ClientNetworkCoordinatorSocketHandler _network_coordinator_client; ///< The connection to the Game Coordinator. ConnectionType _network_server_connection_type = CONNECTION_TYPE_UNKNOWN; ///< What type of connection the Game Coordinator detected we are on. +std::string _network_server_invite_code = ""; ///< Our invite code as indicated by the Game Coordinator. + +/** Connect to a game server by IP:port. */ +class NetworkDirectConnecter : public TCPConnecter { +private: + std::string token; ///< Token of this connection. + uint8 tracking_number; ///< Tracking number of this connection. + +public: + /** + * Try to establish a direct (hostname:port based) connection. + * @param hostname The hostname of the server. + * @param port The port of the server. + * @param token The token as given by the Game Coordinator to track this connection attempt. + * @param tracking_number The tracking number as given by the Game Coordinator to track this connection attempt. + */ + NetworkDirectConnecter(const std::string &hostname, uint16 port, const std::string &token, uint8 tracking_number) : TCPConnecter(hostname, port), token(token), tracking_number(tracking_number) {} + + void OnFailure() override + { + _network_coordinator_client.ConnectFailure(this->token, this->tracking_number); + } + + void OnConnect(SOCKET s) override + { + _network_coordinator_client.ConnectSuccess(this->token, s); + } +}; /** Connect to the Game Coordinator server. */ class NetworkCoordinatorConnecter : TCPConnecter { public: /** * Initiate the connecting. - * @param address The address of the Game Coordinator server. + * @param connection_string The address of the Game Coordinator server. */ NetworkCoordinatorConnecter(const std::string &connection_string) : TCPConnecter(connection_string, NETWORK_COORDINATOR_SERVER_PORT) {} @@ -73,6 +100,20 @@ bool ClientNetworkCoordinatorSocketHandler::Receive_GC_ERROR(Packet *p) this->CloseConnection(); return false; + case NETWORK_COORDINATOR_ERROR_INVALID_INVITE_CODE: { + /* Find the connecter based on the invite code. */ + auto connecter_it = this->connecter_pre.find(detail); + if (connecter_it == this->connecter_pre.end()) return true; + this->connecter_pre.erase(connecter_it); + + /* Mark the server as offline. */ + NetworkGameList *item = NetworkGameListAddItem(detail); + item->online = false; + + UpdateNetworkGameWindow(); + return true; + } + default: Debug(net, 0, "Invalid error type {} received from Game Coordinator", error); this->CloseConnection(); @@ -85,12 +126,21 @@ bool ClientNetworkCoordinatorSocketHandler::Receive_GC_REGISTER_ACK(Packet *p) /* Schedule sending an update. */ this->next_update = std::chrono::steady_clock::now(); + _settings_client.network.server_invite_code = p->Recv_string(NETWORK_INVITE_CODE_LENGTH); + _settings_client.network.server_invite_code_secret = p->Recv_string(NETWORK_INVITE_CODE_SECRET_LENGTH); _network_server_connection_type = (ConnectionType)p->Recv_uint8(); if (_network_server_connection_type == CONNECTION_TYPE_ISOLATED) { ShowErrorMessage(STR_NETWORK_ERROR_COORDINATOR_ISOLATED, STR_NETWORK_ERROR_COORDINATOR_ISOLATED_DETAIL, WL_ERROR); } + /* Users can change the invite code in the settings, but this has no effect + * on the invite code as assigned by the server. So + * _network_server_invite_code contains the current invite code, + * and _settings_client.network.server_invite_code contains the one we will + * attempt to re-use when registering again. */ + _network_server_invite_code = _settings_client.network.server_invite_code; + SetWindowDirty(WC_CLIENT_LIST, 0); if (_network_dedicated) { @@ -107,7 +157,10 @@ bool ClientNetworkCoordinatorSocketHandler::Receive_GC_REGISTER_ACK(Packet *p) Debug(net, 3, "Your server is now registered with the Game Coordinator:"); Debug(net, 3, " Game type: Public"); Debug(net, 3, " Connection type: {}", connection_type); + Debug(net, 3, " Invite code: {}", _network_server_invite_code); Debug(net, 3, "----------------------------------------"); + } else { + Debug(net, 3, "Game Coordinator registered our server with invite code '{}'", _network_server_invite_code); } return true; @@ -130,7 +183,7 @@ bool ClientNetworkCoordinatorSocketHandler::Receive_GC_LISTING(Packet *p) NetworkGameInfo ngi = {}; DeserializeNetworkGameInfo(p, &ngi); - /* Now we know the join-key, we can add it to our list. */ + /* Now we know the connection string, we can add it to our list. */ NetworkGameList *item = NetworkGameListAddItem(connection_string); /* Clear any existing GRFConfig chain. */ @@ -149,6 +202,58 @@ bool ClientNetworkCoordinatorSocketHandler::Receive_GC_LISTING(Packet *p) return true; } +bool ClientNetworkCoordinatorSocketHandler::Receive_GC_CONNECTING(Packet *p) +{ + std::string token = p->Recv_string(NETWORK_TOKEN_LENGTH); + std::string invite_code = p->Recv_string(NETWORK_INVITE_CODE_LENGTH); + + /* Find the connecter based on the invite code. */ + auto connecter_it = this->connecter_pre.find(invite_code); + if (connecter_it == this->connecter_pre.end()) { + this->CloseConnection(); + return false; + } + + /* Now store it based on the token. */ + this->connecter[token] = connecter_it->second; + this->connecter_pre.erase(connecter_it); + + return true; +} + +bool ClientNetworkCoordinatorSocketHandler::Receive_GC_CONNECT_FAILED(Packet *p) +{ + std::string token = p->Recv_string(NETWORK_TOKEN_LENGTH); + + auto connecter_it = this->connecter.find(token); + if (connecter_it != this->connecter.end()) { + connecter_it->second->SetFailure(); + this->connecter.erase(connecter_it); + } + + /* Close all remaining connections. */ + this->CloseToken(token); + + return true; +} + +bool ClientNetworkCoordinatorSocketHandler::Receive_GC_DIRECT_CONNECT(Packet *p) +{ + std::string token = p->Recv_string(NETWORK_TOKEN_LENGTH); + uint8 tracking_number = p->Recv_uint8(); + std::string hostname = p->Recv_string(NETWORK_HOSTNAME_LENGTH); + uint16 port = p->Recv_uint16(); + + /* Ensure all other pending connection attempts are killed. */ + if (this->game_connecter != nullptr) { + this->game_connecter->Kill(); + this->game_connecter = nullptr; + } + + this->game_connecter = new NetworkDirectConnecter(hostname, port, token, tracking_number); + return true; +} + void ClientNetworkCoordinatorSocketHandler::Connect() { /* We are either already connected or are trying to connect. */ @@ -172,13 +277,15 @@ NetworkRecvStatus ClientNetworkCoordinatorSocketHandler::CloseConnection(bool er _network_server_connection_type = CONNECTION_TYPE_UNKNOWN; this->next_update = {}; + this->CloseAllTokens(); + SetWindowDirty(WC_CLIENT_LIST, 0); return NETWORK_RECV_STATUS_OKAY; } /** - * Register our server to receive our join-key. + * Register our server to receive our invite code. */ void ClientNetworkCoordinatorSocketHandler::Register() { @@ -193,6 +300,13 @@ void ClientNetworkCoordinatorSocketHandler::Register() p->Send_uint8(NETWORK_COORDINATOR_VERSION); p->Send_uint8(SERVER_GAME_TYPE_PUBLIC); p->Send_uint16(_settings_client.network.server_port); + if (_settings_client.network.server_invite_code.empty() || _settings_client.network.server_invite_code_secret.empty()) { + p->Send_string(""); + p->Send_string(""); + } else { + p->Send_string(_settings_client.network.server_invite_code); + p->Send_string(_settings_client.network.server_invite_code_secret); + } this->SendPacket(p); } @@ -229,6 +343,123 @@ void ClientNetworkCoordinatorSocketHandler::GetListing() this->SendPacket(p); } +/** + * Join a server based on an invite code. + * @param invite_code The invite code of the server to connect to. + * @param connecter The connecter of the request. + */ +void ClientNetworkCoordinatorSocketHandler::ConnectToServer(const std::string &invite_code, TCPServerConnecter *connecter) +{ + assert(StrStartsWith(invite_code, "+")); + + if (this->connecter_pre.find(invite_code) != this->connecter_pre.end()) { + /* If someone is hammering the refresh key, one can sent out two + * requests for the same invite code. There isn't really a great way + * of handling this, so just ignore this request. */ + connecter->SetFailure(); + return; + } + + /* Initially we store based on invite code; on first reply we know the + * token, and will start using that key instead. */ + this->connecter_pre[invite_code] = connecter; + + this->Connect(); + + Packet *p = new Packet(PACKET_COORDINATOR_CLIENT_CONNECT); + p->Send_uint8(NETWORK_COORDINATOR_VERSION); + p->Send_string(invite_code); + + this->SendPacket(p); +} + +/** + * Callback from a Connecter to let the Game Coordinator know the connection failed. + * @param token Token of the connecter that failed. + * @param tracking_number Tracking number of the connecter that failed. + */ +void ClientNetworkCoordinatorSocketHandler::ConnectFailure(const std::string &token, uint8 tracking_number) +{ + /* Connecter will destroy itself. */ + this->game_connecter = nullptr; + + Packet *p = new Packet(PACKET_COORDINATOR_SERCLI_CONNECT_FAILED); + p->Send_uint8(NETWORK_COORDINATOR_VERSION); + p->Send_string(token); + p->Send_uint8(tracking_number); + + this->SendPacket(p); + + auto connecter_it = this->connecter.find(token); + assert(connecter_it != this->connecter.end()); + + connecter_it->second->SetFailure(); + this->connecter.erase(connecter_it); +} + +/** + * Callback from a Connecter to let the Game Coordinator know the connection + * to the game server is established. + * @param token Token of the connecter that succeeded. + * @param sock The socket that the connecter can now use. + */ +void ClientNetworkCoordinatorSocketHandler::ConnectSuccess(const std::string &token, SOCKET sock) +{ + /* Connecter will destroy itself. */ + this->game_connecter = nullptr; + + assert(!_network_server); + + Packet *p = new Packet(PACKET_COORDINATOR_CLIENT_CONNECTED); + p->Send_uint8(NETWORK_COORDINATOR_VERSION); + p->Send_string(token); + this->SendPacket(p); + + auto connecter_it = this->connecter.find(token); + assert(connecter_it != this->connecter.end()); + + connecter_it->second->SetConnected(sock); + this->connecter.erase(connecter_it); + + /* Close all remaining connections. */ + this->CloseToken(token); +} + +/** + * Close everything related to this connection token. + * @param token The connection token to close. + */ +void ClientNetworkCoordinatorSocketHandler::CloseToken(const std::string &token) +{ + /* Ensure all other pending connection attempts are also killed. */ + if (this->game_connecter != nullptr) { + this->game_connecter->Kill(); + this->game_connecter = nullptr; + } +} + +/** + * Close all pending connection tokens. + */ +void ClientNetworkCoordinatorSocketHandler::CloseAllTokens() +{ + /* Ensure all other pending connection attempts are also killed. */ + if (this->game_connecter != nullptr) { + this->game_connecter->Kill(); + this->game_connecter = nullptr; + } + + /* Mark any pending connecters as failed. */ + for (auto &[token, it] : this->connecter) { + it->SetFailure(); + } + for (auto &[invite_code, it] : this->connecter_pre) { + it->SetFailure(); + } + this->connecter.clear(); + this->connecter_pre.clear(); +} + /** * Check whether we received/can send some data from/to the Game Coordinator server and * when that's the case handle it appropriately. diff --git a/src/network/network_coordinator.h b/src/network/network_coordinator.h index 7399ce1fc1..f846958bf8 100644 --- a/src/network/network_coordinator.h +++ b/src/network/network_coordinator.h @@ -1,4 +1,3 @@ - /* * This file is part of OpenTTD. * OpenTTD 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, version 2. @@ -12,6 +11,7 @@ #define NETWORK_COORDINATOR_H #include "core/tcp_coordinator.h" +#include /** * Game Coordinator communication. @@ -25,17 +25,32 @@ * For clients (listing): * - Client sends CLIENT_LISTING. * - Game Coordinator returns the full list of public servers via GC_LISTING (multiple packets). + * + * For clients (connecting): + * - Client sends CLIENT_CONNECT. + * - Game Coordinator checks what type of connections the servers supports: + * 1) Direct connect? + * - Send the client a GC_CONNECT with the peer address. + * - a) Client connects, client sends CLIENT_CONNECTED to Game Coordinator. + * - b) Client connect fails, client sends CLIENT_CONNECT_FAILED to Game Coordinator. + * - If all fails, Game Coordinator sends GC_CONNECT_FAILED to indicate no connection is possible. */ /** Class for handling the client side of the Game Coordinator connection. */ class ClientNetworkCoordinatorSocketHandler : public NetworkCoordinatorSocketHandler { private: std::chrono::steady_clock::time_point next_update; ///< When to send the next update (if server and public). + std::map connecter; ///< Based on tokens, the current connecters that are pending. + std::map connecter_pre; ///< Based on invite codes, the current connecters that are pending. + TCPConnecter *game_connecter = nullptr; ///< Pending connecter to the game server. protected: bool Receive_GC_ERROR(Packet *p) override; bool Receive_GC_REGISTER_ACK(Packet *p) override; bool Receive_GC_LISTING(Packet *p) override; + bool Receive_GC_CONNECTING(Packet *p) override; + bool Receive_GC_CONNECT_FAILED(Packet *p) override; + bool Receive_GC_DIRECT_CONNECT(Packet *p) override; public: /** The idle timeout; when to close the connection because it's idle. */ @@ -49,11 +64,18 @@ public: NetworkRecvStatus CloseConnection(bool error = true) override; void SendReceive(); + void ConnectFailure(const std::string &token, uint8 tracking_number); + void ConnectSuccess(const std::string &token, SOCKET sock); + void Connect(); + void CloseToken(const std::string &token); + void CloseAllTokens(); void Register(); void SendServerUpdate(); void GetListing(); + + void ConnectToServer(const std::string &invite_code, TCPServerConnecter *connecter); }; extern ClientNetworkCoordinatorSocketHandler _network_coordinator_client; diff --git a/src/network/network_gui.cpp b/src/network/network_gui.cpp index 24884ed165..a0d2d47c84 100644 --- a/src/network/network_gui.cpp +++ b/src/network/network_gui.cpp @@ -751,7 +751,7 @@ public: SetDParamStr(0, _settings_client.network.connect_to_ip); ShowQueryString( STR_JUST_RAW_STRING, - STR_NETWORK_SERVER_LIST_ENTER_IP, + STR_NETWORK_SERVER_LIST_ENTER_SERVER_ADDRESS, NETWORK_HOSTNAME_PORT_LENGTH, // maximum number of characters including '\0' this, CS_ALPHANUMERAL, QSF_ACCEPT_UNCHANGED); break; @@ -1632,6 +1632,11 @@ static const NWidgetPart _nested_client_list_widgets[] = { NWidget(NWID_SPACER), SetMinimalSize(10, 0), SetFill(1, 0), SetResize(1, 0), NWidget(WWT_DROPDOWN, COLOUR_GREY, WID_CL_SERVER_VISIBILITY), SetDataTip(STR_BLACK_STRING, STR_NETWORK_CLIENT_LIST_SERVER_VISIBILITY_TOOLTIP), EndContainer(), + NWidget(NWID_HORIZONTAL), SetPIP(0, 3, 0), + NWidget(WWT_TEXT, COLOUR_GREY), SetMinimalTextLines(1, 0), SetDataTip(STR_NETWORK_CLIENT_LIST_SERVER_INVITE_CODE, STR_NULL), + NWidget(NWID_SPACER), SetMinimalSize(10, 0), + NWidget(WWT_TEXT, COLOUR_GREY, WID_CL_SERVER_INVITE_CODE), SetFill(1, 0), SetMinimalTextLines(1, 0), SetResize(1, 0), SetDataTip(STR_BLACK_RAW_STRING, STR_NETWORK_CLIENT_LIST_SERVER_INVITE_CODE_TOOLTIP), SetAlignment(SA_VERT_CENTER | SA_RIGHT), + EndContainer(), NWidget(NWID_HORIZONTAL), SetPIP(0, 3, 0), NWidget(WWT_TEXT, COLOUR_GREY), SetMinimalTextLines(1, 0), SetDataTip(STR_NETWORK_CLIENT_LIST_SERVER_CONNECTION_TYPE, STR_NULL), NWidget(NWID_SPACER), SetMinimalSize(10, 0), @@ -2071,6 +2076,12 @@ public: SetDParam(0, _server_visibility_dropdown[_settings_client.network.server_advertise]); break; + case WID_CL_SERVER_INVITE_CODE: { + static std::string empty = {}; + SetDParamStr(0, _network_server_connection_type == CONNECTION_TYPE_UNKNOWN ? empty : _network_server_invite_code); + break; + } + case WID_CL_SERVER_CONNECTION_TYPE: SetDParam(0, STR_NETWORK_CLIENT_LIST_SERVER_CONNECTION_TYPE_UNKNOWN + _network_server_connection_type); break; diff --git a/src/network/network_internal.h b/src/network/network_internal.h index 95226286c5..2c6639961d 100644 --- a/src/network/network_internal.h +++ b/src/network/network_internal.h @@ -84,6 +84,7 @@ extern uint8 _network_join_waiting; extern uint32 _network_join_bytes; extern uint32 _network_join_bytes_total; extern ConnectionType _network_server_connection_type; +extern std::string _network_server_invite_code; extern uint8 _network_reconnect; diff --git a/src/settings_type.h b/src/settings_type.h index 3377bdede4..79e462c3e2 100644 --- a/src/settings_type.h +++ b/src/settings_type.h @@ -266,6 +266,8 @@ struct NetworkSettings { uint16 server_port; ///< port the server listens on uint16 server_admin_port; ///< port the server listens on for the admin network bool server_admin_chat; ///< allow private chat for the server to be distributed to the admin network + std::string server_invite_code; ///< Invite code to use when registering as server. + std::string server_invite_code_secret; ///< Secret to proof we got this invite code from the Game Coordinator. std::string server_name; ///< name of the server std::string server_password; ///< password for joining this server std::string rcon_password; ///< password for rconsole (server side) diff --git a/src/table/settings/network_secrets_settings.ini b/src/table/settings/network_secrets_settings.ini index fced9240e0..4613636a86 100644 --- a/src/table/settings/network_secrets_settings.ini +++ b/src/table/settings/network_secrets_settings.ini @@ -74,3 +74,17 @@ type = SLE_STR length = NETWORK_SERVER_ID_LENGTH flags = SF_NOT_IN_SAVE | SF_NO_NETWORK_SYNC | SF_NETWORK_ONLY def = nullptr + +[SDTC_SSTR] +var = network.server_invite_code +type = SLE_STR +length = NETWORK_INVITE_CODE_LENGTH +flags = SF_NOT_IN_SAVE | SF_NO_NETWORK_SYNC | SF_NETWORK_ONLY +def = nullptr + +[SDTC_SSTR] +var = network.server_invite_code_secret +type = SLE_STR +length = NETWORK_INVITE_CODE_SECRET_LENGTH +flags = SF_NOT_IN_SAVE | SF_NO_NETWORK_SYNC | SF_NETWORK_ONLY +def = nullptr diff --git a/src/widgets/network_widget.h b/src/widgets/network_widget.h index a665afdb74..c8ec22e861 100644 --- a/src/widgets/network_widget.h +++ b/src/widgets/network_widget.h @@ -101,6 +101,7 @@ enum ClientListWidgets { WID_CL_SERVER_NAME, ///< Server name. WID_CL_SERVER_NAME_EDIT, ///< Edit button for server name. WID_CL_SERVER_VISIBILITY, ///< Server visibility. + WID_CL_SERVER_INVITE_CODE, ///< Invite code for this server. WID_CL_SERVER_CONNECTION_TYPE, ///< The type of connection the Game Coordinator detected for this server. WID_CL_CLIENT_NAME, ///< Client name. WID_CL_CLIENT_NAME_EDIT, ///< Edit button for client name.