wesnoth/src/replay_controller.cpp
Steve Cotton ea25735501 Remove code from replay_controller that's already handled by the hotkey system
Fixes a crash when playing a replay that includes a theme, when that theme doesn't
have all of the buttons that the removed code expected. For example, this replay
from SXRPG 5.2.3 has two replay themes, of which one lacks button-nextmove:
* https://replays.wesnoth.org/1.14/20190701/SXRPG_TempleOfBones_Turn_8_(53897).bz2

All of the button::enable() calls were unnecessary logic, because the hotkey
system will query replay_controller::can_execute_command() and enable or
disable the buttons to match. However, the enable() calls appear to have had
the side-effect of triggering the hotkey system to check the buttons' required
states and redraw.

The code in build_replay_theme() was unreachable, because replay_controller
doesn't attach itself to the completely_redrawn event.
2019-07-28 18:38:18 +02:00

272 lines
7.8 KiB
C++

/*
Copyright (C) 2015 - 2018 by the Battle for Wesnoth Project
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
#include "replay_controller.hpp"
#include "game_config_manager.hpp"
#include "gettext.hpp"
#include "log.hpp"
#include "replay.hpp"
#include "resources.hpp"
#include "playsingle_controller.hpp"
static lg::log_domain log_engine("engine");
#define DBG_NG LOG_STREAM(debug, log_engine)
static lg::log_domain log_replay("replay");
#define DBG_REPLAY LOG_STREAM(debug, log_replay)
#define LOG_REPLAY LOG_STREAM(info, log_replay)
#define ERR_REPLAY LOG_STREAM(err, log_replay)
namespace
{
struct replay_play_nostop : public replay_controller::replay_stop_condition
{
replay_play_nostop() {}
virtual bool should_stop() { return false; }
};
struct replay_play_moves : public replay_controller::replay_stop_condition
{
int moves_todo_;
replay_play_moves(int moves_todo) : moves_todo_(moves_todo) {}
virtual void move_done() { --moves_todo_; }
virtual bool should_stop() { return moves_todo_ == 0; }
};
struct replay_play_turn : public replay_controller::replay_stop_condition
{
int turn_begin_;
int turn_current_;
replay_play_turn(int turn_begin) : turn_begin_(turn_begin), turn_current_(turn_begin) {}
virtual void new_side_turn(int , int turn) { turn_current_ = turn; }
virtual bool should_stop() { return turn_begin_ != turn_current_; }
};
struct replay_play_side : public replay_controller::replay_stop_condition
{
bool next_side_;
replay_play_side() : next_side_(false) {}
virtual void new_side_turn(int , int) { next_side_ = true; }
virtual bool should_stop() { return next_side_; }
};
}
replay_controller::replay_controller(play_controller& controller, bool control_view, const std::shared_ptr<config>& reset_state, const std::function<void()>& on_end_replay)
: controller_(controller)
, stop_condition_(new replay_stop_condition())
, disabler_()
, vision_()
, reset_state_(reset_state)
, on_end_replay_(on_end_replay)
, return_to_play_side_(false)
{
if(control_view) {
vision_ = HUMAN_TEAM;
}
controller_.get_display().get_theme().theme_reset_event().attach_handler(this);
controller_.get_display().create_buttons();
controller_.get_display().redraw_everything();
}
replay_controller::~replay_controller()
{
if(controller_.is_skipping_replay()) {
controller_.toggle_skipping_replay();
}
controller_.get_display().get_theme().theme_reset_event().detach_handler(this);
controller_.get_display().create_buttons();
controller_.get_display().redraw_everything();
controller_.get_display().create_buttons();
}
void replay_controller::add_replay_theme()
{
const config &theme_cfg = controller_.get_theme(game_config_manager::get()->game_config(), controller_.theme());
if (const config &res = theme_cfg.child("resolution"))
{
if (const config &replay_theme_cfg = res.child("replay")) {
controller_.get_display().get_theme().modify(replay_theme_cfg);
}
//Make sure we get notified if the theme is redrawn completely. That way we have
//a chance to restore the replay controls of the theme as well.
controller_.get_display().invalidate_theme();
}
}
void replay_controller::stop_replay()
{
stop_condition_.reset(new replay_stop_condition());
update_enabled_buttons();
}
void replay_controller::replay_next_turn()
{
stop_condition_.reset(new replay_play_turn(controller_.gamestate().tod_manager_.turn()));
update_enabled_buttons();
}
void replay_controller::replay_next_side()
{
stop_condition_.reset(new replay_play_side());
update_enabled_buttons();
}
void replay_controller::replay_next_move()
{
stop_condition_.reset(new replay_play_moves(1));
update_enabled_buttons();
}
//move all sides till stop/end
void replay_controller::play_replay()
{
stop_condition_.reset(new replay_play_nostop());
update_enabled_buttons();
}
void replay_controller::update_gui()
{
controller_.get_display().recalculate_minimap();
controller_.get_display().redraw_minimap();
controller_.get_display().invalidate_all();
controller_.get_display().redraw_everything();
}
void replay_controller::update_enabled_buttons()
{
controller_.get_display().invalidate_theme();
controller_.get_display().redraw_everything();
}
void replay_controller::handle_generic_event(const std::string& name)
{
// this is only attached to one event - the theme_reset_event
if(name == "theme_reset") {
add_replay_theme();
}
if(std::shared_ptr<gui::button> skip_animation_button = controller_.get_display().find_action_button("skip-animation")) {
skip_animation_button->set_check(controller_.is_skipping_replay());
}
}
bool replay_controller::recorder_at_end() const
{
return resources::recorder->at_end();
}
REPLAY_RETURN replay_controller::play_side_impl()
{
update_enabled_buttons();
while(!return_to_play_side_ && !static_cast<playsingle_controller&>(controller_).get_player_type_changed())
{
if(!stop_condition_->should_stop())
{
if(resources::recorder->at_end()) {
//Gather more replay data
on_end_replay_();
}
else {
REPLAY_RETURN res = do_replay(true);
if(res == REPLAY_FOUND_END_MOVE) {
stop_condition_->move_done();
}
if(res == REPLAY_FOUND_END_TURN) {
return res;
}
if(res == REPLAY_RETURN_AT_END) {
stop_replay();
}
if(res == REPLAY_FOUND_INIT_TURN)
{
stop_condition_->new_side_turn(controller_.current_side(), controller_.gamestate().tod_manager_.turn());
}
}
controller_.play_slice(false);
// Update the buttons once, on the transition from not-stopped to stopped.
if(stop_condition_->should_stop()) {
update_enabled_buttons();
}
}
else
{
// Don't move the update_enabled_buttons() call here. This play_slice() should block
// until the next event occurs, but on X11/Linux update_enabled_buttons() seems to put
// an event in the queue, turning this into a busy loop.
controller_.play_slice(true);
}
}
return REPLAY_FOUND_END_MOVE;
}
bool replay_controller::can_execute_command(const hotkey::hotkey_command& cmd, int) const
{
hotkey::HOTKEY_COMMAND command = cmd.id;
switch(command) {
case hotkey::HOTKEY_REPLAY_SKIP_ANIMATION:
return true;
case hotkey::HOTKEY_REPLAY_SHOW_EVERYTHING:
case hotkey::HOTKEY_REPLAY_SHOW_EACH:
case hotkey::HOTKEY_REPLAY_SHOW_TEAM1:
return is_controlling_view();
//commands we only can do before the end of the replay
case hotkey::HOTKEY_REPLAY_STOP:
return !recorder_at_end();
case hotkey::HOTKEY_REPLAY_PLAY:
case hotkey::HOTKEY_REPLAY_NEXT_TURN:
case hotkey::HOTKEY_REPLAY_NEXT_SIDE:
case hotkey::HOTKEY_REPLAY_NEXT_MOVE:
//we have one events_disabler when starting the replay_controller and a second when entering the synced context.
return should_stop() && (events::commands_disabled <= 1 ) && !recorder_at_end();
case hotkey::HOTKEY_REPLAY_RESET:
return allow_reset_replay() && events::commands_disabled <= 1;
default:
assert(false);
return false;
}
}
void replay_controller::replay_show_everything()
{
vision_ = SHOW_ALL;
update_teams();
}
void replay_controller::replay_show_each()
{
vision_ = CURRENT_TEAM;
update_teams();
}
void replay_controller::replay_show_team1()
{
vision_ = HUMAN_TEAM;
update_teams();
}
void replay_controller::update_teams()
{
update_viewing_player();
controller_.get_display().invalidate_all();
update_gui();
}
void replay_controller::update_viewing_player()
{
assert(vision_);
controller_.update_gui_to_player(vision_ == HUMAN_TEAM ? controller_.gamestate().first_human_team_ : controller_.current_side() - 1, *vision_ == SHOW_ALL);
}
bool replay_controller::see_all()
{
return vision_ == SHOW_ALL;
}