Rigs of Rods 2023.09
Soft-body Physics Simulation
All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages
Loading...
Searching...
No Matches
GUI_MultiplayerSelector.cpp
Go to the documentation of this file.
1/*
2 This source file is part of Rigs of Rods
3
4 Copyright 2005-2012 Pierre-Michel Ricordel
5 Copyright 2007-2012 Thomas Fischer
6 Copyright 2013-2020 Petr Ohlidal
7
8 For more information, see http://www.rigsofrods.org/
9
10 Rigs of Rods is free software: you can redistribute it and/or modify
11 it under the terms of the GNU General Public License version 3, as
12 published by the Free Software Foundation.
13
14 Rigs of Rods is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with Rigs of Rods. If not, see <http://www.gnu.org/licenses/>.
21*/
22
24
25#include "Application.h"
26#include "ContentManager.h"
27#include "GameContext.h"
28#include "GUIManager.h"
29#include "GUIUtils.h"
30#include "RoRnet.h"
31#include "RoRVersion.h"
32#include "Language.h"
33
34#include <imgui.h>
35#include <rapidjson/document.h>
36#include <fmt/core.h>
37#include <vector>
38
39#ifdef USE_CURL
40# include <curl/curl.h>
41# include <curl/easy.h>
42#endif //USE_CURL
43
44#if defined(_MSC_VER) && defined(GetObject) // This MS Windows macro from <wingdi.h> (Windows Kit 8.1) clashes with RapidJSON
45# undef GetObject
46#endif
47
48using namespace RoR;
49using namespace GUI;
50
51#if defined(USE_CURL)
52
53// From example: https://gist.github.com/whoshuu/2dc858b8730079602044
54size_t CurlWriteFunc(void *ptr, size_t size, size_t nmemb, std::string* data)
55{
56 data->append((char*) ptr, size * nmemb);
57 return size * nmemb;
58}
59
60void FetchServerlist(std::string portal_url)
61{
62 std::string serverlist_url = portal_url + "/server-list?json=true";
63 std::string response_payload;
64 std::string response_header;
65 long response_code = 0;
66
67 CURL *curl = curl_easy_init();
68 curl_easy_setopt(curl, CURLOPT_URL, serverlist_url.c_str());
69 curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, CurlWriteFunc);
70 curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_payload);
71 curl_easy_setopt(curl, CURLOPT_HEADERDATA, &response_header);
72
73 CURLcode curl_result = curl_easy_perform(curl);
74 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code);
75
76 curl_easy_cleanup(curl);
77 curl = nullptr;
78
79 if (curl_result != CURLE_OK || response_code != 200)
80 {
81 Ogre::LogManager::getSingleton().stream()
82 << "[RoR|Multiplayer] Failed to retrieve serverlist;"
83 << " Error: '" << curl_easy_strerror(curl_result) << "'; HTTP status code: " << response_code;
84
85 CurlFailInfo* failinfo = new CurlFailInfo();
86 failinfo->title = _LC("MultiplayerSelector", "Error connecting to server :(");
87 failinfo->curl_result = curl_result;
88 failinfo->http_response = response_code;
89
92 return;
93 }
94
95 rapidjson::Document j_data_doc;
96 j_data_doc.Parse(response_payload.c_str());
97 if (j_data_doc.HasParseError() || !j_data_doc.IsArray())
98 {
99 Ogre::LogManager::getSingleton().stream()
100 << "[RoR|Multiplayer] Error parsing serverlist JSON"; // TODO: Report the actual error
102 Message(MSG_NET_REFRESH_SERVERLIST_FAILURE, _LC("MultiplayerSelector", "Server returned invalid data :(")));
103 return;
104 }
105
106 // Pre-process data for display
107 size_t num_rows = j_data_doc.GetArray().Size();
108 GUI::MpServerInfoVec* servers_ptr = new GUI::MpServerInfoVec();
109 GUI::MpServerInfoVec& servers = *servers_ptr;
110 servers.resize(num_rows);
111 for (size_t i = 0; i < num_rows; ++i)
112 {
113 rapidjson::Value& j_row = j_data_doc[static_cast<rapidjson::SizeType>(i)];
114
115 servers[i].display_name = j_row["name"].GetString();
116 servers[i].display_terrn = j_row["terrain-name"].GetString();
117 servers[i].net_host = j_row["ip"].GetString();
118 servers[i].net_port = j_row["port"].GetInt();
119
120 servers[i].has_password = j_row["has-password"].GetBool();
121 servers[i].display_passwd = servers[i].has_password ? _LC("MultiplayerSelector","Yes") : _LC("MultiplayerSelector","No");
122
123 servers[i].display_host = fmt::format("{}:{}", j_row["ip"].GetString(), j_row["port"].GetInt());
124 servers[i].display_users = fmt::format("{} / {}", j_row["current-users"].GetInt(), j_row["max-clients"].GetInt());
125
126 servers[i].net_version = j_row["version"].GetString();
127 servers[i].display_version = Ogre::StringUtil::replaceAll(j_row["version"].GetString(), "RoRnet_", "");
128 }
129
131 Message(MSG_NET_REFRESH_SERVERLIST_SUCCESS, (void*)servers_ptr));
132}
133#endif // defined(USE_CURL)
134
135inline void DrawTableHeader(const char* title) // Internal helper
136{
137 float table_padding_y = 4.f;
138 ImGui::SetCursorPosY(ImGui::GetCursorPosY() + table_padding_y);
139 ImGui::Text("%s", title);
140 ImGui::NextColumn();
141}
142
144{
145 snprintf(m_window_title, 100, "Multiplayer (Rigs of Rods %s | %s)", ROR_VERSION_STRING, RORNET_VERSION);
146}
147
149{
150 int window_flags = ImGuiWindowFlags_NoCollapse;
151 ImGui::SetNextWindowSize(ImVec2(750.f, 400.f), ImGuiCond_FirstUseEver);
152 ImGui::SetNextWindowPosCenter();
153 bool keep_open = true;
154 ImGui::Begin(m_window_title, &keep_open, window_flags);
155
156 ImGui::BeginTabBar("GameSettingsTabs");
157
158 if (ImGui::BeginTabItem(_LC("MultiplayerSelector", "Online (click to refresh)")))
159 {
160 if (ImGui::IsItemClicked())
161 {
162 this->StartAsyncRefresh();
163 }
164 this->DrawServerlistTab();
165 ImGui::EndTabItem();
166 }
167 if (ImGui::BeginTabItem(_LC("MultiplayerSelector", "Direct IP")))
168 {
169 this->DrawDirectTab();
170 ImGui::EndTabItem();
171 }
172 int settingstab_flags = m_set_settings_tab_selected ? ImGuiTabItemFlags_SetSelected : 0;
174 if (ImGui::BeginTabItem(_LC("MultiplayerSelector", "Settings"), nullptr, settingstab_flags))
175 {
176 this->DrawSettingsTab();
177 ImGui::EndTabItem();
178 }
179
180 ImGui::EndTabBar();
181
182 ImGui::End();
183 if (!keep_open)
184 {
185 this->SetVisible(false);
186 }
187}
188
190{
191 ImGui::PushID("setup");
192
193 DrawGCheckbox(App::mp_join_on_startup, _LC("MultiplayerSelector", "Auto connect"));
194 DrawGCheckbox(App::mp_chat_auto_hide, _LC("MultiplayerSelector", "Auto hide chat"));
195 DrawGCheckbox(App::mp_hide_net_labels, _LC("MultiplayerSelector", "Hide net labels"));
196 DrawGCheckbox(App::mp_hide_own_net_label, _LC("MultiplayerSelector", "Hide own net label"));
197 DrawGCheckbox(App::mp_pseudo_collisions, _LC("MultiplayerSelector", "Multiplayer collisions"));
198 DrawGCheckbox(App::mp_cyclethru_net_actors, _LC("MultiplayerSelector", "Include remote actors when cycling via hotkeys"));
199
200 ImGui::SetCursorPosY(ImGui::GetCursorPosY() + BUTTONS_EXTRA_SPACE);
201 ImGui::Separator();
202
203 ImGui::PushItemWidth(250.f);
204
205 DrawGTextEdit(App::mp_player_name, _LC("MultiplayerSelector", "Player nickname"), m_player_name_buf);
206 DrawGTextEdit(App::mp_server_password, _LC("MultiplayerSelector", "Default server password"), m_password_buf);
207
208 ImGui::SetCursorPosY(ImGui::GetCursorPosY() + BUTTONS_EXTRA_SPACE);
209 ImGui::Separator();
210
211 DrawGTextEdit(App::mp_player_token, _LC("MultiplayerSelector", "User token"), m_user_token_buf);
212 ImGui::SameLine();
213 ImHyperlink("https://forum.rigsofrods.org/account/user-token", _LC("MultiplayerSelector", "(Get token online)"));
214 ImGui::TextDisabled(_LC("MultiplayerSelector", "Never share your user token, even if someone is claiming to be an administrator."));
215 ImGui::PopItemWidth();
216
217 ImGui::PopID();
218}
219
221{
222 ImGui::PushID("direct");
223
224 ImGui::PushItemWidth(250.f);
225 DrawGTextEdit(App::mp_server_host, _LC("MultiplayerSelector", "Server host"), m_server_host_buf);
226 DrawGIntBox(App::mp_server_port, _LC("MultiplayerSelector", "Server port"));
227 ImGui::InputText( _LC("MultiplayerSelector", "Server password"), m_password_buf.GetBuffer(), m_password_buf.GetCapacity());
228 ImGui::PopItemWidth();
229
230 ImGui::SetCursorPosY(ImGui::GetCursorPosY() + BUTTONS_EXTRA_SPACE);
231 if (ImGui::Button(_LC("MultiplayerSelector", "Join")))
232 {
235 }
236
237 ImGui::PopID();
238}
239
240
242{
244
245 // LOAD RESOURCES
246 if (!m_lock_icon)
247 {
248 try
249 {
251 m_lock_icon = Ogre::TextureManager::getSingleton().load(
252 "lock.png", ContentManager::ResourcePack::FAMICONS.resource_group_name);
253 }
254 catch (...) {} // Logged by OGRE
255 }
256
257 if (m_show_spinner)
258 {
259 float spinner_size = 25.f;
260 ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.f) - spinner_size);
261 ImGui::SetCursorPosY((ImGui::GetWindowSize().y / 2.f) - spinner_size);
262 LoadingIndicatorCircle("spinner", spinner_size, theme.value_blue_text_color, theme.value_blue_text_color, 10, 10);
263 }
264
265 // DRAW SERVERLIST TABLE
266 if (m_draw_table)
267 {
268 // Setup serverlist table ... the scroll area
269 const float table_height = ImGui::GetWindowHeight()
270 - ((2.f * ImGui::GetStyle().WindowPadding.y) + (3.f * ImGui::GetItemsLineHeightWithSpacing())
271 + ImGui::GetStyle().ItemSpacing.y);
272 ImGui::BeginChild("scrolling", ImVec2(0.f, table_height), false);
273 // ... and the table itself
274 const float table_width = ImGui::GetWindowContentRegionWidth();
275 ImGui::Columns(5, "mp-selector-columns"); // Col #0: Server name (and lock icon)
276 ImGui::SetColumnOffset(1, 0.36f * table_width); // Col #1: Terrain name
277 ImGui::SetColumnOffset(2, 0.67f * table_width); // Col #2: Users/Max
278 ImGui::SetColumnOffset(3, 0.74f * table_width); // Col #3: Version
279 ImGui::SetColumnOffset(4, 0.82f * table_width); // Col #4: Host/Port
280 // Draw table header
281 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + TABLE_PADDING_LEFT);
282 DrawTableHeader(_LC("MultiplayerSelector", "Name"));
283 DrawTableHeader(_LC("MultiplayerSelector", "Terrain"));
284 DrawTableHeader(_LC("MultiplayerSelector", "Users"));
285 DrawTableHeader(_LC("MultiplayerSelector", "Version"));
286 DrawTableHeader(_LC("MultiplayerSelector", "Host/Port"));
287 ImGui::Separator();
288 // Draw table body
289 for (int i = 0; i < (int)m_serverlist_data.size(); i++)
290 {
291 ImGui::PushID(i);
292
293 // First column (name)
294 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + TABLE_PADDING_LEFT);
295 MpServerInfo& server = m_serverlist_data[i];
296 if (ImGui::Selectable(server.display_name.c_str(), m_selected_item == i, ImGuiSelectableFlags_SpanAllColumns))
297 {
298 // Update selection
299 m_selected_item = i;
300 }
301 if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0))
302 {
303 // Handle left doubleclick
305 App::mp_server_host->setStr(server.net_host.c_str());
308 }
309 if (server.has_password && m_lock_icon)
310 {
311 // Draw lock icon for password-protected servers.
312 ImGui::SameLine();
313 ImGui::Image(reinterpret_cast<ImTextureID>(m_lock_icon->getHandle()), ImVec2(16, 16));
314 }
315 ImGui::NextColumn();
316
317 bool compatible = (server.net_version == RORNET_VERSION);
318 ImVec4 version_color = compatible ? ImVec4(0.0f, 0.9f, 0.0f, 1.0f) : ImVec4(0.9f, 0.0f, 0.0f, 1.0f);
319
320 // Other collumns
321 ImGui::Text("%s", server.display_terrn.c_str()); ImGui::NextColumn();
322 ImGui::Text("%s", server.display_users.c_str()); ImGui::NextColumn();
323 ImGui::PushStyleColor(ImGuiCol_Text, version_color);
324 ImGui::Text("%s", server.display_version.c_str()); ImGui::NextColumn();
325 ImGui::PopStyleColor();
326 ImGui::Text("%s", server.display_host.c_str()); ImGui::NextColumn();
327
328 ImGui::PopID();
329 }
330 ImGui::Columns(1);
331 ImGui::EndChild(); // End of scroll area
332
333 // Simple join button (and password input box)
335 {
337 if (ImGui::Button(_LC("MultiplayerSelector", "Join"), ImVec2(200.f, 0.f)))
338 {
340 App::mp_server_host->setStr(server.net_host.c_str());
343 }
344 if (server.has_password)
345 {
346 // TODO: Find out why this is always visible ~ ulteq 01/2019
347 ImGui::SameLine();
348 ImGui::PushItemWidth(250.f);
349 ImGui::InputText(_LC("MultiplayerSelector", "Server password"), m_password_buf.GetBuffer(), m_password_buf.GetCapacity());
350 ImGui::PopItemWidth();
351 }
352 }
353 }
354
355 // DRAW CENTERED LABEL
356 if (m_serverlist_msg != "")
357 {
358 const ImVec2 label_size = ImGui::CalcTextSize(m_serverlist_msg.c_str());
359 float y = (ImGui::GetWindowSize().y / 2.f) - (ImGui::GetTextLineHeight() / 2.f);
360 ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.f) - (label_size.x / 2.f));
361 ImGui::SetCursorPosY(y);
362 ImGui::TextColored(m_serverlist_msg_color, "%s", m_serverlist_msg.c_str());
363 y += ImGui::GetTextLineHeightWithSpacing();
364
365 if (m_serverlist_curlmsg != "")
366 {
367 const ImVec2 detail_size = ImGui::CalcTextSize(m_serverlist_curlmsg.c_str());
368 ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.f) - (detail_size.x / 2.f));
369 ImGui::SetCursorPosY(y);
370 ImGui::TextDisabled("%s", m_serverlist_curlmsg.c_str());
371 y += ImGui::GetTextLineHeight();
372 }
373
374 if (m_serverlist_httpmsg != "")
375 {
376 const ImVec2 detail_size = ImGui::CalcTextSize(m_serverlist_httpmsg.c_str());
377 ImGui::SetCursorPosX((ImGui::GetWindowSize().x / 2.f) - (detail_size.x / 2.f));
378 ImGui::SetCursorPosY(y);
379 ImGui::TextDisabled("%s", m_serverlist_httpmsg.c_str());
380 }
381 }
382}
383
385{
386#if defined(USE_CURL)
387 m_show_spinner = true;
388 m_draw_table = false;
389 m_serverlist_data.clear();
390 m_selected_item = -1;
391 m_serverlist_msg = "";
392 std::packaged_task<void(std::string)> task(FetchServerlist);
393 std::thread(std::move(task), App::mp_api_url->getStr()).detach(); // launch on a thread
394#endif // defined(USE_CURL)
395}
396
398{
399 m_is_visible = visible;
400 if (visible && m_serverlist_data.size() == 0) // Do an initial refresh
401 {
402 this->StartAsyncRefresh();
404 }
405 else if (!visible && App::app_state->getEnum<AppState>() == AppState::MAIN_MENU)
406 {
408 }
409}
410
412{
413 m_show_spinner = false;
414 m_serverlist_msg = failinfo->title;
416 m_draw_table = false;
417 if (failinfo->curl_result != CURLE_OK)
418 m_serverlist_curlmsg = curl_easy_strerror(failinfo->curl_result);
419 if (failinfo->http_response != 0)
420 m_serverlist_httpmsg = fmt::format(_L("HTTP code: {}"), failinfo->http_response);
421}
422
424{
425 m_show_spinner = false;
426 m_serverlist_data = *data;
427 m_draw_table = true;
428 if (m_serverlist_data.empty())
429 {
430 m_serverlist_msg = _LC("MultiplayerSelector", "There are no available servers :/");
432 }
433 else
434 {
435 m_serverlist_msg = "";
436 }
437}
438
Central state/object manager and communications hub.
#define _L
size_t CurlWriteFunc(void *ptr, size_t size, size_t nmemb, std::string *data)
void FetchServerlist(std::string portal_url)
void DrawTableHeader(const char *title)
Game state manager and message-queue provider.
#define _LC(ctx, str)
Definition Language.h:38
const char *const ROR_VERSION_STRING
std::string const & getStr() const
Definition CVar.h:95
void setStr(std::string const &str)
Definition CVar.h:83
void setVal(T val)
Definition CVar.h:72
void AddResourcePack(ResourcePack const &resource_pack, std::string const &override_rgn="")
Loads resources if not already loaded.
std::string m_serverlist_httpmsg
Displayed as dimmed text.
void DisplayRefreshFailed(CurlFailInfo *failinfo)
void StartAsyncRefresh()
Launch refresh from main thread.
std::string m_serverlist_curlmsg
Displayed as dimmed text.
void UpdateServerlist(MpServerInfoVec *data)
GUI::GameMainMenu GameMainMenu
Definition GUIManager.h:117
GuiTheme & GetTheme()
Definition GUIManager.h:168
void PushMessage(Message m)
Doesn't guarantee order! Use ChainMessage() if order matters.
char * GetBuffer()
Definition Str.h:48
size_t GetCapacity() const
Definition Str.h:49
@ MSG_NET_CONNECT_REQUESTED
Definition Application.h:98
@ MSG_NET_REFRESH_SERVERLIST_FAILURE
Payload = RoR::CurlFailInfo* (owner)
@ MSG_NET_REFRESH_SERVERLIST_SUCCESS
Payload = GUI::MpServerInfoVec* (owner)
CVar * mp_player_name
CVar * mp_server_password
CVar * mp_api_url
ContentManager * GetContentManager()
CVar * mp_hide_own_net_label
CVar * mp_hide_net_labels
CVar * mp_cyclethru_net_actors
Include remote actors when cycling through with CTRL + [ and CTRL + ].
CVar * mp_server_port
GUIManager * GetGuiManager()
GameContext * GetGameContext()
CVar * app_state
CVar * mp_chat_auto_hide
CVar * mp_player_token
CVar * mp_pseudo_collisions
CVar * mp_server_host
CVar * mp_join_on_startup
std::vector< MpServerInfo > MpServerInfoVec
void DrawGTextEdit(CVar *cvar, const char *label, Str< 1000 > &buf)
Definition GUIUtils.cpp:345
void ImHyperlink(std::string url, std::string caption="", bool tooltip=true)
Full-featured hypertext with tooltip showing full URL.
Definition GUIUtils.cpp:612
void DrawGIntBox(CVar *cvar, const char *label)
Definition GUIUtils.cpp:307
bool DrawGCheckbox(CVar *cvar, const char *label)
Definition GUIUtils.cpp:287
void LoadingIndicatorCircle(const char *label, const float indicator_radius, const ImVec4 &main_color, const ImVec4 &backdrop_color, const int circle_count, const float speed)
Draws animated loading spinner.
Definition GUIUtils.cpp:158
#define RORNET_VERSION
Definition RoRnet.h:35
static const ResourcePack FAMICONS
CURLcode curl_result
Definition Network.h:49
std::string title
Definition Network.h:48
Unified game event system - all requests and state changes are reported using a message.
Definition GameContext.h:52