1 /*
2 Copyright (C) 2006 - 2018 by Joerg Hinrichs <joerg.hinrichs@alice-dsl.de>
3 wesnoth playlevel Copyright (C) 2003 by David White <dave@whitevine.net>
4 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY.
12
13 See the COPYING file for more details.
14 */
15
16 /**
17 * @file
18 * Logic for single-player game.
19 */
20
21 #include "playsingle_controller.hpp"
22
23 #include "actions/undo.hpp"
24 #include "ai/manager.hpp"
25 #include "ai/testing.hpp"
26 #include "display_chat_manager.hpp"
27 #include "game_end_exceptions.hpp"
28 #include "game_events/pump.hpp"
29 #include "preferences/game.hpp"
30 #include "gettext.hpp"
31 #include "gui/dialogs/story_viewer.hpp"
32 #include "gui/dialogs/transient_message.hpp"
33 #include "hotkey/hotkey_handler_sp.hpp"
34 #include "log.hpp"
35 #include "map/label.hpp"
36 #include "map/map.hpp"
37 #include "playturn.hpp"
38 #include "random_deterministic.hpp"
39 #include "replay_helper.hpp"
40 #include "resources.hpp"
41 #include "savegame.hpp"
42 #include "sound.hpp"
43 #include "synced_context.hpp"
44 #include "formula/string_utils.hpp"
45 #include "events.hpp"
46 #include "save_blocker.hpp"
47 #include "scripting/plugins/context.hpp"
48 #include "soundsource.hpp"
49 #include "statistics.hpp"
50 #include "units/unit.hpp"
51 #include "wesnothd_connection_error.hpp"
52 #include "whiteboard/manager.hpp"
53 #include "hotkey/hotkey_item.hpp"
54 #include <boost/dynamic_bitset.hpp>
55
56 static lg::log_domain log_aitesting("aitesting");
57 #define LOG_AIT LOG_STREAM(info, log_aitesting)
58 //If necessary, this define can be replaced with `#define LOG_AIT std::cout` to restore previous behavior
59
60 static lg::log_domain log_engine("engine");
61 #define ERR_NG LOG_STREAM(err, log_engine)
62 #define LOG_NG LOG_STREAM(info, log_engine)
63
64 static lg::log_domain log_enginerefac("enginerefac");
65 #define LOG_RG LOG_STREAM(info, log_enginerefac)
66
playsingle_controller(const config & level,saved_game & state_of_game,const ter_data_cache & tdata,bool skip_replay)67 playsingle_controller::playsingle_controller(const config& level,
68 saved_game& state_of_game, const ter_data_cache & tdata, bool skip_replay)
69 : play_controller(level, state_of_game, tdata, skip_replay)
70 , cursor_setter_(cursor::NORMAL)
71 , textbox_info_()
72 , replay_sender_(*resources::recorder)
73 , network_reader_([this](config& cfg) {return receive_from_wesnothd(cfg);})
74 , turn_data_(replay_sender_, network_reader_)
75 , end_turn_(END_TURN_NONE)
76 , skip_next_turn_(false)
77 , ai_fallback_(false)
78 , replay_controller_()
79 {
80 hotkey_handler_.reset(new hotkey_handler(*this, saved_game_)); //upgrade hotkey handler to the sp (whiteboard enabled) version
81
82
83 // game may need to start in linger mode
84 linger_ = this->is_regular_game_end();
85
86 plugins_context_->set_accessor_string("level_result", std::bind(&playsingle_controller::describe_result, this));
87 plugins_context_->set_accessor_int("turn", std::bind(&play_controller::turn, this));
88 }
89
describe_result() const90 std::string playsingle_controller::describe_result() const
91 {
92 if(!is_regular_game_end()) {
93 return "NONE";
94 }
95 else if(get_end_level_data_const().is_victory){
96 return "VICTORY";
97 }
98 else {
99 return "DEFEAT";
100 }
101 }
102
init_gui()103 void playsingle_controller::init_gui()
104 {
105 LOG_NG << "Initializing GUI... " << (SDL_GetTicks() - ticks()) << "\n";
106 play_controller::init_gui();
107
108 // Scroll to the starting position of the first team. If there is a
109 // human team, use that team; otherwise use team 1. If the map defines
110 // a starting position for the selected team, scroll to that tile. Note
111 // this often does not matter since many scenario start with messages,
112 // which will usually scroll to the speaker. Also note that the map
113 // does not necessarily define the starting positions. While usually
114 // best to use the map, the scenarion may explicitly set the positions,
115 // overriding those found in the map (if any).
116 if(map_start_.valid())
117 {
118 gui_->scroll_to_tile(map_start_, game_display::WARP, false);
119 LOG_NG << "Found good stored ui location " << map_start_ << "\n";
120 }
121 else
122 {
123 int scroll_team = gamestate().first_human_team_ + 1;
124 if (scroll_team == 0) {
125 scroll_team = 1;
126 }
127 map_location loc(gamestate().board_.map().starting_position(scroll_team));
128 if ((loc.x >= 0) && (loc.y >= 0)) {
129 gui_->scroll_to_tile(loc, game_display::WARP);
130 LOG_NG << "Found bad stored ui location " << map_start_ << " using side starting location " << loc << "\n";
131 }
132 else {
133 LOG_NG << "Found bad stored ui location\n";
134 }
135 }
136
137 update_locker lock_display(gui_->video(), is_skipping_replay());
138 get_hotkey_command_executor()->set_button_state();
139 }
140
141
play_scenario_init()142 void playsingle_controller::play_scenario_init()
143 {
144 // At the beginning of the scenario, save a snapshot as replay_start
145 if(saved_game_.replay_start().empty()){
146 saved_game_.replay_start() = to_config();
147 }
148 start_game();
149 if(!saved_game_.classification().random_mode.empty() && is_networked_mp()) {
150 // This won't cause errors later but we should notify the user about it in case he didn't knew it.
151 gui2::show_transient_message(
152 // TODO: find a better title
153 _("Game Error"),
154 _("This multiplayer game uses an alternative random mode, if you don't know what this message means, then most likely someone is cheating or someone reloaded a corrupt game.")
155 );
156 }
157 return;
158 }
159
play_scenario_main_loop()160 void playsingle_controller::play_scenario_main_loop()
161 {
162 LOG_NG << "starting main loop\n" << (SDL_GetTicks() - ticks()) << "\n";
163
164
165 // Avoid autosaving after loading, but still
166 // allow the first turn to have an autosave.
167 ai_testing::log_game_start();
168 if(gamestate().board_.teams().empty())
169 {
170 ERR_NG << "Playing game with 0 teams." << std::endl;
171 }
172 while(true) {
173 try {
174 play_turn();
175 if (is_regular_game_end()) {
176 turn_data_.send_data();
177 return;
178 }
179 }
180 catch(const reset_gamestate_exception& ex) {
181 //
182 // TODO:
183 //
184 // The MP replay feature still doesn't work properly (causes OOS)
185 // because:
186 //
187 // 1) The undo stack is not reset along with the gamestate (fixed).
188 // 2) The server_request_number_ is not reset along with the
189 // gamestate (fixed).
190 // 3) chat and other unsynced actions are inserted in the middle of
191 // the replay bringing the replay_pos in unorder (fixed).
192 // 4) untracked changes in side controllers are lost when resetting
193 // gamestate (fixed).
194 // 5) The game should have a stricter check for whether the loaded
195 // game is actually a parent of this game.
196 // 6) If an action was undone after a game was saved it can cause
197 // OOS if the undone action is in the snapshot of the saved
198 // game (luckily this is never the case for autosaves).
199 //
200 boost::dynamic_bitset<> local_players;
201 local_players.resize(gamestate().board_.teams().size(), true);
202 //Preserve side controllers, because we won't get the side controoller updates again when replaying.
203 for(size_t i = 0; i < local_players.size(); ++i) {
204 local_players[i] = gamestate().board_.teams()[i].is_local();
205 }
206 if(ex.start_replay) {
207 // MP "Back to turn"
208 statistics::read_stats(*ex.stats_);
209 } else {
210 // SP replay
211 statistics::reset_current_scenario();
212 }
213 reset_gamestate(*ex.level, (*ex.level)["replay_pos"]);
214 for(size_t i = 0; i < local_players.size(); ++i) {
215 resources::gameboard->teams()[i].set_local(local_players[i]);
216 }
217 play_scenario_init();
218 if (replay_controller_ == nullptr) {
219 replay_controller_.reset(new replay_controller(*this, false, ex.level, [this](){ on_replay_end(false); } ));
220 }
221 if(ex.start_replay) {
222 replay_controller_->play_replay();
223 }
224 }
225 } //end for loop
226 }
227
play_scenario(const config & level)228 LEVEL_RESULT playsingle_controller::play_scenario(const config& level)
229 {
230 LOG_NG << "in playsingle_controller::play_scenario()...\n";
231
232 // Start music.
233 for(const config &m : level.child_range("music")) {
234 sound::play_music_config(m, true);
235 }
236 sound::commit_music_changes();
237
238 if(!this->is_skipping_replay() && !this->is_skipping_story()) {
239 // Combine all the [story] tags into a single config. Handle this here since
240 // storyscreen::controller doesn't have a default constructor.
241 config cfg;
242 for(const auto& iter : level.child_range("story")) {
243 cfg.append_children(iter);
244 }
245
246 if(!cfg.empty()) {
247 gui2::dialogs::story_viewer::display(get_scenario_name(), cfg);
248 }
249 }
250 gui_->labels().read(level);
251
252 // Read sound sources
253 assert(soundsources_manager_ != nullptr);
254 for (const config &s : level.child_range("sound_source")) {
255 try {
256 soundsource::sourcespec spec(s);
257 soundsources_manager_->add(spec);
258 } catch (const bad_lexical_cast &) {
259 ERR_NG << "Error when parsing sound_source config: bad lexical cast." << std::endl;
260 ERR_NG << "sound_source config was: " << s.debug() << std::endl;
261 ERR_NG << "Skipping this sound source..." << std::endl;
262 }
263 }
264 LOG_NG << "entering try... " << (SDL_GetTicks() - ticks()) << "\n";
265 try {
266 play_scenario_init();
267 // clears level config;
268 this->saved_game_.remove_snapshot();
269
270 if (!is_regular_game_end() && !linger_) {
271 play_scenario_main_loop();
272 }
273 if (game_config::exit_at_end) {
274 exit(0);
275 }
276 const bool is_victory = get_end_level_data_const().is_victory;
277
278 if(gamestate().gamedata_.phase() <= game_data::PRESTART) {
279 gui_->video().clear_screen();
280 }
281
282 ai_testing::log_game_end();
283
284 const end_level_data& end_level = get_end_level_data_const();
285
286 if (gamestate().board_.teams().empty())
287 {
288 //store persistent teams
289 saved_game_.set_snapshot(config());
290
291 return LEVEL_RESULT::VICTORY; // this is probably only a story scenario, i.e. has its endlevel in the prestart event
292 }
293 if(linger_) {
294 LOG_NG << "resuming from loaded linger state...\n";
295 //as carryover information is stored in the snapshot, we have to re-store it after loading a linger state
296 saved_game_.set_snapshot(config());
297 if(!is_observer()) {
298 persist_.end_transaction();
299 }
300 return LEVEL_RESULT::VICTORY;
301 }
302 pump().fire(is_victory ? "local_victory" : "local_defeat");
303 { // Block for set_scontext_synced_base
304 set_scontext_synced_base sync;
305 pump().fire(end_level.proceed_to_next_level ? "victory" : "defeat");
306 pump().fire("scenario_end");
307 }
308 if(end_level.proceed_to_next_level) {
309 gamestate().board_.heal_all_survivors();
310 }
311 if(is_observer()) {
312 gui2::show_transient_message(_("Game Over"), _("The game is over."));
313 return LEVEL_RESULT::OBSERVER_END;
314 }
315 // If we're a player, and the result is victory/defeat, then send
316 // a message to notify the server of the reason for the game ending.
317 send_to_wesnothd(config {
318 "info", config {
319 "type", "termination",
320 "condition", "game over",
321 "result", is_victory ? "victory" : "defeat",
322 },
323 });
324 // Play victory music once all victory events
325 // are finished, if we aren't observers and the
326 // carryover dialog isn't disabled.
327 //
328 // Some scenario authors may use 'continue'
329 // result for something that is not story-wise
330 // a victory, so let them use [music] tags
331 // instead should they want special music.
332 const std::string& end_music = select_music(is_victory);
333 if((!is_victory || end_level.transient.carryover_report) && !end_music.empty()) {
334 sound::empty_playlist();
335 sound::play_music_once(end_music);
336 }
337 persist_.end_transaction();
338 return is_victory ? LEVEL_RESULT::VICTORY : LEVEL_RESULT::DEFEAT;
339 } catch(const savegame::load_game_exception &) {
340 // Loading a new game is effectively a quit.
341 saved_game_.clear();
342 throw;
343 } catch(const wesnothd_error& e) {
344 scoped_savegame_snapshot snapshot(*this);
345 savegame::ingame_savegame save(saved_game_, preferences::save_compression_format());
346 if(e.message == "") {
347 save.save_game_interactive(_("A network disconnection has occurred, and the game cannot continue. Do you want to save the game?"), savegame::savegame::YES_NO);
348 } else {
349 save.save_game_interactive(_("This game has been ended.\nReason: ")+e.message+_("\nDo you want to save the game?"), savegame::savegame::YES_NO);
350 }
351 if(dynamic_cast<const ingame_wesnothd_error*>(&e)) {
352 return LEVEL_RESULT::QUIT;
353 } else {
354 throw;
355 }
356 }
357 }
358
play_idle_loop()359 void playsingle_controller::play_idle_loop()
360 {
361 while(!should_return_to_play_side()) {
362 play_slice_catch();
363 SDL_Delay(10);
364 }
365 }
366
play_side_impl()367 void playsingle_controller::play_side_impl()
368 {
369 if (!skip_next_turn_) {
370 end_turn_ = END_TURN_NONE;
371 }
372 if(replay_controller_.get() != nullptr) {
373 init_side_done_now_ = false;
374 REPLAY_RETURN res = replay_controller_->play_side_impl();
375 if(res == REPLAY_FOUND_END_TURN) {
376 end_turn_ = END_TURN_SYNCED;
377 }
378 if (player_type_changed_) {
379 replay_controller_.reset();
380 }
381 } else if((current_team().is_local_human() && current_team().is_proxy_human())) {
382 LOG_NG << "is human...\n";
383 // If a side is dead end the turn, but play at least side=1's
384 // turn in case all sides are dead
385 if (gamestate().board_.side_units(current_side()) == 0 && !(gamestate().board_.units().empty() && current_side() == 1)) {
386 end_turn_ = END_TURN_REQUIRED;
387 }
388
389 before_human_turn();
390 if (end_turn_ == END_TURN_NONE) {
391 play_human_turn();
392 }
393 if ( !player_type_changed_ && !is_regular_game_end()) {
394 after_human_turn();
395 }
396 LOG_NG << "human finished turn...\n";
397
398 } else if(current_team().is_local_ai() || (current_team().is_local_human() && current_team().is_droid())) {
399 play_ai_turn();
400 } else if(current_team().is_network()) {
401 play_network_turn();
402 } else if(current_team().is_local_human() && current_team().is_idle()) {
403 end_turn_enable(false);
404 do_idle_notification();
405 before_human_turn();
406 if (end_turn_ == END_TURN_NONE) {
407 play_idle_loop();
408 }
409 }
410 else {
411 // we should have skipped over empty controllers before so this shouldn't be possible
412 ERR_NG << "Found invalid side controller " << current_team().controller().to_string() << " (" << current_team().proxy_controller().to_string() << ") for side " << current_team().side() << "\n";
413 }
414 }
415
before_human_turn()416 void playsingle_controller::before_human_turn()
417 {
418 log_scope("player turn");
419 assert(!linger_);
420 if(end_turn_ != END_TURN_NONE || is_regular_game_end()) {
421 return;
422 }
423
424 if(init_side_done_now_ && !game_config::disable_autosave && preferences::autosavemax() > 0) {
425 scoped_savegame_snapshot snapshot(*this);
426 savegame::autosave_savegame save(saved_game_, preferences::save_compression_format());
427 save.autosave(game_config::disable_autosave, preferences::autosavemax(), preferences::INFINITE_AUTO_SAVES);
428 }
429
430 if(preferences::turn_bell()) {
431 sound::play_bell(game_config::sounds::turn_bell);
432 }
433 }
434
show_turn_dialog()435 void playsingle_controller::show_turn_dialog(){
436 if(preferences::turn_dialog() && !is_regular_game_end() ) {
437 blindfold b(*gui_, true); //apply a blindfold for the duration of this dialog
438 gui_->redraw_everything();
439 gui_->recalculate_minimap();
440 std::string message = _("It is now $name|’s turn");
441 utils::string_map symbols;
442 symbols["name"] = gamestate().board_.get_team(current_side()).side_name();
443 message = utils::interpolate_variables_into_string(message, &symbols);
444 gui2::show_transient_message("", message);
445 }
446 }
447
execute_gotos()448 void playsingle_controller::execute_gotos()
449 {
450 if(should_return_to_play_side())
451 {
452 return;
453 }
454 try
455 {
456 menu_handler_.execute_gotos(mouse_handler_, current_side());
457 }
458 catch (const return_to_play_side_exception&)
459 {
460 }
461 }
462
play_human_turn()463 void playsingle_controller::play_human_turn() {
464 show_turn_dialog();
465
466 if (!preferences::disable_auto_moves()) {
467 execute_gotos();
468 }
469
470 end_turn_enable(true);
471 while(!should_return_to_play_side()) {
472 check_objectives();
473 play_slice_catch();
474 }
475
476 }
477
linger()478 void playsingle_controller::linger()
479 {
480 LOG_NG << "beginning end-of-scenario linger\n";
481 linger_ = true;
482
483 // If we need to set the status depending on the completion state
484 // the key to it is here.
485 gui_->set_game_mode(game_display::LINGER);
486
487 // change the end-turn button text to its alternate label
488 gui_->get_theme().refresh_title2("button-endturn", "title2");
489 gui_->invalidate_theme();
490 gui_->redraw_everything();
491
492 // End all unit moves
493 gamestate().board_.set_all_units_user_end_turn();
494 try {
495 // Same logic as single-player human turn, but
496 // *not* the same as multiplayer human turn.
497 end_turn_enable(true);
498 end_turn_ = END_TURN_NONE;
499 while(end_turn_ == END_TURN_NONE) {
500 play_slice();
501 }
502 } catch(const savegame::load_game_exception &) {
503 // Loading a new game is effectively a quit.
504 saved_game_.clear();
505 throw;
506 }
507
508 // revert the end-turn button text to its normal label
509 gui_->get_theme().refresh_title2("button-endturn", "title");
510 gui_->invalidate_theme();
511 gui_->redraw_everything();
512 gui_->set_game_mode(game_display::RUNNING);
513
514 LOG_NG << "ending end-of-scenario linger\n";
515 }
516
end_turn_enable(bool enable)517 void playsingle_controller::end_turn_enable(bool enable)
518 {
519 gui_->enable_menu("endturn", enable);
520 get_hotkey_command_executor()->set_button_state();
521 }
522
523
after_human_turn()524 void playsingle_controller::after_human_turn()
525 {
526 // Clear moves from the GUI.
527 gui_->set_route(nullptr);
528 gui_->unhighlight_reach();
529 }
530
play_ai_turn()531 void playsingle_controller::play_ai_turn()
532 {
533 LOG_NG << "is ai...\n";
534
535 end_turn_enable(false);
536 gui_->recalculate_minimap();
537
538 const cursor::setter cursor_setter(cursor::WAIT);
539
540 // Correct an oddball case where a human could have left delayed shroud
541 // updates on before giving control to the AI. (The AI does not bother
542 // with the undo stack, so it cannot delay shroud updates.)
543 team & cur_team = current_team();
544 if ( !cur_team.auto_shroud_updates() ) {
545 // We just took control, so the undo stack is empty. We still need
546 // to record this change for the replay though.
547 synced_context::run_and_store("auto_shroud", replay_helper::get_auto_shroud(true));
548 }
549 undo_stack().clear();
550
551 turn_data_.send_data();
552 try {
553 try {
554 if (!should_return_to_play_side()) {
555 ai::manager::get_singleton().play_turn(current_side());
556 }
557 }
558 catch (const return_to_play_side_exception&) {
559 }
560 catch (const fallback_ai_to_human_exception&) {
561 current_team().make_human();
562 player_type_changed_ = true;
563 ai_fallback_ = true;
564 }
565 }
566 catch(...) {
567 turn_data_.sync_network();
568 throw;
569 }
570 if(!should_return_to_play_side()) {
571 end_turn_ = END_TURN_REQUIRED;
572 }
573 turn_data_.sync_network();
574 gui_->recalculate_minimap();
575 gui_->invalidate_unit();
576 gui_->invalidate_game_status();
577 gui_->invalidate_all();
578 }
579
580
581 /**
582 * Will handle sending a networked notification in descendent classes.
583 */
do_idle_notification()584 void playsingle_controller::do_idle_notification()
585 {
586 gui_->get_chat_manager().add_chat_message(time(nullptr), "Wesnoth", 0,
587 "This side is in an idle state. To proceed with the game, the host must assign it to another controller.",
588 events::chat_handler::MESSAGE_PUBLIC, false);
589 }
590
591 /**
592 * Will handle networked turns in descendent classes.
593 */
play_network_turn()594 void playsingle_controller::play_network_turn()
595 {
596 // There should be no networked sides in single-player.
597 ERR_NG << "Networked team encountered by playsingle_controller." << std::endl;
598 }
599
600
handle_generic_event(const std::string & name)601 void playsingle_controller::handle_generic_event(const std::string& name){
602 if (name == "ai_user_interact"){
603 play_slice(false);
604 }
605 }
606
607
608
end_turn()609 void playsingle_controller::end_turn(){
610 if (linger_)
611 end_turn_ = END_TURN_REQUIRED;
612 else if (!is_browsing() && menu_handler_.end_turn(current_side())){
613 end_turn_ = END_TURN_REQUIRED;
614 }
615 }
616
force_end_turn()617 void playsingle_controller::force_end_turn(){
618 skip_next_turn_ = true;
619 end_turn_ = END_TURN_REQUIRED;
620 }
621
check_objectives()622 void playsingle_controller::check_objectives()
623 {
624 if (!gamestate().board_.teams().empty()) {
625 const team &t = gamestate().board_.teams()[gui_->viewing_team()];
626
627 if (!is_regular_game_end() && !is_browsing() && t.objectives_changed()) {
628 show_objectives();
629 }
630 }
631 }
632
633
maybe_linger()634 void playsingle_controller::maybe_linger()
635 {
636 // mouse_handler expects at least one team for linger mode to work.
637 assert(is_regular_game_end());
638 if (get_end_level_data_const().transient.linger_mode && !gamestate().board_.teams().empty()) {
639 linger();
640 }
641 }
642
sync_end_turn()643 void playsingle_controller::sync_end_turn()
644 {
645 //We cannot add [end_turn] to the recorder while executing another action.
646 assert(synced_context::synced_state() == synced_context::UNSYNCED);
647 if(end_turn_ == END_TURN_REQUIRED && current_team().is_local())
648 {
649 //TODO: we should also send this immediately.
650 resources::recorder->end_turn(gamestate_->next_player_number_);
651 end_turn_ = END_TURN_SYNCED;
652 }
653
654 assert(end_turn_ == END_TURN_SYNCED);
655 skip_next_turn_ = false;
656
657 if(ai_fallback_) {
658 current_team().make_ai();
659 ai_fallback_ = false;
660 }
661 }
662
update_viewing_player()663 void playsingle_controller::update_viewing_player()
664 {
665 if(replay_controller_ && replay_controller_->is_controlling_view()) {
666 replay_controller_->update_viewing_player();
667 }
668 //Update viewing team in case it has changed during the loop.
669 else if(int side_num = play_controller::find_last_visible_team()) {
670 if(side_num != this->gui_->viewing_side()) {
671 update_gui_to_player(side_num - 1);
672 }
673 }
674 }
675
reset_replay()676 void playsingle_controller::reset_replay()
677 {
678 if(replay_controller_ && replay_controller_->allow_reset_replay()) {
679 replay_controller_->stop_replay();
680 throw reset_gamestate_exception(replay_controller_->get_reset_state(), {}, false);
681 }
682 else {
683 ERR_NG << "received invalid reset replay\n";
684 }
685 }
686
enable_replay(bool is_unit_test)687 void playsingle_controller::enable_replay(bool is_unit_test)
688 {
689 replay_controller_.reset(new replay_controller(*this, gamestate().has_human_sides(), std::shared_ptr<config>( new config(saved_game_.get_replay_starting_point())), std::bind(&playsingle_controller::on_replay_end, this, is_unit_test)));
690 if(is_unit_test) {
691 replay_controller_->play_replay();
692 }
693 }
694
should_return_to_play_side() const695 bool playsingle_controller::should_return_to_play_side() const
696 {
697 if(player_type_changed_ || is_regular_game_end()) {
698 return true;
699 }
700 else if (end_turn_ == END_TURN_NONE || replay_controller_.get() != 0 || current_team().is_network()) {
701 return false;
702 }
703 else {
704 return true;
705 }
706 }
707
on_replay_end(bool is_unit_test)708 void playsingle_controller::on_replay_end(bool is_unit_test)
709 {
710 if(is_networked_mp()) {
711 set_player_type_changed();
712 }
713 else if(is_unit_test) {
714 replay_controller_->return_to_play_side();
715 if(!is_regular_game_end()) {
716 end_level_data e;
717 e.proceed_to_next_level = false;
718 e.is_victory = false;
719 set_end_level_data(e);
720 }
721 }
722 }
723