diff --git a/data/language/english_uk.txt b/data/language/english_uk.txt index 59af7466cd..1032ba2d42 100644 --- a/data/language/english_uk.txt +++ b/data/language/english_uk.txt @@ -3899,11 +3899,23 @@ STR_5557 :Stay connected after desynchronisation (Multiplayer) STR_5558 :A restart is required for this setting to take effect STR_5559 :10 min. inspections STR_5560 :{SMALLFONT}{BLACK}Sets the inspection time to 'Every 10 minutes' on all rides +<<<<<<< HEAD STR_5561 :Failed to load language file STR_5562 :WARNING! STR_5563 :This feature is currently unstable, take extra caution. STR_5564 :Insert Corrupt Element STR_5565 :{SMALLFONT}{BLACK}Inserts a corrupt map element at top of tile. This will hide any element above the corrupt element. +STR_5566 :Password: +STR_5567 :Advertise +STR_5568 :Password Required +STR_5569 :This server requires a password +STR_5570 :Fetch Servers +STR_5571 :Join Game +STR_5572 :Add To Favorites +STR_5573 :Remove From Favorites +STR_5574 :Server Name: +STR_5575 :Max Players: +STR_5576 :Port: ##################### # Rides/attractions # diff --git a/projects/openrct2.vcxproj b/projects/openrct2.vcxproj index 05dae3caf6..74c3166567 100644 --- a/projects/openrct2.vcxproj +++ b/projects/openrct2.vcxproj @@ -103,6 +103,7 @@ + diff --git a/projects/openrct2.vcxproj.filters b/projects/openrct2.vcxproj.filters index c08b462c63..734c2e6363 100644 --- a/projects/openrct2.vcxproj.filters +++ b/projects/openrct2.vcxproj.filters @@ -546,6 +546,9 @@ Source\Platform + + Source\Windows + diff --git a/src/config.c b/src/config.c index a2d5d74d1a..40e83036fa 100644 --- a/src/config.c +++ b/src/config.c @@ -157,7 +157,7 @@ config_enum_definition _dateFormatEnum[] = { config_property_definition _generalDefinitions[] = { { offsetof(general_configuration, always_show_gridlines), "always_show_gridlines", CONFIG_VALUE_TYPE_BOOLEAN, false, NULL }, - { offsetof(general_configuration, autosave_frequency), "autosave", CONFIG_VALUE_TYPE_UINT8, AUTOSAVE_EVERY_5MINUTES, NULL }, + { offsetof(general_configuration, autosave_frequency), "autosave", CONFIG_VALUE_TYPE_UINT8, AUTOSAVE_EVERY_5MINUTES, NULL }, { offsetof(general_configuration, confirmation_prompt), "confirmation_prompt", CONFIG_VALUE_TYPE_UINT8, 0, NULL }, { offsetof(general_configuration, construction_marker_colour), "construction_marker_colour", CONFIG_VALUE_TYPE_UINT8, false, NULL }, { offsetof(general_configuration, currency_format), "currency_format", CONFIG_VALUE_TYPE_UINT8, CURRENCY_POUNDS, _currencyEnum }, @@ -239,7 +239,12 @@ config_property_definition _twitchDefinitions[] = { config_property_definition _networkDefinitions[] = { { offsetof(network_configuration, player_name), "player_name", CONFIG_VALUE_TYPE_STRING, {.value_string = "Player" }, NULL }, { offsetof(network_configuration, default_port), "default_port", CONFIG_VALUE_TYPE_UINT32, NETWORK_DEFAULT_PORT, NULL }, - { offsetof(network_configuration, stay_connected), "stay_connected", CONFIG_VALUE_TYPE_BOOLEAN, false, NULL }, + { offsetof(network_configuration, stay_connected), "stay_connected", CONFIG_VALUE_TYPE_BOOLEAN, true, NULL }, + { offsetof(network_configuration, advertise), "advertise", CONFIG_VALUE_TYPE_BOOLEAN, false, NULL }, + { offsetof(network_configuration, maxplayers), "maxplayers", CONFIG_VALUE_TYPE_UINT8, 16, NULL }, + { offsetof(network_configuration, server_name), "server_name", CONFIG_VALUE_TYPE_STRING, {.value_string = "Server" }, NULL }, + { offsetof(network_configuration, server_description), "server_description", CONFIG_VALUE_TYPE_STRING, {.value_string = NULL }, NULL }, + { offsetof(network_configuration, master_server_url), "master_server_url", CONFIG_VALUE_TYPE_STRING, {.value_string = NULL }, NULL } }; config_section_definition _sectionDefinitions[] = { diff --git a/src/config.h b/src/config.h index b41965c4d7..fc070133f4 100644 --- a/src/config.h +++ b/src/config.h @@ -214,6 +214,11 @@ typedef struct { utf8string player_name; uint32 default_port; uint8 stay_connected; + uint8 advertise; + uint8 maxplayers; + utf8string server_name; + utf8string server_description; + utf8string master_server_url; } network_configuration; typedef struct theme_window { diff --git a/src/game.c b/src/game.c index c15739fe7e..bfeb6a639c 100644 --- a/src/game.c +++ b/src/game.c @@ -261,7 +261,7 @@ void game_update() numUpdates = clamp(1, numUpdates, 4); } - if (network_get_mode() == NETWORK_MODE_CLIENT && network_get_status() == NETWORK_STATUS_CONNECTED) { + if (network_get_mode() == NETWORK_MODE_CLIENT && network_get_status() == NETWORK_STATUS_CONNECTED && network_get_authstatus() == NETWORK_AUTH_OK) { if (network_get_server_tick() - RCT2_GLOBAL(RCT2_ADDRESS_CURRENT_TICKS, uint32) >= 10) { // make sure client doesn't fall behind the server too much numUpdates += 10; @@ -340,7 +340,7 @@ void game_logic_update() gInUpdateCode = true; /////////////////////////// network_update(); - if (network_get_mode() == NETWORK_MODE_CLIENT && network_get_status() == NETWORK_STATUS_CONNECTED) { + if (network_get_mode() == NETWORK_MODE_CLIENT && network_get_status() == NETWORK_STATUS_CONNECTED && network_get_authstatus() == NETWORK_AUTH_OK) { if (RCT2_GLOBAL(RCT2_ADDRESS_CURRENT_TICKS, uint32) >= network_get_server_tick()) { // dont run past the server return; diff --git a/src/interface/chat.c b/src/interface/chat.c index ec783c1b4d..eb656b22eb 100644 --- a/src/interface/chat.c +++ b/src/interface/chat.c @@ -60,7 +60,8 @@ void chat_update() void chat_draw() { - if (network_get_mode() == NETWORK_MODE_NONE) { + if (network_get_mode() == NETWORK_MODE_NONE || network_get_status() != NETWORK_STATUS_CONNECTED || network_get_authstatus() != NETWORK_AUTH_OK) { + gChatOpen = false; return; } rct_drawpixelinfo *dpi = (rct_drawpixelinfo*)RCT2_ADDRESS_SCREEN_DPI; @@ -102,7 +103,7 @@ void chat_history_add(const char *src) { int index = _chatHistoryIndex % CHAT_HISTORY_SIZE; memset(_chatHistory[index], 0, CHAT_INPUT_SIZE); - memcpy(_chatHistory[index], src, min(strlen(src), CHAT_INPUT_SIZE)); + memcpy(_chatHistory[index], src, min(strlen(src), CHAT_INPUT_SIZE - 1)); _chatHistoryTime[index] = SDL_GetTicks(); _chatHistoryIndex++; Mixer_Play_Effect(SOUND_NEWS_ITEM, 0, SDL_MIX_MAXVOLUME, 0, 1.5f, true); diff --git a/src/interface/window.h b/src/interface/window.h index e37b9244c6..429a6ce07f 100644 --- a/src/interface/window.h +++ b/src/interface/window.h @@ -450,6 +450,7 @@ enum { WC_PLAYER_LIST = 125, WC_NETWORK_STATUS = 126, WC_SERVER_LIST = 127, + WC_SERVER_START = 128, // Only used for colour schemes WC_STAFF = 220, @@ -628,7 +629,9 @@ void window_cheats_open(); void window_player_list_open(); void window_network_status_open(const char* text); void window_network_status_close(); +void window_network_status_open_password(); void window_server_list_open(); +void window_server_start_open(); void window_research_open(); void window_research_development_page_paint(rct_window *w, rct_drawpixelinfo *dpi, int baseWidgetIndex); diff --git a/src/localisation/string_ids.h b/src/localisation/string_ids.h index 42619e6b09..c6192a913a 100644 --- a/src/localisation/string_ids.h +++ b/src/localisation/string_ids.h @@ -2154,6 +2154,18 @@ enum { STR_INSERT_CORRUPT = 5564, STR_INSERT_CORRUPT_TIP = 5565, + STR_PASSWORD = 5566, + STR_ADVERTISE = 5567, + STR_PASSWORD_REQUIRED = 5568, + STR_PASSWORD_REQUIRED_DESC = 5569, + STR_FETCH_SERVERS = 5570, + STR_JOIN_GAME = 5571, + STR_ADD_TO_FAVORITES = 5572, + STR_REMOVE_FROM_FAVORITES = 5573, + STR_SERVER_NAME = 5574, + STR_MAX_PLAYERS = 5575, + STR_PORT = 5576, + // Have to include resource strings (from scenarios and objects) for the time being now that language is partially working STR_COUNT = 32768 }; diff --git a/src/network/http.cpp b/src/network/http.cpp index 3711694acb..ba6557578c 100644 --- a/src/network/http.cpp +++ b/src/network/http.cpp @@ -69,6 +69,10 @@ http_json_response *http_request_json(const char *url) writeBuffer.length = 0; writeBuffer.capacity = 0; + curl_slist *headers = NULL; + headers = curl_slist_append(headers, "Accept: application/json"); + + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, true); curl_easy_setopt(curl, CURLOPT_CAINFO, "curl-ca-bundle.crt"); diff --git a/src/network/network.cpp b/src/network/network.cpp index faf75bc010..eef553901a 100644 --- a/src/network/network.cpp +++ b/src/network/network.cpp @@ -41,6 +41,7 @@ extern "C" { #include "../interface/window.h" #include "../localisation/date.h" #include "../localisation/localisation.h" +#include "../network/http.h" #include "../scenario.h" #include "../windows/error.h" #include "../util/util.h" @@ -67,7 +68,9 @@ enum { NETWORK_COMMAND_PING, NETWORK_COMMAND_PINGLIST, NETWORK_COMMAND_SETDISCONNECTMSG, - NETWORK_COMMAND_MAX + NETWORK_COMMAND_GAMEINFO, + NETWORK_COMMAND_MAX, + NETWORK_COMMAND_INVALID = -1 }; const char *NetworkCommandNames[] = { @@ -81,6 +84,22 @@ const char *NetworkCommandNames[] = { "NETWORK_COMMAND_PINGLIST", }; +enum { + ADVERTISE_STATUS_DISABLED, + ADVERTISE_STATUS_UNREGISTERED, + ADVERTISE_STATUS_REGISTERED +}; + +enum { + MASTER_SERVER_STATUS_OK = 200, + MASTER_SERVER_STATUS_INVALID_TOKEN = 401, + MASTER_SERVER_STATUS_SERVER_NOT_FOUND = 404, + MASTER_SERVER_STATUS_INTERNAL_ERROR = 500 +}; + +constexpr int MASTER_SERVER_REGISTER_TIME = 120 * 1000; // 2 minutes +constexpr int MASTER_SERVER_HEARTBEAT_TIME = 60 * 1000; // 1 minute + NetworkPacket::NetworkPacket() { transferred = 0; @@ -104,6 +123,15 @@ uint8* NetworkPacket::GetData() return &(*data)[0]; } +uint32 NetworkPacket::GetCommand() +{ + if (data->size() >= sizeof(uint32)) { + return ByteSwapBE(*(uint32*)(&(*data)[0])); + } else { + return NETWORK_COMMAND_INVALID; + } +} + void NetworkPacket::Write(uint8* bytes, unsigned int size) { data->insert(data->end(), bytes, bytes + size); @@ -147,6 +175,18 @@ void NetworkPacket::Clear() data->clear(); } +bool NetworkPacket::CommandRequiresAuth() +{ + switch (GetCommand()) { + case NETWORK_COMMAND_PING: + case NETWORK_COMMAND_AUTH: + case NETWORK_COMMAND_GAMEINFO: + return false; + default: + return true; + } +} + NetworkPlayer::NetworkPlayer(const char* name) { safe_strncpy((char*)NetworkPlayer::name, name, sizeof(NetworkPlayer::name)); @@ -232,11 +272,15 @@ bool NetworkConnection::SendPacket(NetworkPacket& packet) return false; } -void NetworkConnection::QueuePacket(std::unique_ptr packet) +void NetworkConnection::QueuePacket(std::unique_ptr packet, bool front) { - if (authstatus == NETWORK_AUTH_OK || authstatus == NETWORK_AUTH_REQUESTED) { + if (authstatus == NETWORK_AUTH_OK || !packet->CommandRequiresAuth()) { packet->size = (uint16)packet->data->size(); - outboundpackets.push_back(std::move(packet)); + if (front) { + outboundpackets.push_front(std::move(packet)); + } else { + outboundpackets.push_back(std::move(packet)); + } } } @@ -356,7 +400,7 @@ Network::Network() status = NETWORK_STATUS_NONE; last_tick_sent_time = 0; last_ping_sent_time = 0; - strcpy(password, ""); + last_advertise_time = 0; client_command_handlers.resize(NETWORK_COMMAND_MAX, 0); client_command_handlers[NETWORK_COMMAND_AUTH] = &Network::Client_Handle_AUTH; client_command_handlers[NETWORK_COMMAND_MAP] = &Network::Client_Handle_MAP; @@ -372,6 +416,7 @@ Network::Network() server_command_handlers[NETWORK_COMMAND_CHAT] = &Network::Server_Handle_CHAT; server_command_handlers[NETWORK_COMMAND_GAMECMD] = &Network::Server_Handle_GAMECMD; server_command_handlers[NETWORK_COMMAND_PING] = &Network::Server_Handle_PING; + server_command_handlers[NETWORK_COMMAND_GAMEINFO] = &Network::Server_Handle_GAMEINFO; } Network::~Network() @@ -425,6 +470,10 @@ void Network::Close() bool Network::BeginClient(const char* host, unsigned short port) { + if (GetMode() != NETWORK_MODE_NONE) { + return false; + } + Close(); if (!Init()) return false; @@ -486,6 +535,19 @@ bool Network::BeginServer(unsigned short port, const char* address) printf("Ready for clients...\n"); mode = NETWORK_MODE_SERVER; + status = NETWORK_STATUS_CONNECTED; + listening_port = port; + advertise_status = ADVERTISE_STATUS_DISABLED; + last_advertise_time = 0; + last_heartbeat_time = 0; + advertise_token = ""; + advertise_key = GenerateAdvertiseKey(); + +#ifndef DISABLE_HTTP + if (gConfigNetwork.advertise) { + advertise_status = ADVERTISE_STATUS_UNREGISTERED; + } +#endif return true; } @@ -544,14 +606,26 @@ void Network::UpdateServer() } } if (SDL_TICKS_PASSED(SDL_GetTicks(), last_tick_sent_time + 25)) { - last_tick_sent_time = SDL_GetTicks(); Server_Send_TICK(); } if (SDL_TICKS_PASSED(SDL_GetTicks(), last_ping_sent_time + 3000)) { - last_ping_sent_time = SDL_GetTicks(); Server_Send_PING(); Server_Send_PINGLIST(); } + + switch (advertise_status) { + case ADVERTISE_STATUS_UNREGISTERED: + if (last_advertise_time == 0 || SDL_TICKS_PASSED(SDL_GetTicks(), last_advertise_time + MASTER_SERVER_REGISTER_TIME)) { + AdvertiseRegister(); + } + break; + case ADVERTISE_STATUS_REGISTERED: + if (SDL_TICKS_PASSED(SDL_GetTicks(), last_heartbeat_time + MASTER_SERVER_HEARTBEAT_TIME)) { + AdvertiseHeartbeat(); + } + break; + } + SOCKET socket = accept(listening_socket, NULL, NULL); if (socket == INVALID_SOCKET) { if (LAST_SOCKET_ERROR() != EWOULDBLOCK) { @@ -573,7 +647,7 @@ void Network::UpdateClient() bool connectfailed = false; switch(status){ case NETWORK_STATUS_RESOLVING:{ - if(server_address.GetResolveStatus() == NetworkAddress::RESOLVE_OK){ + if (server_address.GetResolveStatus() == NetworkAddress::RESOLVE_OK) { server_connection.socket = socket(server_address.ss->ss_family, SOCK_STREAM, IPPROTO_TCP); if (server_connection.socket == INVALID_SOCKET) { log_error("Unable to create socket."); @@ -606,8 +680,7 @@ void Network::UpdateClient() int error = 0; socklen_t len = sizeof(error); int result = getsockopt(server_connection.socket, SOL_SOCKET, SO_ERROR, (char*)&error, &len); - if (result != 0) - { + if (result != 0) { log_error("getsockopt failed with error %d", LAST_SOCKET_ERROR()); break; } @@ -631,29 +704,26 @@ void Network::UpdateClient() error = 0; socklen_t len = sizeof(error); result = getsockopt(server_connection.socket, SOL_SOCKET, SO_ERROR, (char*)&error, &len); - if (result != 0) - { + if (result != 0) { log_error("getsockopt failed with error %d", LAST_SOCKET_ERROR()); break; } if (error == 0) { status = NETWORK_STATUS_CONNECTED; server_connection.ResetLastPacketTime(); - Client_Send_AUTH(OPENRCT2_VERSION, gConfigNetwork.player_name, ""); + Client_Send_AUTH(gConfigNetwork.player_name, ""); window_network_status_open("Authenticating..."); } } }break; case NETWORK_STATUS_CONNECTED: if (!ProcessConnection(server_connection)) { - char errormsg[256]; - char reason[100]; - reason[0] = 0; + std::string errormsg = "Disconnected"; if (server_connection.last_disconnect_reason) { - sprintf(reason, ": %s", server_connection.last_disconnect_reason); + errormsg += ": "; + errormsg += server_connection.last_disconnect_reason; } - sprintf(errormsg, "Disconnected%s", reason); - window_network_status_open(errormsg); + window_network_status_open(errormsg.c_str()); Close(); } ProcessGameCommandQueue(); @@ -702,10 +772,10 @@ const char* Network::FormatChat(NetworkPlayer* fromplayer, const char* text) return formatted; } -void Network::SendPacketToClients(NetworkPacket& packet) +void Network::SendPacketToClients(NetworkPacket& packet, bool front) { for (auto it = client_connection_list.begin(); it != client_connection_list.end(); it++) { - (*it)->QueuePacket(std::move(NetworkPacket::Duplicate(packet))); + (*it)->QueuePacket(std::move(NetworkPacket::Duplicate(packet)), front); } } @@ -728,17 +798,152 @@ bool Network::CheckSRAND(uint32 tick, uint32 srand0) return true; } -void Network::Client_Send_AUTH(const char* gameversion, const char* name, const char* password) +void Network::KickPlayer(int playerId) +{ + NetworkPlayer *player = GetPlayerByID(playerId); + for(auto it = client_connection_list.begin(); it != client_connection_list.end(); it++) { + if ((*it)->player->id == playerId) { + // Disconnect the client gracefully + (*it)->last_disconnect_reason = "Kicked"; + Server_Send_SETDISCONNECTMSG(*(*it), "Get out of the server!"); + shutdown((*it)->socket, SHUT_RD); + (*it)->SendQueuedPackets(); + break; + } + } +} + +void Network::SetPassword(const char* password) +{ + Network::password = password; +} + +void Network::ShutdownClient() +{ + if (GetMode() == NETWORK_MODE_CLIENT) { + shutdown(server_connection.socket, SHUT_RDWR); + } +} + +std::string Network::GenerateAdvertiseKey() +{ + // Generate a string of 16 random hex characters (64-integer key as a hex formatted string) + static char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; + char key[17]; + for (int i = 0; i < 16; i++) { + int hexCharIndex = rand() % countof(hexChars); + key[i] = hexChars[hexCharIndex]; + } + key[countof(key) - 1] = 0; + + return key; +} + +const char *Network::GetMasterServerUrl() +{ + if (str_is_null_or_empty(gConfigNetwork.master_server_url)) { + return OPENRCT2_MASTER_SERVER_URL; + } else { + return gConfigNetwork.master_server_url; + } +} + +void Network::AdvertiseRegister() +{ +#ifndef DISABLE_HTTP + last_advertise_time = SDL_GetTicks(); + + // Send the registration request + std::string url = GetMasterServerUrl() + + std::string("?command=register&port=") + std::to_string(listening_port) + + std::string("&key=") + advertise_key; + http_request_json_async(url.c_str(), [](http_json_response *response) -> void { + if (response == NULL) { + log_warning("Unable to connect to master server"); + return; + } + + json_t *jsonStatus = json_object_get(response->root, "status"); + if (json_is_integer(jsonStatus)) { + int status = (int)json_integer_value(jsonStatus); + if (status == MASTER_SERVER_STATUS_OK) { + json_t *jsonToken = json_object_get(response->root, "token"); + if (json_is_string(jsonToken)) { + gNetwork.advertise_token = json_string_value(jsonToken); + gNetwork.advertise_status = ADVERTISE_STATUS_REGISTERED; + } + } else { + const char *message = "Invalid response from server"; + json_t *jsonMessage = json_object_get(response->root, "message"); + if (json_is_string(jsonMessage)) { + message = json_string_value(jsonMessage); + } + log_warning("Unable to advertise: %s", message); + } + } + http_request_json_dispose(response); + }); +#endif +} + +void Network::AdvertiseHeartbeat() +{ +#ifndef DISABLE_HTTP + // Send the heartbeat request + std::string url = GetMasterServerUrl() + + std::string("?command=heartbeat&token=") + advertise_token + + std::string("&players=") + std::to_string(network_get_num_players()); + + // TODO send status data (e.g. players) via JSON body + + gNetwork.last_heartbeat_time = SDL_GetTicks(); + http_request_json_async(url.c_str(), [](http_json_response *response) -> void { + if (response == NULL) { + log_warning("Unable to connect to master server"); + return; + } + + json_t *jsonStatus = json_object_get(response->root, "status"); + if (json_is_integer(jsonStatus)) { + int status = (int)json_integer_value(jsonStatus); + if (status == MASTER_SERVER_STATUS_OK) { + // Master server has successfully updated our server status + } else if (status == MASTER_SERVER_STATUS_INVALID_TOKEN) { + gNetwork.advertise_status = ADVERTISE_STATUS_UNREGISTERED; + log_warning("Master server heartbeat failed: Invalid Token"); + } + } + http_request_json_dispose(response); + }); +#endif +} + +void Network::Client_Send_AUTH(const char* name, const char* password) { std::unique_ptr packet = std::move(NetworkPacket::Allocate()); *packet << (uint32)NETWORK_COMMAND_AUTH; - packet->WriteString(gameversion); + packet->WriteString(OPENRCT2_VERSION); packet->WriteString(name); packet->WriteString(password); server_connection.authstatus = NETWORK_AUTH_REQUESTED; server_connection.QueuePacket(std::move(packet)); } +void Network::Server_Send_AUTH(NetworkConnection& connection) +{ + uint8 new_playerid = 0; + if (connection.player) { + new_playerid = connection.player->id; + } + std::unique_ptr packet = std::move(NetworkPacket::Allocate()); + *packet << (uint32)NETWORK_COMMAND_AUTH << (uint32)connection.authstatus << (uint8)new_playerid; + connection.QueuePacket(std::move(packet)); + if (connection.authstatus != NETWORK_AUTH_OK && connection.authstatus != NETWORK_AUTH_REQUIREPASSWORD) { + shutdown(connection.socket, SHUT_RD); + connection.SendQueuedPackets(); + } +} + void Network::Server_Send_MAP(NetworkConnection* connection) { int buffersize = 0x600000; @@ -765,7 +970,7 @@ void Network::Client_Send_CHAT(const char* text) { std::unique_ptr packet = std::move(NetworkPacket::Allocate()); *packet << (uint32)NETWORK_COMMAND_CHAT; - packet->Write((uint8*)text, strlen(text) + 1); + packet->WriteString(text); server_connection.QueuePacket(std::move(packet)); } @@ -773,7 +978,7 @@ void Network::Server_Send_CHAT(const char* text) { std::unique_ptr packet = std::move(NetworkPacket::Allocate()); *packet << (uint32)NETWORK_COMMAND_CHAT; - packet->Write((uint8*)text, strlen(text) + 1); + packet->WriteString(text); SendPacketToClients(*packet); } @@ -793,6 +998,7 @@ void Network::Server_Send_GAMECMD(uint32 eax, uint32 ebx, uint32 ecx, uint32 edx void Network::Server_Send_TICK() { + last_tick_sent_time = SDL_GetTicks(); std::unique_ptr packet = std::move(NetworkPacket::Allocate()); *packet << (uint32)NETWORK_COMMAND_TICK << (uint32)RCT2_GLOBAL(RCT2_ADDRESS_CURRENT_TICKS, uint32) << (uint32)RCT2_GLOBAL(RCT2_ADDRESS_SCENARIO_SRAND_0, uint32); SendPacketToClients(*packet); @@ -818,12 +1024,13 @@ void Network::Client_Send_PING() void Network::Server_Send_PING() { + last_ping_sent_time = SDL_GetTicks(); std::unique_ptr packet = std::move(NetworkPacket::Allocate()); *packet << (uint32)NETWORK_COMMAND_PING; for (auto it = client_connection_list.begin(); it != client_connection_list.end(); it++) { (*it)->ping_time = SDL_GetTicks(); } - SendPacketToClients(*packet); + SendPacketToClients(*packet, true); } void Network::Server_Send_PINGLIST() @@ -844,9 +1051,26 @@ void Network::Server_Send_SETDISCONNECTMSG(NetworkConnection& connection, const connection.QueuePacket(std::move(packet)); } +void Network::Server_Send_GAMEINFO(NetworkConnection& connection) +{ + std::unique_ptr packet = std::move(NetworkPacket::Allocate()); + *packet << (uint32)NETWORK_COMMAND_GAMEINFO; +#ifndef DISABLE_HTTP + json_t* obj = json_object(); + json_object_set(obj, "name", json_string(gConfigNetwork.server_name)); + json_object_set(obj, "requiresPassword", json_boolean(password.size() > 0)); + json_object_set(obj, "version", json_string(OPENRCT2_VERSION)); + json_object_set(obj, "players", json_integer(player_list.size())); + json_object_set(obj, "maxPlayers", json_integer(gConfigNetwork.maxplayers)); + json_object_set(obj, "description", json_string(gConfigNetwork.server_description)); + packet->WriteString(json_dumps(obj, 0)); + json_object_clear(obj); +#endif + connection.QueuePacket(std::move(packet)); +} + bool Network::ProcessConnection(NetworkConnection& connection) { - connection.SendQueuedPackets(); int packetStatus; do { packetStatus = connection.ReadPacket(); @@ -871,12 +1095,13 @@ bool Network::ProcessConnection(NetworkConnection& connection) break; } } while (packetStatus == NETWORK_READPACKET_MORE_DATA || packetStatus == NETWORK_READPACKET_SUCCESS); -#if !DEBUG + connection.SendQueuedPackets(); if (!connection.ReceivedPacketRecently()) { - connection.last_disconnect_reason = "No Data"; + if (!connection.last_disconnect_reason) { + connection.last_disconnect_reason = "No Data"; + } return false; } -#endif return true; } @@ -889,7 +1114,7 @@ void Network::ProcessPacket(NetworkConnection& connection, NetworkPacket& packet switch (gNetwork.GetMode()) { case NETWORK_MODE_SERVER: if (server_command_handlers[command]) { - if (connection.authstatus == NETWORK_AUTH_OK || command == NETWORK_COMMAND_AUTH) { + if (connection.authstatus == NETWORK_AUTH_OK || !packet.CommandRequiresAuth()) { (this->*server_command_handlers[command])(connection, packet); } } @@ -935,7 +1160,7 @@ void Network::RemoveClient(std::unique_ptr& connection) lineCh = utf8_write_codepoint(lineCh, FORMAT_RED); char reasonstr[100]; reasonstr[0] = 0; - if (connection->last_disconnect_reason) { + if (connection->last_disconnect_reason && strlen(connection->last_disconnect_reason) < sizeof(reasonstr)) { sprintf(reasonstr, " (%s)", connection->last_disconnect_reason); } sprintf(lineCh, "%s has disconnected%s", connection_player->name, reasonstr); @@ -991,6 +1216,27 @@ void Network::PrintError() int Network::Client_Handle_AUTH(NetworkConnection& connection, NetworkPacket& packet) { packet >> (uint32&)connection.authstatus >> (uint8&)player_id; + switch(connection.authstatus) { + case NETWORK_AUTH_BADNAME: + connection.last_disconnect_reason = "Bad Player Name"; + shutdown(connection.socket, SHUT_RDWR); + break; + case NETWORK_AUTH_BADVERSION: + connection.last_disconnect_reason = "Incorrect Software Version"; + shutdown(connection.socket, SHUT_RDWR); + break; + case NETWORK_AUTH_BADPASSWORD: + connection.last_disconnect_reason = "Bad Password"; + shutdown(connection.socket, SHUT_RDWR); + break; + case NETWORK_AUTH_FULL: + connection.last_disconnect_reason = "Server Full"; + shutdown(connection.socket, SHUT_RDWR); + break; + case NETWORK_AUTH_REQUIREPASSWORD: + window_network_status_open_password(); + break; + } return 1; } @@ -1000,21 +1246,25 @@ int Network::Server_Handle_AUTH(NetworkConnection& connection, NetworkPacket& pa const char* gameversion = packet.ReadString(); const char* name = packet.ReadString(); const char* password = packet.ReadString(); - uint8 playerid = 0; if (!gameversion || strcmp(gameversion, OPENRCT2_VERSION) != 0) { connection.authstatus = NETWORK_AUTH_BADVERSION; } else if (!name) { connection.authstatus = NETWORK_AUTH_BADNAME; } else - if (!password || strcmp(password, Network::password) != 0) { + if ((!password || strlen(password) == 0) && Network::password.size() > 0) { + connection.authstatus = NETWORK_AUTH_REQUIREPASSWORD; + } else + if (password && Network::password != password) { connection.authstatus = NETWORK_AUTH_BADPASSWORD; + } else + if (gConfigNetwork.maxplayers <= player_list.size()) { + connection.authstatus = NETWORK_AUTH_FULL; } else { connection.authstatus = NETWORK_AUTH_OK; NetworkPlayer* player = AddPlayer(name); connection.player = player; if (player) { - playerid = player->id; char text[256]; char* lineCh = text; lineCh = utf8_write_codepoint(lineCh, FORMAT_OUTLINE); @@ -1025,9 +1275,7 @@ int Network::Server_Handle_AUTH(NetworkConnection& connection, NetworkPacket& pa Server_Send_MAP(&connection); } } - std::unique_ptr responsepacket = std::move(NetworkPacket::Allocate()); - *responsepacket << (uint32)NETWORK_COMMAND_AUTH << (uint32)connection.authstatus << (uint8)playerid; - connection.QueuePacket(std::move(responsepacket)); + Server_Send_AUTH(connection); } return 1; } @@ -1182,15 +1430,21 @@ int Network::Client_Handle_PINGLIST(NetworkConnection& connection, NetworkPacket int Network::Client_Handle_SETDISCONNECTMSG(NetworkConnection& connection, NetworkPacket& packet) { - static char msg[256] = {0}; + static std::string msg; const char* disconnectmsg = packet.ReadString(); if (disconnectmsg) { - safe_strncpy(msg, disconnectmsg, sizeof(msg)); - connection.last_disconnect_reason = msg; + msg = disconnectmsg; + connection.last_disconnect_reason = msg.c_str(); } return 1; } +int Network::Server_Handle_GAMEINFO(NetworkConnection& connection, NetworkPacket& packet) +{ + Server_Send_GAMEINFO(connection); + return 1; +} + int network_init() { return gNetwork.Init(); @@ -1201,13 +1455,14 @@ void network_close() gNetwork.Close(); } +void network_shutdown_client() +{ + gNetwork.ShutdownClient(); +} + int network_begin_client(const char *host, int port) { - if (gNetwork.GetMode() == NETWORK_MODE_NONE) { return gNetwork.BeginClient(host, port); - } else { - return false; - } } int network_begin_server(int port) @@ -1300,18 +1555,9 @@ void network_send_gamecmd(uint32 eax, uint32 ebx, uint32 ecx, uint32 edx, uint32 } } -void Network::KickPlayer(int playerId) +void network_send_password(const char* password) { - NetworkPlayer *player = GetPlayerByID(playerId); - for(auto it = client_connection_list.begin(); it != client_connection_list.end(); it++) { - if ((*it)->player->id == playerId) { - // Disconnect the client gracefully - (*it)->last_disconnect_reason = "Kicked"; - Server_Send_SETDISCONNECTMSG(*(*it), "Get out of the server!"); - shutdown((*it)->socket, SHUT_RD); - break; - } - } + gNetwork.Client_Send_AUTH(gConfigNetwork.player_name, password); } void network_kick_player(int playerId) @@ -1319,9 +1565,15 @@ void network_kick_player(int playerId) gNetwork.KickPlayer(playerId); } +void network_set_password(const char* password) +{ + gNetwork.SetPassword(password); +} + #else int network_get_mode() { return NETWORK_MODE_NONE; } int network_get_status() { return NETWORK_STATUS_NONE; } +int network_get_authstatus() { return NETWORK_AUTH_NONE; } uint32 network_get_server_tick() { return RCT2_GLOBAL(RCT2_ADDRESS_CURRENT_TICKS, uint32); } void network_send_gamecmd(uint32 eax, uint32 ebx, uint32 ecx, uint32 edx, uint32 esi, uint32 edi, uint32 ebp, uint8 callback) {} void network_send_map() {} @@ -1334,7 +1586,10 @@ uint32 network_get_player_flags(unsigned int index) { return 0; } int network_get_player_ping(unsigned int index) { return 0; } int network_get_player_id(unsigned int index) { return 0; } void network_send_chat(const char* text) {} +void network_send_password(const char* password) {} void network_close() {} -void network_kick_player(int playerId) { } +void network_shutdown_client() {} +void network_kick_player(int playerId) {} +void network_set_password(const char* password) {} uint8 network_get_current_player_id() { return 0; } #endif /* DISABLE_NETWORK */ diff --git a/src/network/network.h b/src/network/network.h index 2356937cdb..0c092fa1f7 100644 --- a/src/network/network.h +++ b/src/network/network.h @@ -37,7 +37,9 @@ enum { NETWORK_AUTH_OK, NETWORK_AUTH_BADVERSION, NETWORK_AUTH_BADNAME, - NETWORK_AUTH_BADPASSWORD + NETWORK_AUTH_BADPASSWORD, + NETWORK_AUTH_FULL, + NETWORK_AUTH_REQUIREPASSWORD }; enum { @@ -69,6 +71,9 @@ extern "C" { #ifndef SHUT_RD #define SHUT_RD SD_RECEIVE #endif + #ifndef SHUT_RDWR + #define SHUT_RDWR SD_BOTH + #endif #else #include #include @@ -109,6 +114,7 @@ public: static std::unique_ptr Allocate(); static std::unique_ptr Duplicate(NetworkPacket& packet); uint8* GetData(); + uint32 GetCommand(); template NetworkPacket& operator<<(T value) { T swapped = ByteSwapBE(value); uint8* bytes = (uint8*)&swapped; data->insert(data->end(), bytes, bytes + sizeof(value)); return *this; } void Write(uint8* bytes, unsigned int size); @@ -118,6 +124,7 @@ public: const uint8* Read(unsigned int size); const char* ReadString(); void Clear(); + bool CommandRequiresAuth(); uint16 size; std::shared_ptr> data; @@ -141,7 +148,7 @@ public: NetworkConnection(); ~NetworkConnection(); int ReadPacket(); - void QueuePacket(std::unique_ptr packet); + void QueuePacket(std::unique_ptr packet, bool front = false); void SendQueuedPackets(); bool SetTCPNoDelay(bool on); bool SetNonBlocking(bool on); @@ -206,11 +213,16 @@ public: void Update(); NetworkPlayer* GetPlayerByID(int id); const char* FormatChat(NetworkPlayer* fromplayer, const char* text); - void SendPacketToClients(NetworkPacket& packet); + void SendPacketToClients(NetworkPacket& packet, bool front = false); bool CheckSRAND(uint32 tick, uint32 srand0); void KickPlayer(int playerId); + void SetPassword(const char* password); + void ShutdownClient(); + void AdvertiseRegister(); + void AdvertiseHeartbeat(); - void Client_Send_AUTH(const char* gameversion, const char* name, const char* password); + void Client_Send_AUTH(const char* name, const char* password); + void Server_Send_AUTH(NetworkConnection& connection); void Server_Send_MAP(NetworkConnection* connection = nullptr); void Client_Send_CHAT(const char* text); void Server_Send_CHAT(const char* text); @@ -222,6 +234,7 @@ public: void Server_Send_PING(); void Server_Send_PINGLIST(); void Server_Send_SETDISCONNECTMSG(NetworkConnection& connection, const char* msg); + void Server_Send_GAMEINFO(NetworkConnection& connection); std::vector> player_list; @@ -233,6 +246,8 @@ private: void RemoveClient(std::unique_ptr& connection); NetworkPlayer* AddPlayer(const char* name); void PrintError(); + const char *GetMasterServerUrl(); + std::string GenerateAdvertiseKey(); struct GameCommand { @@ -251,6 +266,7 @@ private: NetworkAddress server_address; bool wsa_initialized; SOCKET listening_socket; + unsigned short listening_port; NetworkConnection server_connection; uint32 last_tick_sent_time; uint32 last_ping_sent_time; @@ -261,9 +277,14 @@ private: std::list> client_connection_list; std::multiset game_command_queue; std::vector chunk_buffer; - char password[33]; + std::string password; bool _desynchronised; uint32 server_connect_time; + uint32 last_advertise_time; + std::string advertise_token; + std::string advertise_key; + int advertise_status; + uint32 last_heartbeat_time; void UpdateServer(); void UpdateClient(); @@ -284,6 +305,7 @@ private: int Server_Handle_PING(NetworkConnection& connection, NetworkPacket& packet); int Client_Handle_PINGLIST(NetworkConnection& connection, NetworkPacket& packet); int Client_Handle_SETDISCONNECTMSG(NetworkConnection& connection, NetworkPacket& packet); + int Server_Handle_GAMEINFO(NetworkConnection& connection, NetworkPacket& packet); }; #endif // __cplusplus @@ -294,6 +316,7 @@ extern "C" { #endif // __cplusplus int network_init(); void network_close(); +void network_shutdown_client(); int network_begin_client(const char *host, int port); int network_begin_server(int port); @@ -312,8 +335,10 @@ int network_get_player_id(unsigned int index); void network_send_map(); void network_send_chat(const char* text); void network_send_gamecmd(uint32 eax, uint32 ebx, uint32 ecx, uint32 edx, uint32 esi, uint32 edi, uint32 ebp, uint8 callback); +void network_send_password(const char* password); void network_kick_player(int playerId); +void network_set_password(const char* password); void network_print_error(); diff --git a/src/rct2.h b/src/rct2.h index 9a98d84e81..841aa697d8 100644 --- a/src/rct2.h +++ b/src/rct2.h @@ -105,6 +105,7 @@ typedef utf16* utf16string; #define OPENRCT2_BRANCH "develop" #define OPENRCT2_COMMIT_SHA1 "" #define OPENRCT2_COMMIT_SHA1_SHORT "" +#define OPENRCT2_MASTER_SERVER_URL "https://servers.openrct2.website" // Represent fixed point numbers. dp = decimal point typedef uint8 fixed8_1dp; diff --git a/src/windows/network_status.c b/src/windows/network_status.c index cadcbe99b9..1aadcf2d51 100644 --- a/src/windows/network_status.c +++ b/src/windows/network_status.c @@ -25,10 +25,13 @@ #include "../util/util.h" #include "../network/network.h" +char _password[33]; + enum WINDOW_NETWORK_STATUS_WIDGET_IDX { WIDX_BACKGROUND, WIDX_TITLE, WIDX_CLOSE, + WIDX_PASSWORD }; static rct_widget window_network_status_widgets[] = { @@ -42,6 +45,7 @@ static char window_network_status_text[1024]; static void window_network_status_mouseup(rct_window *w, int widgetIndex); static void window_network_status_update(rct_window *w); +static void window_network_status_textinput(rct_window *w, int widgetIndex, char *text); static void window_network_status_invalidate(rct_window *w); static void window_network_status_paint(rct_window *w, rct_drawpixelinfo *dpi); @@ -65,7 +69,7 @@ static rct_window_event_list window_network_status_events = { NULL, NULL, NULL, - NULL, + window_network_status_textinput, NULL, NULL, NULL, @@ -112,6 +116,16 @@ void window_network_status_close() window_close_by_class(WC_NETWORK_STATUS); } +void window_network_status_open_password() +{ + rct_window* window; + window = window_bring_to_front_by_class(WC_NETWORK_STATUS); + if (window == NULL) + return; + + window_text_input_raw_open(window, WIDX_PASSWORD, STR_PASSWORD_REQUIRED, STR_PASSWORD_REQUIRED_DESC, _password, 32); +} + static void window_network_status_mouseup(rct_window *w, int widgetIndex) { switch (widgetIndex) { @@ -126,6 +140,22 @@ static void window_network_status_update(rct_window *w) widget_invalidate(w, WIDX_BACKGROUND); } +static void window_network_status_textinput(rct_window *w, int widgetIndex, char *text) +{ + strcpy(_password, ""); + switch (widgetIndex) { + case WIDX_PASSWORD: + if (text != NULL) + safe_strncpy(_password, text, sizeof(_password)); + break; + } + if (text == NULL) { + network_shutdown_client(); + } else { + network_send_password(_password); + } +} + static void window_network_status_invalidate(rct_window *w) { window_network_status_widgets[WIDX_BACKGROUND].right = w->width - 1; diff --git a/src/windows/server_list.c b/src/windows/server_list.c index fb10c4442d..5bbb4bfbff 100644 --- a/src/windows/server_list.c +++ b/src/windows/server_list.c @@ -18,12 +18,15 @@ * along with this program. If not, see . *****************************************************************************/ +#include "../interface/colour.h" #include "../interface/themes.h" #include "../interface/widget.h" #include "../interface/window.h" #include "../localisation/localisation.h" +#include "../network/http.h" #include "../network/network.h" #include "../sprites.h" +#include "../windows/dropdown.h" #include "../util/util.h" #include "error.h" @@ -31,17 +34,23 @@ #define WHEIGHT_MIN 300 #define WWIDTH_MAX 1200 #define WHEIGHT_MAX 800 -#define ITEM_HEIGHT (3 + 11 + 1 + 11 + 3) +#define ITEM_HEIGHT (3 + 9 + 3) typedef struct { char *address; utf8 *name; + bool requiresPassword; utf8 *description; + char *version; + bool favorite; + uint8 players; + uint8 maxplayers; } saved_server; -char _playerName[64]; +char _playerName[32 + 1]; saved_server *_savedServers = NULL; int _numSavedServers = 0; +SDL_mutex *_mutex = 0; enum { WIDX_BACKGROUND, @@ -49,6 +58,7 @@ enum { WIDX_CLOSE, WIDX_PLAYER_NAME_INPUT, WIDX_LIST, + WIDX_FETCH_SERVERS, WIDX_ADD_SERVER, WIDX_START_SERVER }; @@ -64,14 +74,16 @@ static rct_widget window_server_list_widgets[] = { { WWT_CLOSEBOX, 0, 327, 337, 2, 13, STR_CLOSE_X, STR_CLOSE_WINDOW_TIP }, // close x button { WWT_TEXT_BOX, 1, 100, 344, 20, 31, (uint32)_playerName, STR_NONE }, // player name text box { WWT_SCROLL, 1, 6, 337, 37, 50, STR_NONE, STR_NONE }, // server list - { WWT_DROPDOWN_BUTTON, 1, 6, 106, 53, 64, STR_ADD_SERVER, STR_NONE }, // add server button - { WWT_DROPDOWN_BUTTON, 1, 112, 212, 53, 64, STR_START_SERVER, STR_NONE }, // start server button + { WWT_DROPDOWN_BUTTON, 1, 6, 106, 53, 64, STR_FETCH_SERVERS, STR_NONE }, // fetch servers button + { WWT_DROPDOWN_BUTTON, 1, 112, 212, 53, 64, STR_ADD_SERVER, STR_NONE }, // add server button + { WWT_DROPDOWN_BUTTON, 1, 218, 318, 53, 64, STR_START_SERVER, STR_NONE }, // start server button { WIDGETS_END }, }; static void window_server_list_close(rct_window *w); static void window_server_list_mouseup(rct_window *w, int widgetIndex); static void window_server_list_resize(rct_window *w); +static void window_server_list_dropdown(rct_window *w, int widgetIndex, int dropdownIndex); static void window_server_list_update(rct_window *w); static void window_server_list_scroll_getsize(rct_window *w, int scrollIndex, int *width, int *height); static void window_server_list_scroll_mousedown(rct_window *w, int scrollIndex, int x, int y); @@ -86,7 +98,7 @@ static rct_window_event_list window_server_list_events = { window_server_list_mouseup, window_server_list_resize, NULL, - NULL, + window_server_list_dropdown, NULL, window_server_list_update, NULL, @@ -112,17 +124,25 @@ static rct_window_event_list window_server_list_events = { window_server_list_scrollpaint }; +enum { + DDIDX_JOIN, + DDIDX_FAVORITE +}; + static int _hoverButtonIndex = -1; static void server_list_get_item_button(int buttonIndex, int x, int y, int width, int *outX, int *outY); -static void server_list_update_player_name(); static void server_list_load_saved_servers(); static void server_list_save_saved_servers(); static void dispose_saved_server_list(); static void dispose_saved_server(saved_server *serverInfo); -static void add_saved_server(char *address); +static saved_server* add_saved_server(char *address); static void remove_saved_server(int index); -static void join_server(char *address, bool spectate); +static void join_server(char *address); +static void fetch_servers(); +#ifndef DISABLE_HTTP +static void fetch_servers_callback(http_json_response* response); +#endif void window_server_list_open() { @@ -133,12 +153,17 @@ void window_server_list_open() if (window != NULL) return; + if (_mutex == 0) { + _mutex = SDL_CreateMutex(); + } + window = window_create_centred(WWIDTH_MIN, WHEIGHT_MIN, &window_server_list_events, WC_SERVER_LIST, WF_10 | WF_RESIZABLE); window->widgets = window_server_list_widgets; window->enabled_widgets = ( (1 << WIDX_CLOSE) | (1 << WIDX_PLAYER_NAME_INPUT) | + (1 << WIDX_FETCH_SERVERS) | (1 << WIDX_ADD_SERVER) | (1 << WIDX_START_SERVER) ); @@ -163,12 +188,18 @@ void window_server_list_open() server_list_load_saved_servers(); window->no_list_items = _numSavedServers; + + fetch_servers(); } static void window_server_list_close(rct_window *w) { - server_list_update_player_name(); dispose_saved_server_list(); + if (_mutex) { + SDL_LockMutex(_mutex); + SDL_DestroyMutex(_mutex); + _mutex = 0; + } } static void window_server_list_mouseup(rct_window *w, int widgetIndex) @@ -180,12 +211,14 @@ static void window_server_list_mouseup(rct_window *w, int widgetIndex) case WIDX_PLAYER_NAME_INPUT: window_start_textbox(w, widgetIndex, 1170, (uint32)_playerName, 63); break; + case WIDX_FETCH_SERVERS: + fetch_servers(); + break; case WIDX_ADD_SERVER: window_text_input_open(w, widgetIndex, STR_ADD_SERVER, STR_ENTER_HOSTNAME_OR_IP_ADDRESS, STR_NONE, 0, 128); break; case WIDX_START_SERVER: - server_list_update_player_name(); - window_loadsave_open(LOADSAVETYPE_LOAD | LOADSAVETYPE_GAME | LOADSAVETYPE_NETWORK, NULL); + window_server_start_open(); break; } } @@ -195,6 +228,25 @@ static void window_server_list_resize(rct_window *w) window_set_resize(w, WWIDTH_MIN, WHEIGHT_MIN, WWIDTH_MAX, WHEIGHT_MAX); } +static void window_server_list_dropdown(rct_window *w, int widgetIndex, int dropdownIndex) +{ + int serverIndex = w->selected_list_item; + if (serverIndex < 0) return; + if (serverIndex >= _numSavedServers) return; + + char *serverAddress = _savedServers[serverIndex].address; + + switch (dropdownIndex) { + case DDIDX_JOIN: + join_server(serverAddress); + break; + case DDIDX_FAVORITE: + _savedServers[serverIndex].favorite = !_savedServers[serverIndex].favorite; + server_list_save_saved_servers(); + break; + } +} + static void window_server_list_update(rct_window *w) { if (gCurrentTextBox.window.classification == w->classification && gCurrentTextBox.window.number == w->number) { @@ -217,22 +269,17 @@ static void window_server_list_scroll_mousedown(rct_window *w, int scrollIndex, char *serverAddress = _savedServers[serverIndex].address; - switch (_hoverButtonIndex) { - case WIDX_LIST_REMOVE: - remove_saved_server(serverIndex); - server_list_save_saved_servers(); - w->no_list_items = _numSavedServers; - window_invalidate(w); - break; - case WIDX_LIST_SPECTATE: - server_list_update_player_name(); - join_server(serverAddress, true); - break; - default: - server_list_update_player_name(); - join_server(serverAddress, false); - break; + rct_widget *listWidget = &w->widgets[WIDX_LIST]; + int ddx = w->x + listWidget->left + x; + int ddy = w->y + listWidget->top + y; + + gDropdownItemsFormat[0] = STR_JOIN_GAME; + if (_savedServers[serverIndex].favorite) { + gDropdownItemsFormat[1] = STR_REMOVE_FROM_FAVORITES; + } else { + gDropdownItemsFormat[1] = STR_ADD_TO_FAVORITES; } + window_dropdown_show_text(ddx, ddy, 0, COLOUR_GREY, 0, 2); } static void window_server_list_scroll_mouseover(rct_window *w, int scrollIndex, int x, int y) @@ -281,13 +328,18 @@ static void window_server_list_textinput(rct_window *w, int widgetIndex, char *t safe_strncpy(_playerName, text, sizeof(_playerName)); } + if (strlen(_playerName) > 0) { + SafeFree(gConfigNetwork.player_name); + gConfigNetwork.player_name = _strdup(_playerName); + config_save_default(); + } + widget_invalidate(w, WIDX_PLAYER_NAME_INPUT); break; case WIDX_ADD_SERVER: add_saved_server(text); server_list_save_saved_servers(); - w->no_list_items = _numSavedServers; window_invalidate(w); break; } @@ -305,23 +357,26 @@ static void window_server_list_invalidate(rct_window *w) window_server_list_widgets[WIDX_LIST].left = 6; window_server_list_widgets[WIDX_LIST].right = w->width - 6; window_server_list_widgets[WIDX_LIST].bottom = w->height - 6 - 11 - 6; + window_server_list_widgets[WIDX_FETCH_SERVERS].top = w->height - 6 - 11; + window_server_list_widgets[WIDX_FETCH_SERVERS].bottom = w->height - 6; window_server_list_widgets[WIDX_ADD_SERVER].top = w->height - 6 - 11; window_server_list_widgets[WIDX_ADD_SERVER].bottom = w->height - 6; window_server_list_widgets[WIDX_START_SERVER].top = w->height - 6 - 11; window_server_list_widgets[WIDX_START_SERVER].bottom = w->height - 6; + + w->no_list_items = _numSavedServers; } static void window_server_list_paint(rct_window *w, rct_drawpixelinfo *dpi) { window_draw_widgets(w, dpi); - gfx_draw_string_left(dpi, STR_PLAYER_NAME, NULL, w->colours[1], w->x + 6, w->y + w->widgets[WIDX_PLAYER_NAME_INPUT].top); + gfx_draw_string_left(dpi, STR_PLAYER_NAME, NULL, COLOUR_WHITE, w->x + 6, w->y + w->widgets[WIDX_PLAYER_NAME_INPUT].top); } static void window_server_list_scrollpaint(rct_window *w, rct_drawpixelinfo *dpi, int scrollIndex) { uint32 colour; - int bx, by; colour = ((char*)0x0141FC48)[w->colours[1] * 8]; colour = (colour << 24) | (colour << 16) | (colour << 8) | colour; @@ -342,27 +397,26 @@ static void window_server_list_scrollpaint(rct_window *w, rct_drawpixelinfo *dpi gfx_fill_rect(dpi, 0, y, width, y + ITEM_HEIGHT, 0x02000031); } + int colour = w->colours[1]; + if (serverDetails->favorite) { + colour = COLOUR_YELLOW; + } + // Draw server information if (highlighted) { - gfx_draw_string(dpi, serverDetails->address, w->colours[1], 3, y + 3); + gfx_draw_string(dpi, serverDetails->address, colour, 3, y + 3); } else { - gfx_draw_string(dpi, serverDetails->name, w->colours[1], 3, y + 3); + gfx_draw_string(dpi, serverDetails->name, colour, 3, y + 3); } - gfx_draw_string(dpi, serverDetails->description, w->colours[1], 3, y + 14); + //gfx_draw_string(dpi, serverDetails->description, w->colours[1], 3, y + 14); - // Draw delete server button - server_list_get_item_button(0, 0, y, width, &bx, &by); - if (highlighted && _hoverButtonIndex == WIDX_LIST_REMOVE) { - gfx_fill_rect_inset(dpi, bx, by, bx + 24, by + 24, w->colours[1], 0); + // Draw number of players + char players[32]; + players[0] = 0; + if (serverDetails->maxplayers > 0) { + sprintf(players, "%d/%d", serverDetails->players, serverDetails->maxplayers); } - gfx_draw_sprite(dpi, SPR_DEMOLISH, bx, by, 0); - - // Draw spectate server button - server_list_get_item_button(1, 0, y, width, &bx, &by); - if (highlighted && _hoverButtonIndex == WIDX_LIST_SPECTATE) { - gfx_fill_rect_inset(dpi, bx, by, bx + 24, by + 24, w->colours[1], 0); - } - gfx_draw_sprite(dpi, SPR_LOCATE, bx, by, 0); + gfx_draw_string(dpi, players, w->colours[1], width - 3 - 14 - gfx_get_string_width(players), y + 3); y += ITEM_HEIGHT; } @@ -374,15 +428,6 @@ static void server_list_get_item_button(int buttonIndex, int x, int y, int width *outY = y + 2; } -static void server_list_update_player_name() -{ - if (strlen(_playerName) > 0) { - SafeFree(gConfigNetwork.player_name); - gConfigNetwork.player_name = _strdup(_playerName); - config_save_default(); - } -} - static char *freadstralloc(SDL_RWops *file) { int capacity = 64; @@ -421,6 +466,7 @@ static void server_list_load_saved_servers() return; } + SDL_LockMutex(_mutex); dispose_saved_server_list(); // Read number of saved servers @@ -433,10 +479,16 @@ static void server_list_load_saved_servers() serverInfo->address = freadstralloc(file); serverInfo->name = freadstralloc(file); + serverInfo->requiresPassword = false; serverInfo->description = freadstralloc(file); + serverInfo->version = _strdup(""); + serverInfo->favorite = true; + serverInfo->players = 0; + serverInfo->maxplayers = 0; } SDL_RWclose(file); + SDL_UnlockMutex(_mutex); } static void server_list_save_saved_servers() @@ -453,23 +505,34 @@ static void server_list_save_saved_servers() return; } + SDL_LockMutex(_mutex); + int count = 0; + for (int i = 0; i < _numSavedServers; i++) { + saved_server *serverInfo = &_savedServers[i]; + if (serverInfo->favorite) { + count++; + } + } // Write number of saved servers - SDL_RWwrite(file, &_numSavedServers, sizeof(uint32), 1); + SDL_RWwrite(file, &count, sizeof(uint32), 1); // Write each saved server for (int i = 0; i < _numSavedServers; i++) { saved_server *serverInfo = &_savedServers[i]; - - SDL_RWwrite(file, serverInfo->address, strlen(serverInfo->address) + 1, 1); - SDL_RWwrite(file, serverInfo->name, strlen(serverInfo->name) + 1, 1); - SDL_RWwrite(file, serverInfo->description, strlen(serverInfo->description) + 1, 1); + if (serverInfo->favorite) { + SDL_RWwrite(file, serverInfo->address, strlen(serverInfo->address) + 1, 1); + SDL_RWwrite(file, serverInfo->name, strlen(serverInfo->name) + 1, 1); + SDL_RWwrite(file, serverInfo->description, strlen(serverInfo->description) + 1, 1); + } } SDL_RWclose(file); + SDL_UnlockMutex(_mutex); } static void dispose_saved_server_list() { + SDL_LockMutex(_mutex); if (_savedServers != NULL) { for (int i = 0; i < _numSavedServers; i++) { dispose_saved_server(&_savedServers[i]); @@ -478,6 +541,7 @@ static void dispose_saved_server_list() _savedServers = NULL; } _numSavedServers = 0; + SDL_UnlockMutex(_mutex); } static void dispose_saved_server(saved_server *serverInfo) @@ -485,10 +549,19 @@ static void dispose_saved_server(saved_server *serverInfo) SafeFree(serverInfo->address); SafeFree(serverInfo->name); SafeFree(serverInfo->description); + SafeFree(serverInfo->version); } -static void add_saved_server(char *address) +static saved_server* add_saved_server(char *address) { + SDL_LockMutex(_mutex); + for (int i = 0; i < _numSavedServers; i++) { + if (strcmp(_savedServers[i].address, address) == 0) { + SDL_UnlockMutex(_mutex); + return &_savedServers[i]; + } + } + _numSavedServers++; if (_savedServers == NULL) { _savedServers = malloc(_numSavedServers * sizeof(saved_server)); @@ -497,20 +570,30 @@ static void add_saved_server(char *address) } int index = _numSavedServers - 1; - _savedServers[index].address = _strdup(address); - _savedServers[index].name = _strdup(address); - _savedServers[index].description = _strdup(""); + saved_server* newserver = &_savedServers[index]; + newserver->address = _strdup(address); + newserver->name = _strdup(address); + newserver->requiresPassword = false; + newserver->description = _strdup(""); + newserver->version = _strdup(""); + newserver->favorite = false; + newserver->players = 0; + newserver->maxplayers = 0; + SDL_UnlockMutex(_mutex); + return newserver; } static void remove_saved_server(int index) { - if (_numSavedServers <= index) return; + SDL_LockMutex(_mutex); + if (_numSavedServers > index) { + int serversToMove = _numSavedServers - index - 1; + memmove(&_savedServers[index], &_savedServers[index + 1], serversToMove * sizeof(saved_server)); - int serversToMove = _numSavedServers - index - 1; - memmove(&_savedServers[index], &_savedServers[index + 1], serversToMove * sizeof(saved_server)); - - _numSavedServers--; - _savedServers = realloc(_savedServers, _numSavedServers * sizeof(saved_server)); + _numSavedServers--; + _savedServers = realloc(_savedServers, _numSavedServers * sizeof(saved_server)); + } + SDL_UnlockMutex(_mutex); } static char *substr(char *start, int length) @@ -521,7 +604,7 @@ static char *substr(char *start, int length) return result; } -static void join_server(char *address, bool spectate) +static void join_server(char *address) { int port = gConfigNetwork.default_port; @@ -551,3 +634,96 @@ static void join_server(char *address, bool spectate) free(address); } } + +static void fetch_servers() +{ +#ifndef DISABLE_HTTP + const char *masterServerUrl = OPENRCT2_MASTER_SERVER_URL; + if (!str_is_null_or_empty(gConfigNetwork.master_server_url)) { + masterServerUrl = gConfigNetwork.master_server_url; + } + + SDL_LockMutex(_mutex); + for (int i = 0; i < _numSavedServers; i++) { + if (!_savedServers[i].favorite) { + remove_saved_server(i); + i = 0; + } + } + SDL_UnlockMutex(_mutex); + http_request_json_async(masterServerUrl, fetch_servers_callback); +#endif +} + +#ifndef DISABLE_HTTP +static void fetch_servers_callback(http_json_response* response) +{ + if (response == NULL) { + log_warning("Unable to connect to master server"); + return; + } + + json_t *jsonStatus = json_object_get(response->root, "status"); + if (!json_is_number(jsonStatus)) { + http_request_json_dispose(response); + log_warning("Invalid response from master server"); + return; + } + + int status = (int)json_integer_value(jsonStatus); + if (status != 200) { + http_request_json_dispose(response); + log_warning("Master server failed to return servers"); + return; + } + + json_t *jsonServers = json_object_get(response->root, "servers"); + if (!json_is_array(jsonServers)) { + http_request_json_dispose(response); + log_warning("Invalid response from master server"); + return; + } + + int count = json_array_size(jsonServers); + for (int i = 0; i < count; i++) { + json_t *server = json_array_get(jsonServers, i); + if (!json_is_object(server)) { + continue; + } + + json_t *port = json_object_get(server, "port"); + json_t *name = json_object_get(server, "name"); + json_t *description = json_object_get(server, "description"); + json_t *requiresPassword = json_object_get(server, "requiresPassword"); + json_t *version = json_object_get(server, "version"); + json_t *players = json_object_get(server, "players"); + json_t *maxPlayers = json_object_get(server, "maxPlayers"); + json_t *ip = json_object_get(server, "ip"); + json_t *ip4 = json_object_get(ip, "v4"); + json_t *ip6 = json_object_get(ip, "v6"); + json_t *addressIp = json_array_get(ip4, 0); + + char address[256]; + snprintf(address, sizeof(address), "%s:%d", json_string_value(addressIp), (int)json_integer_value(port)); + + SDL_LockMutex(_mutex); + saved_server* newserver = add_saved_server(address); + SafeFree(newserver->name); + SafeFree(newserver->description); + SafeFree(newserver->version); + newserver->name = _strdup(json_string_value(name)); + newserver->requiresPassword = json_boolean_value(requiresPassword); + newserver->description = _strdup(json_string_value(description)); + newserver->version = _strdup(json_string_value(version)); + newserver->players = (uint8)json_integer_value(players); + newserver->maxplayers = (uint8)json_integer_value(maxPlayers); + SDL_UnlockMutex(_mutex); + } + http_request_json_dispose(response); + + rct_window* window = window_bring_to_front_by_class(WC_SERVER_LIST); + if (window != NULL) { + window_invalidate(window); + } +} +#endif diff --git a/src/windows/server_start.c b/src/windows/server_start.c new file mode 100644 index 0000000000..752c7efd4b --- /dev/null +++ b/src/windows/server_start.c @@ -0,0 +1,266 @@ +/***************************************************************************** +* Copyright (c) 2014 Ted John +* OpenRCT2, an open source clone of Roller Coaster Tycoon 2. +* +* This file is part of OpenRCT2. +* +* OpenRCT2 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 3 of the License, or +* (at your option) any later version. + +* This program 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 this program. If not, see . +*****************************************************************************/ + +#include "../config.h" +#include "../interface/themes.h" +#include "../interface/widget.h" +#include "../interface/window.h" +#include "../localisation/localisation.h" +#include "../network/network.h" +#include "../sprites.h" +#include "../util/util.h" +#include "error.h" + +char _port[7]; +char _name[65]; +char _password[33]; + +enum { + WIDX_BACKGROUND, + WIDX_TITLE, + WIDX_CLOSE, + WIDX_PORT_INPUT, + WIDX_NAME_INPUT, + WIDX_PASSWORD_INPUT, + WIDX_MAXPLAYERS, + WIDX_MAXPLAYERS_INCREASE, + WIDX_MAXPLAYERS_DECREASE, + WIDX_ADVERTISE_CHECKBOX, + WIDX_START_SERVER +}; + +#define WW 300 +#define WH 120 + +static rct_widget window_server_start_widgets[] = { + { WWT_FRAME, 0, 0, WW-1, 0, WH-1, 0xFFFFFFFF, STR_NONE }, // panel / background + { WWT_CAPTION, 0, 1, WW-2, 1, 14, STR_START_SERVER, STR_WINDOW_TITLE_TIP }, // title bar + { WWT_CLOSEBOX, 0, WW-13, WW-3, 2, 13, STR_CLOSE_X, STR_CLOSE_WINDOW_TIP }, // close x button + { WWT_TEXT_BOX, 1, 120, WW-8, 20, 32, (uint32)_port, STR_NONE }, // port text box + { WWT_TEXT_BOX, 1, 120, WW-8, 36, 48, (uint32)_name, STR_NONE }, // name text box + { WWT_TEXT_BOX, 1, 120, WW-8, 52, 64, (uint32)_password, STR_NONE }, // password text box + { WWT_SPINNER, 1, 120, WW-8, 68, 77, 1871, STR_NONE }, // max players + { WWT_DROPDOWN_BUTTON, 1, WW-18, WW-8, 68, 72, STR_NUMERIC_UP, STR_NONE }, + { WWT_DROPDOWN_BUTTON, 1, WW-18, WW-8, 72, 76, STR_NUMERIC_DOWN, STR_NONE }, + { WWT_CHECKBOX, 1, 6, WW-8, 85, 91, STR_ADVERTISE, STR_NONE }, // advertise checkbox + { WWT_DROPDOWN_BUTTON, 1, 6, 106, WH-6-11, WH-6, STR_START_SERVER, STR_NONE }, // start server button + { WIDGETS_END }, +}; + +static void window_server_start_close(rct_window *w); +static void window_server_start_mouseup(rct_window *w, int widgetIndex); +static void window_server_start_update(rct_window *w); +static void window_server_start_textinput(rct_window *w, int widgetIndex, char *text); +static void window_server_start_invalidate(rct_window *w); +static void window_server_start_paint(rct_window *w, rct_drawpixelinfo *dpi); + +static rct_window_event_list window_server_start_events = { + window_server_start_close, + window_server_start_mouseup, + NULL, + NULL, + NULL, + NULL, + window_server_start_update, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + window_server_start_textinput, + NULL, + NULL, + NULL, + NULL, + NULL, + window_server_start_invalidate, + window_server_start_paint, + NULL +}; + +void window_server_start_open() +{ + rct_window* window; + + // Check if window is already open + window = window_bring_to_front_by_class(WC_SERVER_START); + if (window != NULL) + return; + + window = window_create_centred(WW, WH, &window_server_start_events, WC_SERVER_START, WF_10); + + window->widgets = window_server_start_widgets; + window->enabled_widgets = ( + (1 << WIDX_CLOSE) | + (1 << WIDX_PORT_INPUT) | + (1 << WIDX_NAME_INPUT) | + (1 << WIDX_PASSWORD_INPUT) | + (1 << WIDX_MAXPLAYERS) | + (1 << WIDX_MAXPLAYERS_INCREASE) | + (1 << WIDX_MAXPLAYERS_DECREASE) | + (1 << WIDX_ADVERTISE_CHECKBOX) | + (1 << WIDX_START_SERVER) + ); + window_init_scroll_widgets(window); + window->no_list_items = 0; + window->selected_list_item = -1; + window->frame_no = 0; + window->min_width = window->width; + window->min_height = window->height; + window->max_width = window->min_width; + window->max_height = window->min_height; + + window->page = 0; + window->list_information_type = 0; + window->colours[0] = 1; + window->colours[1] = 26; + window->colours[2] = 26; + + sprintf(_port, "%lu", gConfigNetwork.default_port); + safe_strncpy(_name, gConfigNetwork.server_name, sizeof(_name)); +} + +static void window_server_start_close(rct_window *w) +{ + +} + +static void window_server_start_mouseup(rct_window *w, int widgetIndex) +{ + switch (widgetIndex) { + case WIDX_CLOSE: + window_close(w); + break; + case WIDX_PORT_INPUT: + window_start_textbox(w, widgetIndex, 1170, (uint32)_port, 6); + break; + case WIDX_NAME_INPUT: + window_start_textbox(w, widgetIndex, 1170, (uint32)_name, 64); + break; + case WIDX_PASSWORD_INPUT: + window_start_textbox(w, widgetIndex, 1170, (uint32)_password, 32); + break; + case WIDX_MAXPLAYERS_INCREASE: + if (gConfigNetwork.maxplayers < 255) { + gConfigNetwork.maxplayers++; + } + config_save_default(); + window_invalidate(w); + break; + case WIDX_MAXPLAYERS_DECREASE: + if (gConfigNetwork.maxplayers > 1) { + gConfigNetwork.maxplayers--; + } + config_save_default(); + window_invalidate(w); + break; + case WIDX_ADVERTISE_CHECKBOX: + gConfigNetwork.advertise = !gConfigNetwork.advertise; + config_save_default(); + window_invalidate(w); + break; + case WIDX_START_SERVER: + network_set_password(_password); + window_loadsave_open(LOADSAVETYPE_LOAD | LOADSAVETYPE_GAME | LOADSAVETYPE_NETWORK, NULL); + break; + } +} + +static void window_server_start_update(rct_window *w) +{ + if (gCurrentTextBox.window.classification == w->classification && gCurrentTextBox.window.number == w->number) { + window_update_textbox_caret(); + widget_invalidate(w, WIDX_NAME_INPUT); + widget_invalidate(w, WIDX_PASSWORD_INPUT); + } +} + +static void window_server_start_textinput(rct_window *w, int widgetIndex, char *text) +{ + if (text == NULL) return; + + switch (widgetIndex) { + case WIDX_PORT_INPUT: + if (strcmp(_port, text) == 0) + return; + + memset(_port, 0, sizeof(_port)); + if (strlen(text) > 0) { + safe_strncpy(_port, text, sizeof(_port)); + } + + gConfigNetwork.default_port = atoi(_port); + config_save_default(); + + widget_invalidate(w, WIDX_NAME_INPUT); + break; + case WIDX_NAME_INPUT: + if (strcmp(_name, text) == 0) + return; + + memset(_name, 0, sizeof(_name)); + if (strlen(text) > 0) { + safe_strncpy(_name, text, sizeof(_name)); + } + + if (strlen(_name) > 0) { + SafeFree(gConfigNetwork.server_name); + gConfigNetwork.server_name = _strdup(_name); + config_save_default(); + } + + widget_invalidate(w, WIDX_NAME_INPUT); + break; + case WIDX_PASSWORD_INPUT: + if (strcmp(_password, text) == 0) + return; + + memset(_password, 0, sizeof(_password)); + if (strlen(text) > 0) { + safe_strncpy(_password, text, sizeof(_password)); + } + + widget_invalidate(w, WIDX_PASSWORD_INPUT); + break; + } +} + +static void window_server_start_invalidate(rct_window *w) +{ + widget_set_checkbox_value(w, WIDX_ADVERTISE_CHECKBOX, gConfigNetwork.advertise); + RCT2_GLOBAL(0x013CE964, uint16) = gConfigNetwork.maxplayers; +} + +static void window_server_start_paint(rct_window *w, rct_drawpixelinfo *dpi) +{ + window_draw_widgets(w, dpi); + + gfx_draw_string_left(dpi, STR_PORT, NULL, w->colours[1], w->x + 6, w->y + w->widgets[WIDX_PORT_INPUT].top); + gfx_draw_string_left(dpi, STR_SERVER_NAME, NULL, w->colours[1], w->x + 6, w->y + w->widgets[WIDX_NAME_INPUT].top); + gfx_draw_string_left(dpi, STR_PASSWORD, NULL, w->colours[1], w->x + 6, w->y + w->widgets[WIDX_PASSWORD_INPUT].top); + gfx_draw_string_left(dpi, STR_MAX_PLAYERS, NULL, w->colours[1], w->x + 6, w->y + w->widgets[WIDX_MAXPLAYERS].top); +}