1 /*
2    Copyright (C) 2008 - 2018 by the Battle for Wesnoth Project https://www.wesnoth.org/
3 
4    This program is free software; you can redistribute it and/or modify
5    it under the terms of the GNU General Public License as published by
6    the Free Software Foundation; either version 2 of the License, or
7    (at your option) any later version.
8    This program is distributed in the hope that it will be useful,
9    but WITHOUT ANY WARRANTY.
10 
11    See the COPYING file for more details.
12 */
13 
14 #define GETTEXT_DOMAIN "wesnoth-lib"
15 
16 #include "gui/dialogs/multiplayer/mp_join_game.hpp"
17 
18 #include "chat_log.hpp"
19 #include "font/text_formatting.hpp"
20 #include "formatter.hpp"
21 #include "formula/string_utils.hpp"
22 #include "game_config.hpp"
23 #include "game_config_manager.hpp"
24 #include "game_initialization/mp_game_utils.hpp"
25 #include "preferences/credentials.hpp"
26 #include "gettext.hpp"
27 #include "gui/auxiliary/find_widget.hpp"
28 #include "gui/core/timer.hpp"
29 #include "gui/dialogs/loading_screen.hpp"
30 #include "gui/dialogs/multiplayer/faction_select.hpp"
31 #include "gui/dialogs/transient_message.hpp"
32 #include "gui/widgets/button.hpp"
33 #include "gui/widgets/chatbox.hpp"
34 #include "gui/widgets/menu_button.hpp"
35 #include "gui/widgets/image.hpp"
36 #include "gui/widgets/label.hpp"
37 #include "gui/widgets/listbox.hpp"
38 #include "gui/widgets/settings.hpp"
39 #include "gui/widgets/tree_view.hpp"
40 #include "gui/widgets/tree_view_node.hpp"
41 #include "log.hpp"
42 #include "mp_ui_alerts.hpp"
43 #include "statistics.hpp"
44 #include "units/types.hpp"
45 #include "utils/scope_exit.hpp"
46 #include "wesnothd_connection.hpp"
47 
48 static lg::log_domain log_mp_connect_engine("mp/connect/engine");
49 #define DBG_MP LOG_STREAM(debug, log_mp_connect_engine)
50 #define LOG_MP LOG_STREAM(info, log_mp_connect_engine)
51 #define WRN_MP LOG_STREAM(warn, log_mp_connect_engine)
52 #define ERR_MP LOG_STREAM(err, log_mp_connect_engine)
53 
54 namespace gui2
55 {
56 namespace dialogs
57 {
58 
REGISTER_DIALOG(mp_join_game)59 REGISTER_DIALOG(mp_join_game)
60 
61 mp_join_game::mp_join_game(saved_game& state, mp::lobby_info& lobby_info, wesnothd_connection& connection, const bool first_scenario, const bool observe_game)
62 	: level_()
63 	, state_(state)
64 	, lobby_info_(lobby_info)
65 	, network_connection_(connection)
66 	, update_timer_(0)
67 	, first_scenario_(first_scenario)
68 	, observe_game_(observe_game)
69 	, stop_updates_(false)
70 	, player_list_(nullptr)
71 	, flg_dialog_(nullptr)
72 {
73 	set_show_even_without_video(true);
74 }
75 
~mp_join_game()76 mp_join_game::~mp_join_game()
77 {
78 	if(update_timer_ != 0) {
79 		remove_timer(update_timer_);
80 		update_timer_ = 0;
81 	}
82 }
83 
84 /*
85  * Fetch the selected game's config from the server and prompts an initial faction selection.
86  */
fetch_game_config()87 bool mp_join_game::fetch_game_config()
88 {
89 	// Ask for the next scenario data, if applicable
90 	if(!first_scenario_) {
91 		network_connection_.send_data(config("load_next_scenario"));
92 	}
93 
94 	bool has_scenario_and_controllers = false;
95 	while(!has_scenario_and_controllers) {
96 		config revc;
97 		const bool data_res =
98 			network_connection_.fetch_data_with_loading_screen(revc, loading_stage::download_level_data);
99 
100 		if(!data_res) {
101 			return false;
102 		}
103 
104 		mp::check_response(data_res, revc);
105 
106 		if(revc.child("leave_game")) {
107 			return false;
108 		} else if(config& next_scenario = revc.child("next_scenario")) {
109 			level_.swap(next_scenario);
110 		} else if(revc.has_attribute("version")) {
111 			level_.swap(revc);
112 
113 			has_scenario_and_controllers = true;
114 		} else if(config& controllers = revc.child("controllers")) {
115 			int index = 0;
116 			for(const config& controller : controllers.child_range("controller")) {
117 				if(config& side = get_scenario().child("side", index)) {
118 					side["is_local"] = controller["is_local"];
119 				}
120 				++index;
121 			}
122 
123 			has_scenario_and_controllers = true;
124 		}
125 	}
126 
127 	if(level_["started"].to_bool()) {
128 		mp::level_to_gamestate(level_, state_);
129 		return true;
130 	}
131 
132 	if(first_scenario_) {
133 		state_.clear();
134 		state_.classification() = game_classification(level_);
135 
136 		// Make sure that we have the same config as host, if possible.
137 		game_config_manager::get()->load_game_config_for_game(state_.classification());
138 	}
139 
140 	game_config::add_color_info(get_scenario());
141 
142 	// If we're just an observer, we don't need to find an appropriate side and set faction selection
143 	if(observe_game_) {
144 		return true;
145 	}
146 
147 	// Search for an appropriate vacant slot. If a description is set (i.e. we're loading from a saved game),
148 	// then prefer to get the side with the same description as our login. Otherwise just choose the first
149 	// available side.
150 	const config* side_choice = nullptr;
151 
152 	int side_num_choice = 1, side_num_counter = 1;
153 	for(const config& side : get_scenario().child_range("side")) {
154 		// TODO: it can happen that the scenario specifies that the controller
155 		//       of a side should also gain control of another side.
156 		if(side["controller"] == "reserved" && side["current_player"] == preferences::login()) {
157 			side_choice = &side;
158 			side_num_choice = side_num_counter;
159 			break;
160 		}
161 
162 		if(side["controller"] == "human" && side["player_id"].empty()) {
163 			if(!side_choice) { // Found the first empty side
164 				side_choice = &side;
165 				side_num_choice = side_num_counter;
166 			}
167 
168 			if(side["current_player"] == preferences::login()) {
169 				side_choice = &side;
170 				side_num_choice = side_num_counter;
171 				break;  // Found the preferred one
172 			}
173 		}
174 
175 		if(side["player_id"] == preferences::login()) {
176 			// We already own a side in this game
177 			return true;
178 		}
179 
180 		++side_num_counter;
181 	}
182 
183 	if(!side_choice) {
184 		observe_game_ = true;
185 		return true;
186 	}
187 
188 	// If the client is allowed to choose their team, do that here instead of having it set by the server
189 	if((*side_choice)["allow_changes"].to_bool(true)) {
190 		if(!show_flg_select(side_num_choice, true)) {
191 			return false;
192 		}
193 	}
194 
195 	return true;
196 }
197 
generate_user_description(const config & side)198 static std::string generate_user_description(const config& side)
199 {
200 	// Allow the host to override, since only the host knows the ai_algorithm.
201 	if(const config::attribute_value* desc = side.get("user_description")) {
202 		return desc->str();
203 	}
204 
205 	const std::string controller_type = side["controller"].str();
206 	const std::string reservation = side["current_player"].str();
207 	const std::string owner = side["player_id"].str();
208 
209 	if(controller_type == "ai") {
210 		return _("Computer Player");
211 	} else if(controller_type == "null") {
212 		return _("Empty slot");
213 	} else if(controller_type == "reserved") {
214 		return VGETTEXT("Reserved for $playername", {{"playername", reservation}});
215 	} else if(owner.empty()) {
216 		return _("Vacant slot");
217 	} else if(controller_type == "human" || controller_type == "network") {
218 		return owner;
219 	} else {
220 		return _("empty");
221 	}
222 }
223 
pre_show(window & window)224 void mp_join_game::pre_show(window& window)
225 {
226 	window.set_enter_disabled(true);
227 	window.set_escape_disabled(true);
228 
229 	//
230 	// Set title
231 	//
232 	label& title = find_widget<label>(&window, "title", false);
233 	title.set_label((formatter() << title.get_label() << " " << font::unicode_em_dash << " " << get_scenario()["name"].t_str()).str());
234 
235 	//
236 	// Set up sides list
237 	//
238 	generate_side_list(window);
239 
240 	//
241 	// Initialize chatbox and game rooms
242 	//
243 	chatbox& chat = find_widget<chatbox>(&window, "chat", false);
244 
245 	chat.set_lobby_info(lobby_info_);
246 	chat.set_wesnothd_connection(network_connection_);
247 
248 	chat.room_window_open(N_("this game"), true, false);
249 	chat.active_window_changed();
250 	chat.load_log(default_chat_log, false);
251 
252 	//
253 	// Set up player list
254 	//
255 	player_list_.reset(new player_list_helper(&window));
256 
257 	//
258 	// Set up the network handling
259 	//
260 	update_timer_ = add_timer(game_config::lobby_network_timer, std::bind(&mp_join_game::network_handler, this, std::ref(window)), true);
261 
262 	//
263 	// Set up the Lua plugin context
264 	//
265 	plugins_context_.reset(new plugins_context("Multiplayer Join"));
266 
267 	plugins_context_->set_callback("launch", [&window](const config&) { window.set_retval(retval::OK); }, false);
268 	plugins_context_->set_callback("quit",   [&window](const config&) { window.set_retval(retval::CANCEL); }, false);
269 	plugins_context_->set_callback("chat",   [&chat](const config& cfg) { chat.send_chat_message(cfg["message"], false); }, true);
270 }
271 
show_flg_select(int side_num,bool first_time)272 bool mp_join_game::show_flg_select(int side_num, bool first_time)
273 {
274 	if(const config& side_choice = get_scenario().child("side", side_num - 1)) {
275 		if(!side_choice["allow_changes"].to_bool(true)) {
276 			return true;
277 		}
278 
279 		const config& era = level_.child("era");
280 		if(!era) {
281 			ERR_MP << "no era information\n";
282 			return false;
283 		}
284 
285 		config::const_child_itors possible_sides = era.child_range("multiplayer_side");
286 		if(possible_sides.empty()) {
287 			WRN_MP << "no [multiplayer_side] found in era '" << era["id"] << "'.\n";
288 			return false;
289 		}
290 
291 		const std::string color = side_choice["color"].str();
292 
293 		std::vector<const config*> era_factions;
294 		//make this safe against changes to level_ that might make possible_sides invalid pointers.
295 		config era_copy;
296 		for(const config& side : possible_sides) {
297 			config& side_new = era_copy.add_child("multiplayer_side", side);
298 			era_factions.push_back(&side_new);
299 		}
300 
301 		const bool is_mp = state_.classification().is_normal_mp_game();
302 		const bool lock_settings = get_scenario()["force_lock_settings"].to_bool(!is_mp);
303 		const bool use_map_settings = level_.child("multiplayer")["mp_use_map_settings"].to_bool();
304 		const bool saved_game = level_.child("multiplayer")["savegame"].to_bool();
305 
306 		ng::flg_manager flg(era_factions, side_choice, lock_settings, use_map_settings, saved_game);
307 
308 		{
309 			gui2::dialogs::faction_select flg_dialog(flg, color, side_num);
310 			flg_dialog_ = &flg_dialog;
311 			utils::scope_exit se([this]() { flg_dialog_ = nullptr; });
312 
313 			if(!flg_dialog.show() && !first_time) {
314 				return true;
315 			}
316 		}
317 
318 		config faction;
319 		config& change = faction.add_child("change_faction");
320 		change["change_faction"] = true;
321 		change["name"] = preferences::login();
322 		change["faction"] = flg.current_faction()["id"];
323 		change["leader"] = flg.current_leader();
324 		change["gender"] = flg.current_gender();
325 		// TODO: the host cannot yet handle this and always uses the first side owned by that player.
326 		change["side_num"] = side_num;
327 
328 		network_connection_.send_data(faction);
329 	}
330 
331 	return true;
332 }
333 
generate_side_list(window & window)334 void mp_join_game::generate_side_list(window& window)
335 {
336 	if(stop_updates_) {
337 		return;
338 	}
339 
340 	tree_view& tree = find_widget<tree_view>(&window, "side_list", false);
341 
342 	window.keyboard_capture(&tree);
343 
344 	tree.clear();
345 	team_tree_map_.clear();
346 	const std::map<std::string, string_map> empty_map;
347 
348 	int side_num = 0;
349 	for(const auto& side : get_scenario().child_range("side")) {
350 		++side_num;
351 		if(!side["allow_player"].to_bool(true)) {
352 			continue;
353 		}
354 
355 		// Check to see whether we've added a toplevel tree node for this team. If not, add one
356 		if(team_tree_map_.find(side["team_name"].str()) == team_tree_map_.end()) {
357 			std::map<std::string, string_map> data;
358 			string_map item;
359 
360 			item["label"] = (formatter() << _("Team:") << " " << t_string::from_serialized(side["user_team_name"])).str();
361 			data.emplace("tree_view_node_label", item);
362 
363 			tree_view_node& team_node = tree.add_node("team_header", data);
364 			team_node.add_sibling("side_spacer", empty_map);
365 
366 			team_tree_map_[side["team_name"].str()] = &team_node;
367 		}
368 
369 		std::map<std::string, string_map> data;
370 		string_map item;
371 
372 		const std::string color = !side["color"].empty() ? side["color"] : side["side"].str();
373 
374 		item["label"] = (formatter() << "<span color='" << font::get_pango_color_from_id(color) << "'>" << side["side"] << "</span>").str();
375 		data.emplace("side_number", item);
376 
377 		std::string leader_image = ng::random_enemy_picture;
378 		std::string leader_type = side["type"];
379 		std::string leader_gender = side["gender"];
380 		std::string leader_name;
381 
382 		// If there is a unit which can recruit, use it as a leader.
383 		// Necessary to display leader information when loading saves.
384 		for(const config& side_unit : side.child_range("unit")) {
385 			if(side_unit["canrecruit"].to_bool()) {
386 				leader_type = side_unit["type"].str();
387 				leader_gender = side_unit["gender"].str();
388 				break;
389 			}
390 		}
391 
392 		if(const unit_type* ut = unit_types.find(leader_type)) {
393 			const unit_type& type = ut->get_gender_unit_type(leader_gender);
394 
395 			leader_image = formatter() << type.image() << "~RC(" << type.flag_rgb() << ">" << color << ")";
396 			leader_name = type.type_name();
397 		}
398 
399 		item["label"] = leader_image;
400 		data.emplace("leader_image", item);
401 
402 		std::string description = generate_user_description(side);
403 		if(!leader_name.empty()) {
404 			description += formatter() << " (<i>" << leader_name << "</i>)";
405 		}
406 
407 		item["label"] = description;
408 		data.emplace("leader_type", item);
409 
410 		item["label"] = (formatter() << "<span color='#a69275'>" << side["faction_name"] << "</span>").str();
411 		data.emplace("leader_faction", item);
412 
413 		std::string gender_icon = "icons/icon-random.png";
414 		if(leader_gender != "null") {
415 			gender_icon = formatter() << "icons/icon-" << leader_gender << ".png";
416 			item["tooltip"] = leader_gender;
417 		}
418 
419 		item["label"] = gender_icon;
420 		data.emplace("leader_gender", item);
421 
422 		item.clear();
423 
424 		// Don't show gold for saved games
425 		// TODO: gold icon
426 		if(side["allow_changes"].to_bool()) {
427 			item["label"] = side["gold"].str() + " " + _("Gold");
428 			data.emplace("side_gold", item);
429 		}
430 
431 		const int income_amt = side["income"];
432 		if(income_amt != 0) {
433 			const std::string income_string = formatter() << (income_amt > 0 ? "+" : "") << income_amt << " " << _("Income");
434 
435 			item["label"] = income_string;
436 			data.emplace("side_income", item);
437 		}
438 
439 		tree_view_node& node = team_tree_map_[side["team_name"].str()]->add_child("side_panel", data);
440 
441 		grid& row_grid = node.get_grid();
442 
443 		auto* select_leader_button = find_widget<button>(&row_grid, "select_leader", false, false);
444 		if(select_leader_button) {
445 			if(side["player_id"] == preferences::login() && side["allow_changes"].to_bool(true)) {
446 				//
447 				// Small wrapper function in order to set the handled and halt parameters and prevent
448 				// crashes in case the dialog closes and the original button to which the callback was
449 				// bound had already been destroyed. The other use of show_flg_select doesn't need these
450 				// parameters, so it's easier not to declare them as function arguments.
451 				//
452 				const auto handler = [this, side_num](bool& handled, bool& halt) {
453 					show_flg_select(side_num);
454 					// note: this function is called from a std::function object stored in the widget
455 					//       and show_flg_select which internally calls
456 					//       show_dialog -> pump -> ... -> network_handler -> ... -> generate_side_list
457 					//       might destroy that std::function object while it is executed, this means that
458 					//       using the captured variables 'this' and 'side_num' after this will result in
459 					//       unexpected behaviour or crashes.
460 					handled = halt = true;
461 				};
462 
463 				connect_signal_mouse_left_click(*select_leader_button, std::bind(handler, _3, _4));
464 			} else {
465 				select_leader_button->set_visible(widget::visibility::hidden);
466 			}
467 		}
468 
469 		if(income_amt == 0) {
470 			find_widget<image>(&row_grid, "income_icon", false).set_visible(widget::visibility::invisible);
471 			find_widget<label>(&row_grid, "side_income", false).set_visible(widget::visibility::invisible);
472 		}
473 	}
474 }
475 
close_faction_select_dialog_if_open()476 void mp_join_game::close_faction_select_dialog_if_open()
477 {
478 	if(flg_dialog_) {
479 		if(window* w = flg_dialog_->get_window()) {
480 			w->set_retval(retval::CANCEL);
481 		}
482 	}
483 }
484 
network_handler(window & window)485 void mp_join_game::network_handler(window& window)
486 {
487 	// If the game has already started, close the dialog immediately.
488 	if(level_["started"].to_bool()) {
489 		window.set_retval(retval::OK);
490 		return;
491 	}
492 
493 	config data;
494 	if(!network_connection_.receive_data(data)) {
495 		return;
496 	}
497 
498 	// Update chat
499 	find_widget<chatbox>(&window, "chat", false).process_network_data(data);
500 
501 	if(!data["message"].empty()) {
502 		gui2::show_transient_message(_("Response") , data["message"]);
503 	}
504 
505 	if(data["failed"].to_bool()) {
506 		close_faction_select_dialog_if_open();
507 
508 		window.set_retval(retval::CANCEL);
509 	} else if(data.child("start_game")) {
510 		close_faction_select_dialog_if_open();
511 
512 		level_["started"] = true;
513 		window.set_retval(retval::OK);
514 	} else if(data.child("leave_game")) {
515 		close_faction_select_dialog_if_open();
516 
517 		window.set_retval(retval::CANCEL);
518 	}
519 
520 	if(data.child("stop_updates")) {
521 		stop_updates_ = true;
522 	} else if(const config& c = data.child("scenario_diff")) {
523 		// TODO: We should catch config::error and then leave the game.
524 		level_.apply_diff(c);
525 
526 		generate_side_list(window);
527 	} else if(const config& change = data.child("change_controller")) {
528 		if(config& side_to_change = get_scenario().find_child("side", "side", change["side"])) {
529 			side_to_change.merge_with(change);
530 		}
531 
532 		if(flg_dialog_ && flg_dialog_->get_side_num() == change["side"].to_int()) {
533 			close_faction_select_dialog_if_open();
534 		}
535 	} else if(data.has_child("scenario") || data.has_child("snapshot") || data.child("next_scenario")) {
536 		level_ = first_scenario_ ? data : data.child("next_scenario");
537 
538 		generate_side_list(window);
539 	}
540 
541 	if(data.has_child("turn")) {
542 		ERR_MP << "received replay data\n" << data << "\n in mp join\n";
543 	}
544 
545 	// Update player list
546 	if(data.has_child("user")) {
547 		player_list_->update_list(data.child_range("user"));
548 	}
549 }
550 
get_scenario()551 config& mp_join_game::get_scenario()
552 {
553 	if(config& scenario = level_.child("scenario")) {
554 		return scenario;
555 	} else if(config& snapshot = level_.child("snapshot")) {
556 		return snapshot;
557 	}
558 
559 	return level_;
560 }
561 
post_show(window & window)562 void mp_join_game::post_show(window& window)
563 {
564 	if(update_timer_ != 0) {
565 		remove_timer(update_timer_);
566 		update_timer_ = 0;
567 	}
568 
569 	if(window.get_retval() == retval::OK) {
570 		if(const config& stats = level_.child("statistics")) {
571 			statistics::fresh_stats();
572 			statistics::read_stats(stats);
573 		}
574 
575 		mp::level_to_gamestate(level_, state_);
576 
577 		mp_ui_alerts::game_has_begun();
578 	} else if(observe_game_) {
579 		network_connection_.send_data(config("observer_quit", config { "name", preferences::login() }));
580 	} else {
581 		network_connection_.send_data(config("leave_game"));
582 	}
583 }
584 
585 } // namespace dialogs
586 } // namespace gui2
587