1 /*
2 * Copyright (C) 2004-2020 by the Widelands Development Team
3 *
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; either version 2
7 * of the License, or (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 *
18 */
19
20 #include "ui_fsmenu/internet_lobby.h"
21
22 #include "base/i18n.h"
23 #include "base/log.h"
24 #include "base/random.h"
25 #include "build_info.h"
26 #include "graphic/graphic.h"
27 #include "graphic/text_layout.h"
28 #include "network/gameclient.h"
29 #include "network/gamehost.h"
30 #include "network/internet_gaming.h"
31 #include "network/internet_gaming_protocol.h"
32 #include "sound/sound_handler.h"
33 #include "ui_basic/messagebox.h"
34 #include "wlapplication_options.h"
35
36 namespace {
37
38 // Constants for convert_clienttype() / compare_clienttype()
39 const uint8_t kClientSuperuser = 0;
40 const uint8_t kClientRegistered = 1;
41 const uint8_t kClientUnregistered = 2;
42 // 3 was INTERNET_CLIENT_BOT which is not used
43 const uint8_t kClientIRC = 4;
44 } // namespace
45
FullscreenMenuInternetLobby(char const * const nick,char const * const pwd,bool registered)46 FullscreenMenuInternetLobby::FullscreenMenuInternetLobby(char const* const nick,
47 char const* const pwd,
48 bool registered)
49 : FullscreenMenuBase(),
50
51 // Values for alignment and size
52 butx_(get_w() * 13 / 40),
53 butw_(get_w() * 36 / 125),
54 buth_(get_h() * 19 / 400),
55 lisw_(get_w() * 635 / 1000),
56 prev_clientlist_len_(1000),
57 new_client_fx_(SoundHandler::register_fx(SoundType::kChat, "sound/lobby_freshmen")),
58
59 // Text labels
60 title(this,
61 get_w() / 2,
62 get_h() / 20,
63 0,
64 0,
65 _("Metaserver Lobby"),
66 UI::Align::kCenter,
67 g_gr->styles().font_style(UI::FontStyle::kFsMenuTitle)),
68 clients_(this, get_w() * 4 / 125, get_h() * 15 / 100, 0, 0, _("Clients online:")),
69 opengames_(this, get_w() * 17 / 25, get_h() * 15 / 100, 0, 0, _("Open Games:")),
70 servername_(this, get_w() * 17 / 25, get_h() * 63 / 100, 0, 0, _("Name of your server:")),
71
72 // Buttons
73 joingame_(this,
74 "join_game",
75 get_w() * 17 / 25,
76 get_h() * 55 / 100,
77 butw_,
78 buth_,
79 UI::ButtonStyle::kFsMenuSecondary,
80 _("Join this game")),
81 hostgame_(this,
82 "host_game",
83 get_w() * 17 / 25,
84 get_h() * 73 / 100,
85 butw_,
86 buth_,
87 UI::ButtonStyle::kFsMenuSecondary,
88 _("Open a new game")),
89 back_(this,
90 "back",
91 get_w() * 17 / 25,
92 get_h() * 90 / 100,
93 butw_,
94 buth_,
95 UI::ButtonStyle::kFsMenuSecondary,
96 _("Leave Lobby")),
97
98 // Edit boxes
99 edit_servername_(this, get_w() * 17 / 25, get_h() * 68 / 100, butw_, UI::PanelStyle::kFsMenu),
100
101 // List
102 clientsonline_list_(
103 this, get_w() * 4 / 125, get_h() / 5, lisw_, get_h() * 3 / 10, UI::PanelStyle::kFsMenu),
104 opengames_list_(
105 this, get_w() * 17 / 25, get_h() / 5, butw_, get_h() * 7 / 20, UI::PanelStyle::kFsMenu),
106
107 // The chat UI
108 chat(this,
109 get_w() * 4 / 125,
110 get_h() * 51 / 100,
111 lisw_,
112 get_h() * 90 / 100 - get_h() * 51 / 100 + buth_ - 1,
113 InternetGaming::ref(),
114 UI::PanelStyle::kFsMenu),
115
116 // Login information
117 nickname_(nick),
118 password_(pwd),
119 is_registered_(registered) {
120
121 joingame_.sigclicked.connect([this]() { clicked_joingame(); });
122 hostgame_.sigclicked.connect([this]() { clicked_hostgame(); });
123 back_.sigclicked.connect([this]() { clicked_back(); });
124
125 // Set the texts and style of UI elements
126 title.set_font_scale(scale_factor());
127
128 opengames_.set_font_scale(scale_factor());
129 clients_.set_font_scale(scale_factor());
130 servername_.set_font_scale(scale_factor());
131
132 std::string server = get_config_string("servername", "");
133 edit_servername_.set_font_scale(scale_factor());
134 edit_servername_.set_text(server);
135 edit_servername_.changed.connect([this]() { change_servername(); });
136
137 // Prepare the lists
138 const std::string t_tip =
139 (boost::format("<rt padding=2><p align=center spacing=3>%s</p>"
140 "<p valign=bottom><img src=images/wui/overlays/road_building_green.png> %s"
141 "<br><img src=images/wui/overlays/road_building_yellow.png> %s"
142 "<br><img src=images/wui/overlays/road_building_red.png> %s</p></rt>") %
143 g_gr->styles().font_style(UI::FontStyle::kTooltipHeader).as_font_tag(_("User Status")) %
144 g_gr->styles().font_style(UI::FontStyle::kTooltip).as_font_tag(_("Administrator")) %
145 g_gr->styles().font_style(UI::FontStyle::kTooltip).as_font_tag(_("Registered")) %
146 g_gr->styles().font_style(UI::FontStyle::kTooltip).as_font_tag(_("Unregistered")))
147 .str();
148 clientsonline_list_.add_column(22, "*", t_tip);
149 /** TRANSLATORS: Player Name */
150 clientsonline_list_.add_column((lisw_ - 22) * 3 / 8, pgettext("player", "Name"));
151 clientsonline_list_.add_column((lisw_ - 22) * 2 / 8, _("Version"));
152 clientsonline_list_.add_column(
153 (lisw_ - 22) * 3 / 8, _("Game"), "", UI::Align::kLeft, UI::TableColumnType::kFlexible);
154 clientsonline_list_.set_column_compare(
155 0, [this](uint32_t a, uint32_t b) { return compare_clienttype(a, b); });
156 clientsonline_list_.double_clicked.connect(
157 [this](uint32_t a) { return client_doubleclicked(a); });
158 opengames_list_.selected.connect([this](uint32_t) { server_selected(); });
159 opengames_list_.double_clicked.connect([this](uint32_t) { server_doubleclicked(); });
160
161 // try to connect to the metaserver
162 if (!InternetGaming::ref().error() && !InternetGaming::ref().logged_in()) {
163 connect_to_metaserver();
164 }
165
166 // set focus to chat input
167 chat.focus_edit();
168 }
169
layout()170 void FullscreenMenuInternetLobby::layout() {
171 // TODO(GunChleoc): Box layout and then implement
172 clientsonline_list_.layout();
173 }
174
175 /// think function of the UI (main loop)
think()176 void FullscreenMenuInternetLobby::think() {
177 FullscreenMenuBase::think();
178
179 if (!InternetGaming::ref().error()) {
180
181 // If we have no connection try to connect
182 if (!InternetGaming::ref().logged_in()) {
183 connect_to_metaserver();
184 }
185
186 // Check whether metaserver send some data
187 InternetGaming::ref().handle_metaserver_communication();
188 }
189
190 if (InternetGaming::ref().update_for_clients()) {
191 fill_client_list(InternetGaming::ref().clients());
192 }
193
194 if (InternetGaming::ref().update_for_games()) {
195 fill_games_list(InternetGaming::ref().games());
196 }
197 // unfocus chat window when other UI element has focus
198 if (!chat.has_focus()) {
199 chat.unfocus_edit();
200 }
201 if (edit_servername_.has_focus()) {
202 change_servername();
203 }
204 }
205
clicked_ok()206 void FullscreenMenuInternetLobby::clicked_ok() {
207 if (joingame_.enabled()) {
208 server_doubleclicked();
209 } else {
210 clicked_hostgame();
211 }
212 }
213
214 /// connects Widelands with the metaserver
connect_to_metaserver()215 void FullscreenMenuInternetLobby::connect_to_metaserver() {
216 const std::string& metaserver =
217 get_config_string("metaserver", INTERNET_GAMING_METASERVER.c_str());
218 uint32_t port = get_config_natural("metaserverport", kInternetGamingPort);
219 std::string auth = is_registered_ ? password_ : get_config_string("uuid", "");
220 assert(!auth.empty());
221 InternetGaming::ref().login(nickname_, auth, is_registered_, metaserver, port);
222 }
223
224 /// fills the server list
fill_games_list(const std::vector<InternetGame> * games)225 void FullscreenMenuInternetLobby::fill_games_list(const std::vector<InternetGame>* games) {
226 // List and button cleanup
227 opengames_list_.clear();
228 hostgame_.set_enabled(true);
229 joingame_.set_enabled(false);
230 std::string localservername = edit_servername_.text();
231 std::string localbuildid = build_id();
232
233 if (games != nullptr) { // If no communication error occurred, fill the list.
234 for (const InternetGame& game : *games) {
235 if (game.connectable == INTERNET_GAME_SETUP && game.build_id == localbuildid) {
236 // only clients with the same build number are displayed
237 opengames_list_.add(richtext_escape(game.name), game,
238 g_gr->images().get("images/ui_basic/continue.png"), false,
239 game.build_id);
240 } else if (game.connectable == INTERNET_GAME_SETUP &&
241 game.build_id.compare(0, 6, "build-") != 0 &&
242 localbuildid.compare(0, 6, "build-") != 0) {
243 // only development clients are allowed to see games openend by such
244 opengames_list_.add(richtext_escape(game.name), game,
245 g_gr->images().get("images/ui_basic/different.png"), false,
246 game.build_id);
247 }
248 }
249 }
250 }
251
convert_clienttype(const std::string & type)252 uint8_t FullscreenMenuInternetLobby::convert_clienttype(const std::string& type) {
253 if (type == INTERNET_CLIENT_REGISTERED) {
254 return kClientRegistered;
255 }
256 if (type == INTERNET_CLIENT_SUPERUSER) {
257 return kClientSuperuser;
258 }
259 if (type == INTERNET_CLIENT_IRC) {
260 return kClientIRC;
261 }
262 // if (type == INTERNET_CLIENT_UNREGISTERED)
263 return kClientUnregistered;
264 }
265
266 /**
267 * \return \c true if the client in row \p rowa should come before the client in
268 * row \p rowb when sorted according to clienttype
269 */
compare_clienttype(unsigned int rowa,unsigned int rowb)270 bool FullscreenMenuInternetLobby::compare_clienttype(unsigned int rowa, unsigned int rowb) {
271 const InternetClient* playera = clientsonline_list_[rowa];
272 const InternetClient* playerb = clientsonline_list_[rowb];
273
274 return convert_clienttype(playera->type) < convert_clienttype(playerb->type);
275 }
276
277 /// fills the client list
fill_client_list(const std::vector<InternetClient> * clients)278 void FullscreenMenuInternetLobby::fill_client_list(const std::vector<InternetClient>* clients) {
279 clientsonline_list_.clear();
280 if (clients != nullptr) { // If no communication error occurred, fill the list.
281 for (const InternetClient& client : *clients) {
282 UI::Table<const InternetClient* const>::EntryRecord& er = clientsonline_list_.add(&client);
283 er.set_string(1, client.name);
284 er.set_string(2, client.build_id);
285 er.set_string(3, client.game);
286
287 const Image* pic;
288 switch (convert_clienttype(client.type)) {
289 case kClientUnregistered:
290 pic = g_gr->images().get("images/wui/overlays/road_building_red.png");
291 er.set_picture(0, pic);
292 break;
293 case kClientRegistered:
294 pic = g_gr->images().get("images/wui/overlays/road_building_yellow.png");
295 er.set_picture(0, pic);
296 break;
297 case kClientSuperuser:
298 pic = g_gr->images().get("images/wui/overlays/road_building_green.png");
299 er.set_font_style(g_gr->styles().font_style(UI::FontStyle::kFsGameSetupSuperuser));
300 er.set_picture(0, pic);
301 break;
302 case kClientIRC:
303 // No icon for IRC users
304 er.set_font_style(g_gr->styles().font_style(UI::FontStyle::kFsGameSetupIrcClient));
305 continue;
306 default:
307 continue;
308 }
309 }
310 // If a new player joins the lobby, play a sound.
311 if (clients->size() > prev_clientlist_len_ && !InternetGaming::ref().sound_off()) {
312 g_sh->play_fx(SoundType::kChat, new_client_fx_);
313 }
314 prev_clientlist_len_ = clients->size();
315 }
316 clientsonline_list_.sort();
317 }
318
319 /// called when an entry of the client list was doubleclicked
client_doubleclicked(uint32_t i)320 void FullscreenMenuInternetLobby::client_doubleclicked(uint32_t i) {
321 // add a @clientname to the current edit text.
322 if (clientsonline_list_.has_selection()) {
323 UI::Table<const InternetClient* const>::EntryRecord& er = clientsonline_list_.get_record(i);
324
325 std::string temp("@");
326 temp += er.get_string(1);
327 std::string text(chat.get_edit_text());
328
329 if (text.size() && (text.at(0) == '@')) { // already PM ?
330 if (text.find(' ') <= text.size()) {
331 text = text.substr(text.find(' '), text.size());
332 } else {
333 text.clear();
334 }
335 } else {
336 temp += " "; // The needed space between name and text
337 }
338
339 temp += text;
340 chat.set_edit_text(temp);
341 chat.focus_edit();
342 }
343 }
344
345 /// called when an entry of the server list was selected
server_selected()346 void FullscreenMenuInternetLobby::server_selected() {
347 // remove focus from chat
348 if (opengames_list_.has_selection()) {
349 const InternetGame* game = &opengames_list_.get_selected();
350 if (game->connectable == INTERNET_GAME_SETUP) {
351 joingame_.set_enabled(true);
352 }
353 }
354 }
355
356 /// called when an entry of the server list was doubleclicked
server_doubleclicked()357 void FullscreenMenuInternetLobby::server_doubleclicked() {
358 // if the game is open try to connect it, if not do nothing.
359 if (opengames_list_.has_selection()) {
360 const InternetGame* game = &opengames_list_.get_selected();
361 if (game->connectable == INTERNET_GAME_SETUP) {
362 clicked_joingame();
363 }
364 }
365 }
366
367 /// called when the servername was changed
change_servername()368 void FullscreenMenuInternetLobby::change_servername() {
369 // Allow client to enter a servername manually
370 hostgame_.set_enabled(true);
371 edit_servername_.set_tooltip("");
372 edit_servername_.set_warning(false);
373 // Check whether a server of that name is already open.
374 // And disable 'hostgame' button if yes.
375 const std::vector<InternetGame>* games = InternetGaming::ref().games();
376 if (games != nullptr) {
377 for (const InternetGame& game : *games) {
378 if (game.name == edit_servername_.text()) {
379 hostgame_.set_enabled(false);
380 edit_servername_.set_warning(true);
381 edit_servername_.set_tooltip(
382 (boost::format(
383 _("The game %s is already running. Please choose a different name.")) %
384 g_gr->styles().font_style(UI::FontStyle::kWarning).as_font_tag(game.name))
385 .str());
386 }
387 }
388 }
389 }
390
wait_for_ip()391 bool FullscreenMenuInternetLobby::wait_for_ip() {
392 if (!InternetGaming::ref().wait_for_ips()) {
393 // Only display a message box if a network error occurred
394 if (InternetGaming::ref().error()) {
395 // Show a popup warning message
396 const std::string warning(
397 _("Widelands was unable to get the IP address of the server in time. "
398 "There seems to be a network problem, either on your side or on the side "
399 "of the server.\n"));
400 UI::WLMessageBox mmb(this, _("Connection Timed Out"), warning,
401 UI::WLMessageBox::MBoxType::kOk, UI::Align::kLeft);
402 mmb.run<UI::Panel::Returncodes>();
403 }
404 return false;
405 }
406 return true;
407 }
408
409 /// called when the 'join game' button was clicked
clicked_joingame()410 void FullscreenMenuInternetLobby::clicked_joingame() {
411 if (opengames_list_.has_selection()) {
412 InternetGaming::ref().join_game(opengames_list_.get_selected().name);
413
414 if (!wait_for_ip()) {
415 return;
416 }
417 const std::pair<NetAddress, NetAddress>& ips = InternetGaming::ref().ips();
418
419 GameClient netgame(ips, InternetGaming::ref().get_local_clientname(), true,
420 opengames_list_.get_selected().name);
421 netgame.run();
422 } else {
423 throw wexception("No server selected! That should not happen!");
424 }
425 }
426
427 /// called when the 'host game' button was clicked
clicked_hostgame()428 void FullscreenMenuInternetLobby::clicked_hostgame() {
429 // Save selected servername as default for next time and during that take care that the name is
430 // not empty.
431 std::string servername_ui = edit_servername_.text();
432
433 const std::vector<InternetGame>* games = InternetGaming::ref().games();
434 if (games != nullptr) {
435 for (const InternetGame& game : *games) {
436 if (servername_ui.empty()) {
437 uint32_t i = 1;
438 do {
439 /** TRANSLATORS: This is shown for multiplayer games when no host */
440 /** TRANSLATORS: server to connect to has been specified yet. */
441 servername_ui = (boost::format(_("unnamed %u")) % i++).str();
442 } while (servername_ui == game.name);
443 } else if (game.name == servername_ui) {
444 change_servername();
445 return;
446 }
447 }
448 if (games->empty() && servername_ui.empty()) {
449 servername_ui = _("unnamed");
450 }
451 }
452
453 set_config_string("servername", servername_ui);
454
455 // Set up the game
456 InternetGaming::ref().set_local_servername(servername_ui);
457
458 // Start the game
459 try {
460
461 // Tell the metaserver about it
462 InternetGaming::ref().open_game();
463
464 // Wait for the response with the IPs of the relay server
465 if (!wait_for_ip()) {
466 InternetGaming::ref().set_error();
467 return;
468 }
469
470 // Start our relay host
471 GameHost netgame(InternetGaming::ref().get_local_clientname(), true);
472 netgame.run();
473 } catch (...) {
474 // Log out before going back to the main menu
475 InternetGaming::ref().logout("SERVER_CRASHED");
476 throw;
477 }
478 }
479