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