refactor playsingle_controller game loop.

The main intention is to move the linger mode into the
loop so that the replay can play until linger mode
is reached. In particular to allow replays to show
victory events aswell.
This commit is contained in:
gfgtdf 2023-03-19 06:44:12 +01:00
parent e3d1e2b4f5
commit 2845e864f4
7 changed files with 186 additions and 191 deletions

View File

@ -85,12 +85,6 @@ level_result::type campaign_controller::playsingle_scenario(end_level_data &end_
}
end_level = playcontroller.get_end_level_data();
carryover_show_gold(playcontroller.gamestate(), playcontroller.is_observer(), is_unit_test_);
if(!video::headless()) {
playcontroller.maybe_linger();
}
state_.set_snapshot(playcontroller.to_config());
return res;
}
@ -107,13 +101,6 @@ level_result::type campaign_controller::playmp_scenario(end_level_data &end_leve
end_level = playcontroller.get_end_level_data();
if(res != level_result::type::observer_end) {
// We need to call this before linger because it prints the defeated/victory message.
//(we want to see that message before entering the linger mode)
carryover_show_gold(playcontroller.gamestate(), playcontroller.is_observer(), is_unit_test_);
}
playcontroller.maybe_linger();
playcontroller.update_savegame_snapshot();
if(mp_info_) {
@ -162,6 +149,7 @@ level_result::type campaign_controller::play_game()
#endif
{
res = playmp_scenario(end_level);
//todo: i removed the code that could return observer_end
}
} catch(const leavegame_wesnothd_error&) {
LOG_NG << "The game was remotely ended";

View File

@ -584,9 +584,8 @@ config play_controller::to_config() const
return cfg;
}
void play_controller::finish_side_turn()
void play_controller::finish_side_turn_events()
{
whiteboard_manager_->on_finish_side_turn(current_side());
{ // Block for set_scontext_synced
set_scontext_synced sync(1);
@ -609,8 +608,6 @@ void play_controller::finish_side_turn()
sync.do_final_checkup();
}
mouse_handler_.deselect_hex();
gamestate().gamedata_.set_phase(game_data::TURN_STARTING_WAITING);
did_autosave_this_turn_ = false;
}
void play_controller::finish_turn()
@ -620,7 +617,6 @@ void play_controller::finish_turn()
pump().fire("turn_end");
pump().fire("turn_" + turn_num + "_end");
sync.do_final_checkup();
did_tod_sound_this_turn_ = false;
}
bool play_controller::enemies_visible() const
@ -1348,78 +1344,6 @@ void play_controller::play_side()
sync_end_turn();
}
void play_controller::play_turn()
{
whiteboard_manager_->on_gamestate_change();
gui_->new_turn();
gui_->invalidate_game_status();
LOG_NG << "turn: " << turn();
if(video::headless()) {
LOG_AIT << "Turn " << turn() << ":";
}
int last_player_number = gamestate_->player_number_;
int next_player_number = gamestate_->next_player_number_;
while(gamestate_->player_number_ <= static_cast<int>(get_teams().size())) {
gamestate_->next_player_number_ = gamestate_->player_number_ + 1;
next_player_number = gamestate_->next_player_number_;
last_player_number = gamestate_->player_number_;
// If a side is empty skip over it.
if(!current_team().is_empty()) {
init_side_begin();
if(is_during_turn()) {
// This is the case in a reloaded game where the side was initialized before saving the game (the default case for reloading games).
init_side_end();
}
ai_testing::log_turn_start(current_side());
play_side();
// ignore any changes to next_player_number_ that happen after the [end_turn] is sended to the server,
// otherwise we will get OOS.
next_player_number = gamestate_->next_player_number_;
assert(next_player_number <= 2 * static_cast<int>(get_teams().size()));
if(is_regular_game_end()) {
return;
}
// note: play_side() send the [end_turn] to the sever and finish_side_turn() callsie the side turn end
// events.
// this means that during the side turn end events the clients think it is still the last sides turn
// while the server thinks that it is already the next plyers turn. i don'T think this is a problem
// though.
finish_side_turn();
if(is_regular_game_end()) {
return;
}
if(video::headless()) {
LOG_AIT << " Player " << current_side() << ": " << current_team().villages().size() << " Villages";
ai_testing::log_turn_end(current_side());
}
}
gamestate_->player_number_ = next_player_number;
}
// If the loop exits due to the last team having been processed.
gamestate_->player_number_ = last_player_number;
finish_turn();
// Time has run out
check_time_over();
if(!is_regular_game_end()) {
gamestate_->player_number_ = modulo(next_player_number, get_teams().size(), 1);
}
}
void play_controller::check_time_over()
{
const bool time_left = gamestate().tod_manager_.next_turn(&gamestate().gamedata_);

View File

@ -146,6 +146,8 @@ public:
return gamestate().end_level_data_.has_value();
}
bool check_regular_game_end();
const end_level_data& get_end_level_data() const
{
return *gamestate().end_level_data_;
@ -340,7 +342,7 @@ protected:
void fire_start();
void start_game();
virtual void init_gui();
void finish_side_turn();
void finish_side_turn_events();
void finish_turn(); //this should not throw an end turn or end level exception
bool enemies_visible() const;
@ -433,5 +435,4 @@ protected:
virtual void sync_end_turn() {}
virtual void check_time_over();
virtual void update_viewing_player() = 0;
void play_turn();
};

