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