1 /*
2    Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
3    Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4 
5    This program is free software; you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation; either version 2 of the License, or
8    (at your option) any later version.
9    This program is distributed in the hope that it will be useful,
10    but WITHOUT ANY WARRANTY.
11 
12    See the COPYING file for more details.
13 */
14 
15 #include "playturn.hpp"
16 
17 #include "actions/undo.hpp"             // for undo_list
18 #include "chat_events.hpp"              // for chat_handler, etc
19 #include "config.hpp"                   // for config, etc
20 #include "display_chat_manager.hpp"	// for add_chat_message, add_observer, etc
21 #include "formula/string_utils.hpp"     // for VGETTEXT
22 #include "game_board.hpp"               // for game_board
23 #include "game_display.hpp"             // for game_display
24 #include "game_end_exceptions.hpp"      // for end_level_exception, etc
25 #include "gettext.hpp"                  // for _
26 #include "gui/dialogs/simple_item_selector.hpp"
27 #include "log.hpp"                      // for LOG_STREAM, logger, etc
28 #include "utils/make_enum.hpp"                // for bad_enum_cast
29 #include "map/label.hpp"
30 #include "play_controller.hpp"          // for play_controller
31 #include "playturn_network_adapter.hpp"  // for playturn_network_adapter
32 #include "preferences/general.hpp"              // for message_bell
33 #include "replay.hpp"                   // for replay, recorder, do_replay, etc
34 #include "resources.hpp"                // for gameboard, screen, etc
35 #include "serialization/string_utils.hpp"  // for string_map
36 #include "synced_context.hpp"
37 #include "team.hpp"                     // for team, team::CONTROLLER::AI, etc
38 #include "wesnothd_connection_error.hpp"
39 #include "whiteboard/manager.hpp"       // for manager
40 #include "widgets/button.hpp"           // for button
41 
42 #include <cassert>                      // for assert
43 #include <ctime>                        // for time
44 #include <ostream>                      // for operator<<, basic_ostream, etc
45 #include <vector>                       // for vector
46 
47 static lg::log_domain log_network("network");
48 #define ERR_NW LOG_STREAM(err, log_network)
49 
turn_info(replay_network_sender & replay_sender,playturn_network_adapter & network_reader)50 turn_info::turn_info(replay_network_sender &replay_sender,playturn_network_adapter &network_reader) :
51 	replay_sender_(replay_sender),
52 	host_transfer_("host_transfer"),
53 	network_reader_(network_reader)
54 {
55 }
56 
~turn_info()57 turn_info::~turn_info()
58 {
59 }
60 
sync_network()61 turn_info::PROCESS_DATA_RESULT turn_info::sync_network()
62 {
63 	//there should be nothing left on the replay and we should get turn_info::PROCESS_CONTINUE back.
64 	turn_info::PROCESS_DATA_RESULT retv = replay_to_process_data_result(do_replay());
65 	if(resources::controller->is_networked_mp()) {
66 
67 		//receive data first, and then send data. When we sent the end of
68 		//the AI's turn, we don't want there to be any chance where we
69 		//could get data back pertaining to the next turn.
70 		config cfg;
71 		while( (retv == turn_info::PROCESS_CONTINUE) &&  network_reader_.read(cfg)) {
72 			retv = process_network_data(cfg);
73 			cfg.clear();
74 		}
75 		send_data();
76 	}
77 	return retv;
78 }
79 
send_data()80 void turn_info::send_data()
81 {
82 	const bool send_everything = synced_context::is_unsynced() ? !resources::undo_stack->can_undo() : synced_context::is_simultaneously();
83 	if ( !send_everything ) {
84 		replay_sender_.sync_non_undoable();
85 	} else {
86 		replay_sender_.commit_and_sync();
87 	}
88 }
89 
handle_turn(const config & t,bool chat_only)90 turn_info::PROCESS_DATA_RESULT turn_info::handle_turn(const config& t, bool chat_only)
91 {
92 	//t can contain a [command] or a [upload_log]
93 	assert(t.all_children_count() == 1);
94 
95 	if(!t.child_or_empty("command").has_child("speak") && chat_only) {
96 		return PROCESS_CANNOT_HANDLE;
97 	}
98 	/** @todo FIXME: Check what commands we execute when it's our turn! */
99 
100 	//note, that this function might call itself recursively: do_replay -> ... -> get_user_choice -> ... -> playmp_controller::pull_remote_choice -> sync_network -> handle_turn
101 	resources::recorder->add_config(t, replay::MARK_AS_SENT);
102 	PROCESS_DATA_RESULT retv = replay_to_process_data_result(do_replay());
103 	return retv;
104 }
105 
do_save()106 void turn_info::do_save()
107 {
108 	if (resources::controller != nullptr) {
109 		resources::controller->do_autosave();
110 	}
111 }
112 
process_network_data_from_reader()113 turn_info::PROCESS_DATA_RESULT turn_info::process_network_data_from_reader()
114 {
115 	config cfg;
116 	while(this->network_reader_.read(cfg))
117 	{
118 		PROCESS_DATA_RESULT res = process_network_data(cfg);
119 		if(res != PROCESS_CONTINUE)
120 		{
121 			return res;
122 		}
123 		cfg.clear();
124 	}
125 	return PROCESS_CONTINUE;
126 }
127 
process_network_data(const config & cfg,bool chat_only)128 turn_info::PROCESS_DATA_RESULT turn_info::process_network_data(const config& cfg, bool chat_only)
129 {
130 	// the simple wesnothserver implementation in wesnoth was removed years ago.
131 	assert(cfg.all_children_count() == 1);
132 	assert(cfg.attribute_range().empty());
133 	if(!resources::recorder->at_end())
134 	{
135 		ERR_NW << "processing network data while still having data on the replay." << std::endl;
136 	}
137 
138 	if (const config &message = cfg.child("message"))
139 	{
140 		game_display::get_singleton()->get_chat_manager().add_chat_message(time(nullptr), message["sender"], message["side"],
141 				message["message"], events::chat_handler::MESSAGE_PUBLIC,
142 				preferences::message_bell());
143 	}
144 	else if (const config &whisper = cfg.child("whisper") /*&& is_observer()*/)
145 	{
146 		game_display::get_singleton()->get_chat_manager().add_chat_message(time(nullptr), "whisper: " + whisper["sender"].str(), 0,
147 				whisper["message"], events::chat_handler::MESSAGE_PRIVATE,
148 				preferences::message_bell());
149 	}
150 	else if (const config &observer = cfg.child("observer") )
151 	{
152 		game_display::get_singleton()->get_chat_manager().add_observer(observer["name"]);
153 	}
154 	else if (const config &observer_quit = cfg.child("observer_quit"))
155 	{
156 		game_display::get_singleton()->get_chat_manager().remove_observer(observer_quit["name"]);
157 	}
158 	else if (cfg.child("leave_game")) {
159 		const bool has_reason = cfg.child("leave_game").has_attribute("reason");
160 		throw leavegame_wesnothd_error(has_reason ? cfg.child("leave_game")["reason"].str() : "");
161 	}
162 	else if (const config &turn = cfg.child("turn"))
163 	{
164 		return handle_turn(turn, chat_only);
165 	}
166 	else if (cfg.has_child("whiteboard"))
167 	{
168 		set_scontext_unsynced scontext;
169 		resources::whiteboard->process_network_data(cfg);
170 	}
171 	else if (const config &change = cfg.child("change_controller"))
172 	{
173 		if(change.empty()) {
174 			ERR_NW << "Bad [change_controller] signal from server, [change_controller] tag was empty." << std::endl;
175 			return PROCESS_CONTINUE;
176 		}
177 
178 		const int side = change["side"].to_int();
179 		const bool is_local = change["is_local"].to_bool();
180 		const std::string player = change["player"];
181 		const size_t index = side - 1;
182 		if(index >= resources::gameboard->teams().size()) {
183 			ERR_NW << "Bad [change_controller] signal from server, side out of bounds: " << change.debug() << std::endl;
184 			return PROCESS_CONTINUE;
185 		}
186 
187 		const team & tm = resources::gameboard->teams().at(index);
188 		const bool was_local = tm.is_local();
189 
190 		resources::gameboard->side_change_controller(side, is_local, player);
191 
192 		if (!was_local && tm.is_local()) {
193 			resources::controller->on_not_observer();
194 		}
195 
196 		auto disp_set_team = [](int side_index) {
197 			const bool side_changed = static_cast<int>(display::get_singleton()->viewing_team()) != side_index;
198 			display::get_singleton()->set_team(side_index);
199 
200 			if(side_changed) {
201 				display::get_singleton()->redraw_everything();
202 				display::get_singleton()->recalculate_minimap();
203 				video2::trigger_full_redraw();
204 			}
205 		};
206 
207 		if (resources::gameboard->is_observer() || (resources::gameboard->teams())[display::get_singleton()->playing_team()].is_local_human()) {
208 			disp_set_team(display::get_singleton()->playing_team());
209 		} else if (tm.is_local_human()) {
210 			disp_set_team(side - 1);
211 		}
212 
213 		resources::whiteboard->on_change_controller(side,tm);
214 
215 		display::get_singleton()->labels().recalculate_labels();
216 
217 		const bool restart = game_display::get_singleton()->playing_side() == side && (was_local || tm.is_local());
218 		return restart ? PROCESS_RESTART_TURN : PROCESS_CONTINUE;
219 	}
220 
221 	else if (const config &side_drop_c = cfg.child("side_drop"))
222 	{
223 		const int  side_drop = side_drop_c["side_num"].to_int(0);
224 		size_t index = side_drop -1;
225 
226 		bool restart = side_drop == game_display::get_singleton()->playing_side();
227 
228 		if (index >= resources::gameboard->teams().size()) {
229 			ERR_NW << "unknown side " << side_drop << " is dropping game" << std::endl;
230 			throw ingame_wesnothd_error("");
231 		}
232 
233 		team::CONTROLLER ctrl;
234 		if(!ctrl.parse(side_drop_c["controller"])) {
235 			ERR_NW << "unknown controller type issued from server on side drop: " << side_drop_c["controller"] << std::endl;
236 			throw ingame_wesnothd_error("");
237 		}
238 
239 		if (ctrl == team::CONTROLLER::AI) {
240 			resources::gameboard->side_drop_to(side_drop, ctrl);
241 			return restart ? PROCESS_RESTART_TURN:PROCESS_CONTINUE;
242 		}
243 		//null controlled side cannot be dropped because they aren't controlled by anyone.
244 		else if (ctrl != team::CONTROLLER::HUMAN) {
245 			ERR_NW << "unknown controller type issued from server on side drop: " << ctrl.to_cstring() << std::endl;
246 			throw ingame_wesnothd_error("");
247 		}
248 
249 		int action = 0;
250 		int first_observer_option_idx = 0;
251 		int control_change_options = 0;
252 		bool has_next_scenario = !resources::gamedata->next_scenario().empty() && resources::gamedata->next_scenario() != "null";
253 
254 		std::vector<std::string> observers;
255 		std::vector<const team *> allies;
256 		std::vector<std::string> options;
257 
258 		const team &tm = resources::gameboard->teams()[index];
259 
260 		for (const team &t : resources::gameboard->teams()) {
261 			if (!t.is_enemy(side_drop) && !t.is_local_human() && !t.is_local_ai() && !t.is_network_ai() && !t.is_empty()
262 				&& t.current_player() != tm.current_player()) {
263 				allies.push_back(&t);
264 			}
265 		}
266 
267 		// We want to give host chance to decide what to do for side
268 		if (!resources::controller->is_linger_mode() || has_next_scenario) {
269 			utils::string_map t_vars;
270 
271 			//get all allies in as options to transfer control
272 			for (const team *t : allies) {
273 				//if this is an ally of the dropping side and it is not us (choose local player
274 				//if you want that) and not ai or empty and if it is not the dropping side itself,
275 				//get this team in as well
276 				t_vars["player"] = t->current_player();
277 				options.emplace_back(VGETTEXT("Give control to their ally $player", t_vars));
278 				control_change_options++;
279 			}
280 
281 			first_observer_option_idx = options.size();
282 
283 			//get all observers in as options to transfer control
284 			for (const std::string &screen_observers : game_display::get_singleton()->observers()) {
285 				t_vars["player"] = screen_observers;
286 				options.emplace_back(VGETTEXT("Give control to observer $player", t_vars));
287 				observers.push_back(screen_observers);
288 				control_change_options++;
289 			}
290 
291 			options.emplace_back(_("Replace with AI"));
292 			options.emplace_back(_("Replace with local player"));
293 			options.emplace_back(_("Set side to idle"));
294 			options.emplace_back(_("Save and abort game"));
295 
296 			t_vars["player"] = tm.current_player();
297 			t_vars["side_drop"] = std::to_string(side_drop);
298 			const std::string gettext_message =  VGETTEXT("$player who controlled side $side_drop has left the game. What do you want to do?", t_vars);
299 			gui2::dialogs::simple_item_selector dlg("", gettext_message, options);
300 			dlg.set_single_button(true);
301 			dlg.show();
302 			action = dlg.selected_index();
303 
304 			// If esc was pressed, default to setting side to idle
305 			if (action == -1) {
306 				action = control_change_options + 2;
307 			}
308 		} else {
309 			// Always set leaving side to idle if in linger mode and there is no next scenario
310 			action = 2;
311 		}
312 
313 		if (action < control_change_options) {
314 			// Grant control to selected ally
315 
316 			{
317 				// Server thinks this side is ours now so in case of error transferring side we have to make local state to same as what server thinks it is.
318 				resources::gameboard->side_drop_to(side_drop, team::CONTROLLER::HUMAN, team::PROXY_CONTROLLER::PROXY_IDLE);
319 			}
320 
321 			if (action < first_observer_option_idx) {
322 				change_side_controller(side_drop, allies[action]->current_player());
323 			} else {
324 				change_side_controller(side_drop, observers[action - first_observer_option_idx]);
325 			}
326 
327 			return restart ? PROCESS_RESTART_TURN : PROCESS_CONTINUE;
328 		} else {
329 			action -= control_change_options;
330 
331 			//make the player an AI, and redo this turn, in case
332 			//it was the current player's team who has just changed into
333 			//an AI.
334 			switch(action) {
335 				case 0:
336 					resources::controller->on_not_observer();
337 					resources::gameboard->side_drop_to(side_drop, team::CONTROLLER::HUMAN, team::PROXY_CONTROLLER::PROXY_AI);
338 
339 					return restart?PROCESS_RESTART_TURN:PROCESS_CONTINUE;
340 
341 				case 1:
342 					resources::controller->on_not_observer();
343 					resources::gameboard->side_drop_to(side_drop, team::CONTROLLER::HUMAN, team::PROXY_CONTROLLER::PROXY_HUMAN);
344 
345 					return restart?PROCESS_RESTART_TURN:PROCESS_CONTINUE;
346 				case 2:
347 					resources::gameboard->side_drop_to(side_drop, team::CONTROLLER::HUMAN, team::PROXY_CONTROLLER::PROXY_IDLE);
348 
349 					return restart?PROCESS_RESTART_TURN:PROCESS_CONTINUE;
350 
351 				case 3:
352 					//The user pressed "end game". Don't throw a network error here or he will get
353 					//thrown back to the title screen.
354 					do_save();
355 					throw_quit_game_exception();
356 				default:
357 					break;
358 			}
359 		}
360 	}
361 
362 	// The host has ended linger mode in a campaign -> enable the "End scenario" button
363 	// and tell we did get the notification.
364 	else if (cfg.child("notify_next_scenario")) {
365 		if(chat_only) {
366 			return PROCESS_CANNOT_HANDLE;
367 		}
368 		std::shared_ptr<gui::button> btn_end = display::get_singleton()->find_action_button("button-endturn");
369 		if(btn_end) {
370 			btn_end->enable(true);
371 		}
372 		return PROCESS_END_LINGER;
373 	}
374 
375 	//If this client becomes the new host, notify the play_controller object about it
376 	else if (cfg.child("host_transfer")){
377 		host_transfer_.notify_observers();
378 	}
379 	else
380 	{
381 		ERR_NW << "found unknown command:\n" << cfg.debug() << std::endl;
382 	}
383 
384 	return PROCESS_CONTINUE;
385 }
386 
387 
change_side_controller(int side,const std::string & player)388 void turn_info::change_side_controller(int side, const std::string& player)
389 {
390 	config cfg;
391 	config& change = cfg.add_child("change_controller");
392 	change["side"] = side;
393 	change["player"] = player;
394 	resources::controller->send_to_wesnothd(cfg);
395 }
396 
replay_to_process_data_result(REPLAY_RETURN replayreturn)397 turn_info::PROCESS_DATA_RESULT turn_info::replay_to_process_data_result(REPLAY_RETURN replayreturn)
398 {
399 	switch(replayreturn)
400 	{
401 	case REPLAY_RETURN_AT_END:
402 		return PROCESS_CONTINUE;
403 	case REPLAY_FOUND_DEPENDENT:
404 		return PROCESS_FOUND_DEPENDENT;
405 	case REPLAY_FOUND_END_TURN:
406 		return PROCESS_END_TURN;
407 	case REPLAY_FOUND_END_LEVEL:
408 		return PROCESS_END_LEVEL;
409 	default:
410 		assert(false);
411 		throw "found invalid REPLAY_RETURN";
412 	}
413 }
414