View File

@ -234,7 +234,6 @@ void playmp_controller::reset_end_scenario_button()
void playmp_controller::linger()
{
LOG_NG << "beginning end-of-scenario linger";
gamestate().gamedata_.set_phase(game_data::GAME_ENDED);
// If we need to set the status depending on the completion state
// we're needed here.
@ -448,6 +447,7 @@ void playmp_controller::maybe_linger()
} else {
linger();
}
end_turn_requested_ = true;
}
void playmp_controller::surrender(int side_number)

View File

@ -26,7 +26,6 @@ class playmp_controller : public playsingle_controller, public syncmp_handler
public:
playmp_controller(const config& level, saved_game& state_of_game, mp_game_metadata* mp_info);
virtual ~playmp_controller();
void maybe_linger() override;
void process_oos(const std::string& err_msg) const override;

View File

@ -25,6 +25,7 @@
#include "ai/manager.hpp"
#include "ai/testing.hpp"
#include "display_chat_manager.hpp"
#include "carryover_show_gold.hpp"
#include "events.hpp"
#include "formula/string_utils.hpp"
#include "game_end_exceptions.hpp"
@ -143,6 +144,21 @@ void playsingle_controller::init_gui()
void playsingle_controller::play_scenario_init(const config& level)
{
gui_->labels().read(level);
// Read sound sources
assert(soundsources_manager_ != nullptr);
for(const config& s : level.child_range("sound_source")) {
try {
soundsource::sourcespec spec(s);
soundsources_manager_->add(spec);
} catch(const bad_lexical_cast&) {
ERR_NG << "Error when parsing sound_source config: bad lexical cast.";
ERR_NG << "sound_source config was: " << s.debug();
ERR_NG << "Skipping this sound source...";
}
}
// At the beginning of the scenario, save a snapshot as replay_start
if(saved_game_.replay_start().empty()) {
saved_game_.replay_start() = to_config();
@ -161,8 +177,96 @@ void playsingle_controller::play_scenario_init(const config& level)
_("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."));
}
}
return;
void playsingle_controller::skip_empty_sides(int& side_num)
{
const int max = side_num + static_cast<int>(get_teams().size());
while (gamestate().board_.get_team(modulo(side_num, get_teams().size(), 1)).is_empty()) {
if(side_num == max) {
throw game::game_error("No teams found");
}
++side_num;
}
}
void playsingle_controller::play_some()
{
assert(is_regular_game_end() || gamestate().in_phase(game_data::TURN_STARTING_WAITING, game_data::TURN_PLAYING, game_data::TURN_ENDED, game_data::GAME_ENDED));
if (!is_regular_game_end() && gamestate().in_phase(game_data::TURN_STARTING_WAITING, game_data::TURN_PLAYING)) {
if(gamestate().in_phase(game_data::TURN_PLAYING)) {
// If we are here we have probably reloaded a savegame
init_side_end();
}
play_side();
assert(is_regular_game_end() || gamestate().in_phase(game_data::TURN_ENDED));
}
if (!is_regular_game_end() && gamestate().in_phase(game_data::TURN_ENDED)) {
finish_side_turn();
}
if (is_regular_game_end() && !gamestate().in_phase(game_data::GAME_ENDED)) {
gamestate().gamedata_.set_phase(game_data::GAME_ENDING);
do_end_level();
gamestate().gamedata_.set_phase(game_data::GAME_ENDED);
}
if (gamestate().in_phase(game_data::GAME_ENDED)) {
if(!get_end_level_data().transient.linger_mode || get_teams().empty() || video::headless()) {
end_turn_requested_ = true;
}
maybe_linger();
}
}
void playsingle_controller::finish_side_turn()
{
if(is_regular_game_end()) {
return;
}
/// Make a copy, since the [end_turn] was already sent to to server any changes to
// next_player_number by wml would cause OOS otherwise.
int next_player_number = gamestate_->next_player_number_;
whiteboard_manager_->on_finish_side_turn(current_side());
finish_side_turn_events();
if(is_regular_game_end()) {
return;
}
skip_empty_sides(next_player_number);
bool new_turn = next_player_number > static_cast<int>(get_teams().size());
next_player_number = modulo(next_player_number, get_teams().size(), 1);
if(new_turn) {
finish_turn();
if(is_regular_game_end()) {
return;
}
// Time has run out
check_time_over();
if(is_regular_game_end()) {
return;
}
did_tod_sound_this_turn_ = false;
}
// the turn end event might have deleted sides, so do this again.
skip_empty_sides(next_player_number);
gamestate_->player_number_ = modulo(next_player_number, get_teams().size(), 1);
gamestate_->next_player_number_ = gamestate_->player_number_ + 1;
if(new_turn) {
whiteboard_manager_->on_gamestate_change();
gui_->new_turn();
gui_->invalidate_game_status();
}
gamestate().gamedata_.set_phase(game_data::TURN_STARTING_WAITING);
did_autosave_this_turn_ = false;
init_side_begin();
}
void playsingle_controller::play_scenario_main_loop()
@ -174,13 +278,9 @@ void playsingle_controller::play_scenario_main_loop()
ERR_NG << "Playing game with 0 teams.";
}
while(true) {
while(!(gamestate().in_phase(game_data::GAME_ENDED) && end_turn_requested_ )) {
try {
play_turn();
if(is_regular_game_end()) {
turn_data_.send_data();
return;
}
play_some();
} catch(const reset_gamestate_exception& ex) {
//
// TODO:
@ -235,6 +335,71 @@ void playsingle_controller::play_scenario_main_loop()
} // end for loop
}
void playsingle_controller::do_end_level()
{
if(game_config::exit_at_end) {
exit(0);
}
const bool is_victory = get_end_level_data().is_victory;
ai_testing::log_game_end();
const end_level_data& end_level = get_end_level_data();
if(get_teams().empty()) {
// this is probably only a story scenario, i.e. has its endlevel in the prestart event
return;
}
pump().fire(is_victory ? "local_victory" : "local_defeat");
{ // Block for set_scontext_synced_base
set_scontext_synced_base sync;
pump().fire(end_level.proceed_to_next_level ? level_result::victory : level_result::defeat);
pump().fire("scenario_end");
}
if(end_level.proceed_to_next_level) {
gamestate().board_.heal_all_survivors();
}
if(is_observer()) {
gui2::show_transient_message(_("Game Over"), _("The game is over."));
}
// If we're a player, and the result is victory/defeat, then send
// a message to notify the server of the reason for the game ending.
send_to_wesnothd(config {
"info", config {
"type", "termination",
"condition", "game over",
"result", is_victory ? level_result::victory : level_result::defeat,
},
});
// Play victory music once all victory events
// are finished, if we aren't observers and the
// carryover dialog isn't disabled.
//
// Some scenario authors may use 'continue'
// result for something that is not story-wise
// a victory, so let them use [music] tags
// instead should they want special music.
const std::string& end_music = select_music(is_victory);
if((!is_victory || end_level.transient.carryover_report) && !end_music.empty()) {
sound::empty_playlist();
sound::play_music_once(end_music);
}
persist_.end_transaction();
if(!is_observer()) {
//TODO: passing is_observer() when is_observer() is false seems wrong.
carryover_show_gold(gamestate(), is_observer(), saved_game_.classification().is_test());
}
}
level_result::type playsingle_controller::play_scenario(const config& level)
{
LOG_NG << "in playsingle_controller::play_scenario()...";
@ -259,101 +424,18 @@ level_result::type playsingle_controller::play_scenario(const config& level)
}
}
gui_->labels().read(level);
// Read sound sources
assert(soundsources_manager_ != nullptr);
for(const config& s : level.child_range("sound_source")) {
try {
soundsource::sourcespec spec(s);
soundsources_manager_->add(spec);
} catch(const bad_lexical_cast&) {
ERR_NG << "Error when parsing sound_source config: bad lexical cast.";
ERR_NG << "sound_source config was: " << s.debug();
ERR_NG << "Skipping this sound source...";
}
}
LOG_NG << "entering try... " << (SDL_GetTicks() - ticks());
try {
play_scenario_init(level);
// clears level config;
saved_game_.remove_snapshot();
if(!is_regular_game_end() && !is_linger_mode()) {
play_scenario_main_loop();
}
if(game_config::exit_at_end) {
exit(0);
}
const bool is_victory = get_end_level_data().is_victory;
ai_testing::log_game_end();
const end_level_data& end_level = get_end_level_data();
if(get_teams().empty()) {
// this is probably only a story scenario, i.e. has its endlevel in the prestart event
return level_result::type::victory;
}
if(is_linger_mode()) {
LOG_NG << "resuming from loaded linger state...";
if(!is_observer()) {
persist_.end_transaction();
}
return level_result::type::victory;
}
gamestate().gamedata_.set_phase(game_data::GAME_ENDING);
pump().fire(is_victory ? "local_victory" : "local_defeat");
{ // Block for set_scontext_synced_base
set_scontext_synced_base sync;
pump().fire(end_level.proceed_to_next_level ? level_result::victory : level_result::defeat);
pump().fire("scenario_end");
}
if(end_level.proceed_to_next_level) {
gamestate().board_.heal_all_survivors();
}
play_scenario_main_loop();
if(is_observer()) {
gui2::show_transient_message(_("Game Over"), _("The game is over."));
return level_result::type::observer_end;
}
// If we're a player, and the result is victory/defeat, then send
// a message to notify the server of the reason for the game ending.
send_to_wesnothd(config {
"info", config {
"type", "termination",
"condition", "game over",
"result", is_victory ? level_result::victory : level_result::defeat,
},
});
// Play victory music once all victory events
// are finished, if we aren't observers and the
// carryover dialog isn't disabled.
//
// Some scenario authors may use 'continue'
// result for something that is not story-wise
// a victory, so let them use [music] tags
// instead should they want special music.
const std::string& end_music = select_music(is_victory);
if((!is_victory || end_level.transient.carryover_report) && !end_music.empty()) {
sound::empty_playlist();
sound::play_music_once(end_music);
}
persist_.end_transaction();
return level_result::get_enum(end_level.test_result).value_or(is_victory ? level_result::type::victory : level_result::type::defeat);
return level_result::get_enum(get_end_level_data().test_result).value_or(get_end_level_data().is_victory ? level_result::type::victory : level_result::type::defeat);
} catch(const savegame::load_game_exception&) {
// Loading a new game is effectively a quit.
saved_game_.clear();
@ -503,7 +585,6 @@ void playsingle_controller::play_human_turn()
void playsingle_controller::linger()
{
LOG_NG << "beginning end-of-scenario linger";
gamestate().gamedata_.set_phase(game_data::GAME_ENDED);
// If we need to set the status depending on the completion state
// the key to it is here.
@ -520,7 +601,6 @@ void playsingle_controller::linger()
// Same logic as single-player human turn, but
// *not* the same as multiplayer human turn.
end_turn_enable(true);
end_turn_requested_ = false;
while(!end_turn_requested_) {
play_slice();
}
@ -664,9 +744,8 @@ void playsingle_controller::maybe_linger()
{
// mouse_handler expects at least one team for linger mode to work.
assert(is_regular_game_end());
if(get_end_level_data().transient.linger_mode && !get_teams().empty()) {
linger();
}
linger();
end_turn_requested_ = true;
}
void playsingle_controller::sync_end_turn()

View File

@ -42,6 +42,10 @@ public:
level_result::type play_scenario(const config& level);
void play_scenario_init(const config& level);
void skip_empty_sides(int& side_num);
void play_some();
void finish_side_turn();
void do_end_level();
void play_scenario_main_loop();
virtual void handle_generic_event(const std::string& name) override;