wesnoth/src/whiteboard/manager.cpp
Charles Dang ac9f0674d7 Display: refactor out [playing|viewing]_side
Now defined in terms of [playing|viewing]_team().side(). Much clearer. No more damn +1 math.
2024-09-02 12:37:41 -04:00

1293 lines
36 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Copyright (C) 2010 - 2024
by Gabriel Morin <gabrielmorin (at) gmail (dot) com>
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
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.
*/
/**
* @file
*/
#include "whiteboard/manager.hpp"
#include "whiteboard/action.hpp"
#include "whiteboard/highlighter.hpp"
#include "whiteboard/mapbuilder.hpp"
#include "whiteboard/move.hpp"
#include "whiteboard/attack.hpp"
#include "whiteboard/recall.hpp"
#include "whiteboard/recruit.hpp"
#include "whiteboard/side_actions.hpp"
#include "whiteboard/utility.hpp"
#include "actions/undo.hpp"
#include "arrow.hpp"
#include "fake_unit_ptr.hpp"
#include "formula/string_utils.hpp"
#include "game_board.hpp"
#include "preferences/preferences.hpp"
#include "game_state.hpp"
#include "gettext.hpp"
#include "gui/dialogs/simple_item_selector.hpp"
#include "key.hpp"
#include "pathfind/pathfind.hpp"
#include "play_controller.hpp"
#include "replay_helper.hpp"
#include "resources.hpp"
#include "synced_context.hpp"
#include "team.hpp"
#include "units/unit.hpp"
#include "units/animation_component.hpp"
#include "units/udisplay.hpp"
#include <functional>
namespace wb {
manager::manager():
active_(false),
inverted_behavior_(false),
self_activate_once_(true),
#if 0
print_help_once_(true),
#endif
wait_for_side_init_(true),
planned_unit_map_active_(false),
executing_actions_(false),
executing_all_actions_(false),
preparing_to_end_turn_(false),
gamestate_mutated_(false),
activation_state_lock_(new bool),
unit_map_lock_(new bool),
mapbuilder_(),
highlighter_(),
route_(),
move_arrows_(),
fake_units_(),
temp_move_unit_underlying_id_(0),
key_poller_(new CKey),
hidden_unit_hexes_(),
net_buffer_(resources::gameboard->teams().size()),
team_plans_hidden_(resources::gameboard->teams().size()),
units_owning_moves_()
{
if(prefs::get().hide_whiteboard()) {
team_plans_hidden_.flip();
}
LOG_WB << "Manager initialized.";
}
manager::~manager()
{
LOG_WB << "Manager destroyed.";
}
//Used for chat-spamming debug info
#if 0
static void print_to_chat(const std::string& title, const std::string& message)
{
display::get_singleton()->add_chat_message(std::time(nullptr), title, 0, message,
events::chat_handler::MESSAGE_PRIVATE, false);
}
#endif
void manager::print_help_once()
{
#if 0
if (!print_help_once_)
return;
else
print_help_once_ = false;
print_to_chat("whiteboard", std::string("Type :wb to activate/deactivate planning mode.")
+ " Hold TAB to temporarily deactivate/activate it.");
std::stringstream hotkeys;
const hotkey::hotkey_item& hk_execute = hotkey::get_hotkey(hotkey::HOTKEY_WB_EXECUTE_ACTION);
if(!hk_execute.null()) {
//print_to_chat("[execute action]", "'" + hk_execute.get_name() + "'");
hotkeys << "Execute: " << hk_execute.get_name() << ", ";
}
const hotkey::hotkey_item& hk_execute_all = hotkey::get_hotkey(hotkey::HOTKEY_WB_EXECUTE_ALL_ACTIONS);
if(!hk_execute_all.null()) {
//print_to_chat("[execute action]", "'" + hk_execute_all.get_name() + "'");
hotkeys << "Execute all: " << hk_execute_all.get_name() << ", ";
}
const hotkey::hotkey_item& hk_delete = hotkey::get_hotkey(hotkey::HOTKEY_WB_DELETE_ACTION);
if(!hk_delete.null()) {
//print_to_chat("[delete action]", "'" + hk_delete.get_name() + "'");
hotkeys << "Delete: " << hk_delete.get_name() << ", ";
}
const hotkey::hotkey_item& hk_bump_up = hotkey::get_hotkey(hotkey::HOTKEY_WB_BUMP_UP_ACTION);
if(!hk_bump_up.null()) {
//print_to_chat("[move action earlier in queue]", "'" + hk_bump_up.get_name() + "'");
hotkeys << "Move earlier: " << hk_bump_up.get_name() << ", ";
}
const hotkey::hotkey_item& hk_bump_down = hotkey::get_hotkey(hotkey::HOTKEY_WB_BUMP_DOWN_ACTION);
if(!hk_bump_down.null()) {
//print_to_chat("[move action later in queue]", "'" + hk_bump_down.get_name() + "'");
hotkeys << "Move later: " << hk_bump_down.get_name() << ", ";
}
print_to_chat("HOTKEYS:", hotkeys.str() + "\n");
#endif
}
bool manager::can_modify_game_state() const
{
if(wait_for_side_init_
|| resources::gameboard == nullptr
|| executing_actions_
|| resources::gameboard->is_observer()
|| resources::controller->is_linger_mode()
|| !synced_context::is_unsynced())
{
return false;
}
else
{
return true;
}
}
bool manager::can_activate() const
{
//any more than one reference means a lock on whiteboard state was requested
if(activation_state_lock_.use_count() != 1)
return false;
return can_modify_game_state();
}
void manager::set_active(bool active)
{
if(!can_activate())
{
active_ = false;
LOG_WB << "Whiteboard can't be activated now.";
}
else if (active != active_)
{
active_ = active;
erase_temp_move();
if (active_)
{
if(should_clear_undo()) {
if(!resources::controller->current_team().auto_shroud_updates()) {
synced_context::run_and_throw("auto_shroud", replay_helper::get_auto_shroud(true));
}
resources::undo_stack->clear();
}
validate_viewer_actions();
LOG_WB << "Whiteboard activated! " << *viewer_actions();
create_temp_move();
} else {
LOG_WB << "Whiteboard deactivated!";
}
}
}
void manager::set_invert_behavior(bool invert)
{
//any more than one reference means a lock on whiteboard state was requested
if(activation_state_lock_.use_count() != 1)
return;
bool block_whiteboard_activation = false;
if(!can_activate())
{
block_whiteboard_activation = true;
}
if (invert)
{
if (!inverted_behavior_)
{
if (active_)
{
DBG_WB << "Whiteboard deactivated temporarily.";
inverted_behavior_ = true;
set_active(false);
}
else if (!block_whiteboard_activation)
{
DBG_WB << "Whiteboard activated temporarily.";
inverted_behavior_ = true;
set_active(true);
}
}
}
else
{
if (inverted_behavior_)
{
if (active_)
{
DBG_WB << "Whiteboard set back to deactivated status.";
inverted_behavior_ = false;
set_active(false);
}
else if (!block_whiteboard_activation)
{
DBG_WB << "Whiteboard set back to activated status.";
inverted_behavior_ = false;
set_active(true);
}
}
}
}
bool manager::can_enable_execution_hotkeys() const
{
return can_enable_modifier_hotkeys() && viewer_side() == resources::controller->current_side()
&& viewer_actions()->turn_size(0) > 0;
}
bool manager::can_enable_modifier_hotkeys() const
{
return can_modify_game_state() && !viewer_actions()->empty();
}
bool manager::can_enable_reorder_hotkeys() const
{
return can_enable_modifier_hotkeys() && highlighter_ && highlighter_->get_bump_target();
}
bool manager::allow_leader_to_move(const unit& leader) const
{
if(!has_actions())
return true;
//Look for another leader on another keep in the same castle
{ wb::future_map future; // start planned unit map scope
// TODO: when the game executes all whiteboard moves at turn end applying the future map
// will fail because we are currently executing actions, and if one of those actions
// was a movement of the leader this function will be called, resulting the the error
// mesage below, we silence that message for now by adding (!executing_actions_)
//
// Also this check is generally flawed, for example it could happen that the leader found
// by find_backup_leader would be moved to that location _after_ the unit would be recruited
// It could also happen that the original leader can be moved back to that location before
// the unit is recruited.
if(!has_planned_unit_map() && !executing_actions_) {
WRN_WB << "Unable to build future map to determine whether leader's allowed to move.";
}
if(find_backup_leader(leader))
return true;
} // end planned unit map scope
if(viewer_actions()->empty()) {
return true;
}
//Look for planned recruits that depend on this leader
for(action_const_ptr action : *viewer_actions())
{
recruit_const_ptr recruit = std::dynamic_pointer_cast<class recruit const>(action);
recall_const_ptr recall = std::dynamic_pointer_cast<class recall const>(action);
if(recruit || recall)
{
map_location const target_hex = recruit?recruit->get_recruit_hex():recall->get_recall_hex();
if (dynamic_cast<game_state&>(*resources::filter_con).can_recruit_on(leader, target_hex))
return false;
}
}
return true;
}
void manager::on_init_side()
{
//Turn should never start with action auto-execution already enabled!
assert(!executing_all_actions_ && !executing_actions_);
update_plan_hiding(); /* validates actions */
wait_for_side_init_ = false;
LOG_WB << "on_init_side()";
if (self_activate_once_ && prefs::get().enable_planning_mode_on_start())
{
self_activate_once_ = false;
set_active(true);
}
}
void manager::on_finish_side_turn(int side)
{
preparing_to_end_turn_ = false;
wait_for_side_init_ = true;
if(side == viewer_side() && !viewer_actions()->empty()) {
viewer_actions()->synced_turn_shift();
}
highlighter_.reset();
erase_temp_move();
LOG_WB << "on_finish_side_turn()";
}
void manager::pre_delete_action(action_ptr)
{
}
void manager::post_delete_action(action_ptr action)
{
// The fake unit representing the destination of a chain of planned moves should have the regular animation.
// If the last remaining action of the unit that owned this move is a move as well,
// adjust its appearance accordingly.
side_actions_ptr side_actions = resources::gameboard->teams().at(action->team_index()).get_side_actions();
unit_ptr actor = action->get_unit();
if(actor) { // The unit might have died following the execution of an attack
side_actions::iterator action_it = side_actions->find_last_action_of(*actor);
if(action_it != side_actions->end()) {
move_ptr move = std::dynamic_pointer_cast<class move>(*action_it);
if(move && move->get_fake_unit()) {
move->get_fake_unit()->anim_comp().set_standing(true);
}
}
}
}
static void hide_all_plans()
{
for(team& t : resources::gameboard->teams()){
t.get_side_actions()->hide();
}
}
/* private */
void manager::update_plan_hiding(std::size_t team_index)
{
//We don't control the "viewing" side ... we're probably an observer
if(!resources::gameboard->teams().at(team_index).is_local_human())
hide_all_plans();
else // normal circumstance
{
for(team& t : resources::gameboard->teams())
{
//make sure only appropriate teams are hidden
if(!t.is_network_human())
team_plans_hidden_[t.side()-1] = false;
if(t.is_enemy(team_index+1) || team_plans_hidden_[t.side()-1])
t.get_side_actions()->hide();
else
t.get_side_actions()->show();
}
}
validate_viewer_actions();
}
void manager::update_plan_hiding()
{
update_plan_hiding(display::get_singleton()->viewing_team_index());
}
void manager::on_viewer_change(std::size_t team_index)
{
if(!wait_for_side_init_)
update_plan_hiding(team_index);
}
void manager::on_change_controller(int side, const team& t)
{
wb::side_actions& sa = *t.get_side_actions();
if(t.is_local_human()) // we own this side now
{
//tell everyone to clear this side's actions -- we're starting anew
queue_net_cmd(sa.team_index(),sa.make_net_cmd_clear());
sa.clear();
//refresh the hidden_ attribute of every team's side_actions
update_plan_hiding();
}
else if(t.is_local_ai() || t.is_network_ai()) // no one owns this side anymore
sa.clear(); // clear its plans away -- the ai doesn't plan ... yet
else if(t.is_network()) // Another client is taking control of the side
{
if(side==viewer_side()) // They're taking OUR side away!
hide_all_plans(); // give up knowledge of everyone's plans, in case we became an observer
//tell them our plans -- they may not have received them up to this point
std::size_t num_teams = resources::gameboard->teams().size();
for(std::size_t i=0; i<num_teams; ++i)
{
team& local_team = resources::gameboard->teams().at(i);
if(local_team.is_local_human() && !local_team.is_enemy(side))
queue_net_cmd(i,local_team.get_side_actions()->make_net_cmd_refresh());
}
}
}
void manager::on_kill_unit()
{
if(highlighter_ != nullptr) {
highlighter_->set_selection_candidate(unit_ptr());
}
}
bool manager::current_side_has_actions()
{
if(current_side_actions()->empty()) {
return false;
}
side_actions::range_t range = current_side_actions()->iter_turn(0);
return range.first != range.second; //non-empty range
}
void manager::validate_viewer_actions()
{
LOG_WB << "'gamestate_mutated_' flag dirty, validating actions.";
gamestate_mutated_ = false;
if(has_planned_unit_map()) {
real_map();
} else {
future_map();
}
}
//helper fcn
static void draw_numbers(const map_location& hex, side_actions::numbers_t numbers)
{
std::vector<int>& numbers_to_draw = numbers.numbers_to_draw;
std::vector<std::size_t>& team_numbers = numbers.team_numbers;
int& main_number = numbers.main_number;
std::set<std::size_t>& secondary_numbers = numbers.secondary_numbers;
const double x_offset_base = 0.0;
const double y_offset_base = 0.2;
//position 0,0 in the hex is the upper left corner
//0.8 = horizontal coord., close to the right side of the hex
const double x_origin = 0.8 - numbers_to_draw.size() * x_offset_base;
//0.5 = halfway in the hex vertically
const double y_origin = 0.5 - numbers_to_draw.size() * (y_offset_base / 2);
double x_offset = 0, y_offset = 0;
std::size_t size = numbers_to_draw.size();
for(std::size_t i=0; i<size; ++i)
{
int number = numbers_to_draw[i];
std::string number_text = std::to_string(number);
std::size_t font_size;
if (static_cast<int>(i) == main_number) font_size = 19;
else if (secondary_numbers.find(i)!=secondary_numbers.end()) font_size = 17;
else font_size = 15;
color_t color = team::get_side_color(static_cast<int>(team_numbers[i]+1));
const double x_in_hex = x_origin + x_offset;
const double y_in_hex = y_origin + y_offset;
display::get_singleton()->draw_text_in_hex(hex, drawing_layer::actions_numbering,
number_text, font_size, color, x_in_hex, y_in_hex);
x_offset += x_offset_base;
y_offset += y_offset_base;
}
}
namespace
{
//Helper struct that finds all units teams whose planned actions are currently visible
//Only used by manager::pre_draw().
//Note that this structure is used as a functor.
struct move_owners_finder: public visitor
{
public:
move_owners_finder(): move_owners_() { }
void operator()(action* action) {
action->accept(*this);
}
const std::set<std::size_t>& get_units_owning_moves() {
return move_owners_;
}
virtual void visit(move_ptr move) {
if(std::size_t id = move->get_unit_id()) {
move_owners_.insert(id);
}
}
virtual void visit(attack_ptr attack) {
//also add attacks if they have an associated move
if(attack->get_route().steps.size() >= 2) {
if(std::size_t id = attack->get_unit_id()) {
move_owners_.insert(id);
}
}
}
virtual void visit(recruit_ptr){}
virtual void visit(recall_ptr){}
virtual void visit(suppose_dead_ptr){}
private:
std::set<std::size_t> move_owners_;
};
}
void manager::pre_draw()
{
if (can_modify_game_state() && has_actions() && unit_map_lock_.use_count() == 1) {
move_owners_finder move_finder;
for_each_action(std::ref(move_finder));
units_owning_moves_ = move_finder.get_units_owning_moves();
for (std::size_t unit_id : units_owning_moves_) {
unit_map::iterator unit_iter = resources::gameboard->units().find(unit_id);
if(unit_iter.valid()) {
ghost_owner_unit(&*unit_iter);
}
}
}
}
void manager::post_draw()
{
for (std::size_t unit_id : units_owning_moves_)
{
unit_map::iterator unit_iter = resources::gameboard->units().find(unit_id);
if (unit_iter.valid()) {
unghost_owner_unit(&*unit_iter);
}
}
units_owning_moves_.clear();
}
void manager::draw_hex(const map_location& hex)
{
/**
* IMPORTANT: none of the code in this method can call anything which would
* cause a hex to be invalidated (i.e. by calling in turn any variant of display::invalidate()).
* Doing so messes up the iterator currently going over the list of invalidated hexes to draw.
*/
if (!wait_for_side_init_ && has_actions())
{
//call draw() for all actions
for_each_action(std::bind(&action::draw_hex, std::placeholders::_1, hex));
//Info about the action numbers to be displayed on screen.
side_actions::numbers_t numbers;
for (team& t : resources::gameboard->teams())
{
side_actions& sa = *t.get_side_actions();
if(!sa.hidden())
sa.get_numbers(hex,numbers);
}
draw_numbers(hex,numbers); // helper fcn
}
}
void manager::on_mouseover_change(const map_location& hex)
{
map_location selected_hex = resources::controller->get_mouse_handler_base().get_selected_hex();
bool hex_has_unit;
{ wb::future_map future; // start planned unit map scope
hex_has_unit = resources::gameboard->units().find(selected_hex) != resources::gameboard->units().end();
} // end planned unit map scope
if (!((selected_hex.valid() && hex_has_unit)
|| has_temp_move() || wait_for_side_init_ || executing_actions_))
{
if (!highlighter_)
{
highlighter_.reset(new highlighter(viewer_actions()));
}
highlighter_->set_mouseover_hex(hex);
highlighter_->highlight();
}
}
void manager::on_gamestate_change()
{
DBG_WB << "Manager received gamestate change notification.";
// if on_gamestate_change() is called while the future unit map is applied,
// it means that the future unit map scope is used where it shouldn't be.
assert(!planned_unit_map_active_);
// Set mutated flag so action queue gets validated on next future map build
gamestate_mutated_ = true;
//Clear exclusive draws that might not get a chance to be cleared the normal way
display::get_singleton()->clear_exclusive_draws();
}
void manager::send_network_data()
{
std::size_t size = net_buffer_.size();
for(std::size_t team_index=0; team_index<size; ++team_index)
{
config& buf_cfg = net_buffer_[team_index];
if(buf_cfg.empty())
continue;
config packet;
config& wb_cfg = packet.add_child("whiteboard",buf_cfg);
wb_cfg["side"] = static_cast<int>(team_index+1);
wb_cfg["to_sides"] = resources::gameboard->teams().at(team_index).allied_human_teams();
buf_cfg.clear();
resources::controller->send_to_wesnothd(packet, "whiteboard");
std::size_t count = wb_cfg.child_count("net_cmd");
LOG_WB << "Side " << (team_index+1) << " sent wb data (" << count << " cmds).";
}
}
void manager::process_network_data(const config& cfg)
{
if(auto wb_cfg = cfg.optional_child("whiteboard"))
{
std::size_t count = wb_cfg->child_count("net_cmd");
LOG_WB << "Received wb data (" << count << ").";
team& team_from = resources::gameboard->get_team(wb_cfg["side"]);
for(const side_actions::net_cmd& cmd : wb_cfg->child_range("net_cmd"))
team_from.get_side_actions()->execute_net_cmd(cmd);
}
}
void manager::queue_net_cmd(std::size_t team_index, const side_actions::net_cmd& cmd)
{
assert(team_index < net_buffer_.size());
net_buffer_[team_index].add_child("net_cmd",cmd);
}
void manager::create_temp_move()
{
route_.reset();
/*
* CHECK PRE-CONDITIONS
* (This section has multiple return paths.)
*/
if ( !active_ || !can_modify_game_state() )
return;
const pathfind::marked_route& route =
resources::controller->get_mouse_handler_base().get_current_route();
if (route.steps.empty() || route.steps.size() < 2) return;
unit* temp_moved_unit =
future_visible_unit(resources::controller->get_mouse_handler_base().get_selected_hex(), viewer_side());
if (!temp_moved_unit) temp_moved_unit =
future_visible_unit(resources::controller->get_mouse_handler_base().get_last_hex(), viewer_side());
if (!temp_moved_unit) return;
if (temp_moved_unit->side() != display::get_singleton()->viewing_team().side()) return;
/*
* DONE CHECKING PRE-CONDITIONS, CREATE THE TEMP MOVE
* (This section has only one return path.)
*/
temp_move_unit_underlying_id_ = temp_moved_unit->underlying_id();
//@todo: May be appropriate to replace these separate components by a temporary
// wb::move object
route_.reset(new pathfind::marked_route(route));
//NOTE: route_->steps.back() = dst, and route_->steps.front() = src
std::size_t turn = 0;
std::vector<map_location>::iterator prev_itor = route.steps.begin();
std::vector<map_location>::iterator curr_itor = prev_itor;
std::vector<map_location>::iterator end_itor = route.steps.end();
for(; curr_itor!=end_itor; ++curr_itor)
{
const map_location& hex = *curr_itor;
//search for end-of-turn marks
pathfind::marked_route::mark_map::const_iterator w =
route.marks.find(hex);
if(w != route.marks.end() && w->second.turns > 0)
{
turn = w->second.turns-1;
if(turn >= move_arrows_.size())
move_arrows_.resize(turn+1);
if(turn >= fake_units_.size())
fake_units_.resize(turn+1);
arrow_ptr& move_arrow = move_arrows_[turn];
fake_unit_ptr& fake_unit = fake_units_[turn];
if(!move_arrow)
{
// Create temp arrow
move_arrow.reset(new arrow());
move_arrow->set_color(team::get_side_color_id(
viewer_side()));
move_arrow->set_style(arrow::STYLE_HIGHLIGHTED);
}
arrow_path_t path(prev_itor,curr_itor+1);
move_arrow->set_path(path);
if(path.size() >= 2)
{
// Bug #20299 demonstrates a situation where an incorrect fake/ghosted unit can be used.
// So before assuming that a pre-existing fake_unit can be re-used, check that its ID matches the unit being moved.
if(!fake_unit || fake_unit.get_unit_ptr()->id() != temp_moved_unit->id())
{
// Create temp ghost unit
fake_unit = fake_unit_ptr(temp_moved_unit->clone(), resources::fake_units);
fake_unit->anim_comp().set_ghosted(true);
}
unit_display::move_unit(path, fake_unit.get_unit_ptr(), false); //get facing right
fake_unit->anim_comp().invalidate(*game_display::get_singleton());
fake_unit->set_location(*curr_itor);
fake_unit->anim_comp().set_ghosted(true);
}
else //zero-hex path -- don't bother drawing a fake unit
fake_unit.reset();
prev_itor = curr_itor;
}
}
//in case path shortens on next step and one ghosted unit has to be removed
int ind = fake_units_.size() - 1;
fake_units_[ind]->anim_comp().invalidate(*game_display::get_singleton());
//toss out old arrows and fake units
move_arrows_.resize(turn+1);
fake_units_.resize(turn+1);
}
void manager::erase_temp_move()
{
move_arrows_.clear();
for(const fake_unit_ptr& tmp : fake_units_) {
if(tmp) {
tmp->anim_comp().invalidate(*game_display::get_singleton());
}
}
fake_units_.clear();
route_.reset();
temp_move_unit_underlying_id_ = 0;
}
void manager::save_temp_move()
{
if (has_temp_move() && !executing_actions_ && !resources::controller->is_linger_mode())
{
side_actions& sa = *viewer_actions();
unit* u = future_visible_unit(route_->steps.front());
assert(u);
std::size_t first_turn = sa.get_turn_num_of(*u);
validate_viewer_actions();
assert(move_arrows_.size() == fake_units_.size());
std::size_t size = move_arrows_.size();
for(std::size_t i=0; i<size; ++i)
{
arrow_ptr move_arrow = move_arrows_[i];
if(!arrow::valid_path(move_arrow->get_path()))
continue;
std::size_t turn = first_turn + i;
//@todo Using a marked_route here is wrong, since right now it's not marked
//either switch over to a plain route for planned moves, or mark it correctly
pathfind::marked_route route;
route.steps = move_arrow->get_path();
// path_cost() is incomplete as it for example doesn't handle skirmisher, we let the move action generate the costs on it own.
// route.move_cost = path_cost(route.steps,*u);
route.move_cost = -1;
sa.queue_move(turn, *u, route, move_arrow, std::move(fake_units_[i]));
}
erase_temp_move();
LOG_WB << *viewer_actions();
print_help_once();
}
}
unit_map::iterator manager::get_temp_move_unit() const
{
return resources::gameboard->units().find(temp_move_unit_underlying_id_);
}
void manager::save_temp_attack(const map_location& attacker_loc, const map_location& defender_loc, int weapon_choice)
{
if (active_ && !executing_actions_ && !resources::controller->is_linger_mode())
{
assert(weapon_choice >= 0);
arrow_ptr move_arrow;
fake_unit_ptr* fake_unit = nullptr;
map_location source_hex;
if (route_ && !route_->steps.empty())
{
//attack-move
assert(move_arrows_.size() == 1);
assert(fake_units_.size() == 1);
move_arrow = move_arrows_.front();
fake_unit = &fake_units_.front();
assert(route_->steps.back() == attacker_loc);
source_hex = route_->steps.front();
(**fake_unit).anim_comp().set_disabled_ghosted(true);
}
else
{
//simple attack
move_arrow.reset(new arrow);
source_hex = attacker_loc;
route_.reset(new pathfind::marked_route);
// We'll pass as parameter a one-hex route with no marks.
route_->steps.push_back(attacker_loc);
}
unit* attacking_unit = future_visible_unit(source_hex);
assert(attacking_unit);
validate_viewer_actions();
side_actions& sa = *viewer_actions();
sa.queue_attack(sa.get_turn_num_of(*attacking_unit), *attacking_unit, defender_loc, weapon_choice, *route_, move_arrow, fake_unit ? std::move(*fake_unit) : fake_unit_ptr());
print_help_once();
display::get_singleton()->invalidate(defender_loc);
display::get_singleton()->invalidate(attacker_loc);
erase_temp_move();
LOG_WB << *viewer_actions();
}
}
bool manager::save_recruit(const std::string& name, int side_num, const map_location& recruit_hex)
{
bool created_planned_recruit = false;
if (active_ && !executing_actions_ && !resources::controller->is_linger_mode()) {
if (side_num != display::get_singleton()->viewing_team().side())
{
LOG_WB <<"manager::save_recruit called for a different side than viewing side.";
created_planned_recruit = false;
}
else
{
side_actions& sa = *viewer_actions();
unit* recruiter;
{ wb::future_map raii;
recruiter = find_recruiter(side_num-1,recruit_hex);
} // end planned unit map scope
assert(recruiter);
std::size_t turn = sa.get_turn_num_of(*recruiter);
sa.queue_recruit(turn,name,recruit_hex);
created_planned_recruit = true;
print_help_once();
}
}
return created_planned_recruit;
}
bool manager::save_recall(const unit& unit, int side_num, const map_location& recall_hex)
{
bool created_planned_recall = false;
if (active_ && !executing_actions_ && !resources::controller->is_linger_mode())
{
if (side_num != display::get_singleton()->viewing_team().side())
{
LOG_WB <<"manager::save_recall called for a different side than viewing side.";
created_planned_recall = false;
}
else
{
side_actions& sa = *viewer_actions();
std::size_t turn = sa.num_turns();
if(turn > 0)
--turn;
sa.queue_recall(turn,unit,recall_hex);
created_planned_recall = true;
print_help_once();
}
}
return created_planned_recall;
}
void manager::save_suppose_dead(unit& curr_unit, const map_location& loc)
{
if(active_ && !executing_actions_ && !resources::controller->is_linger_mode())
{
validate_viewer_actions();
side_actions& sa = *viewer_actions();
sa.queue_suppose_dead(sa.get_turn_num_of(curr_unit),curr_unit,loc);
}
}
void manager::contextual_execute()
{
validate_viewer_actions();
if (can_enable_execution_hotkeys())
{
erase_temp_move();
//For exception-safety, this struct sets executing_actions_ to false on destruction.
variable_finalizer<bool> finally(executing_actions_, false);
side_actions::iterator it = viewer_actions()->end();
unit const* selected_unit = future_visible_unit(resources::controller->get_mouse_handler_base().get_selected_hex(), viewer_side());
auto check_action = [&](side_actions::iterator i) {
it = i;
return it != viewer_actions()->end() && it < viewer_actions()->turn_end(0);
};
if (selected_unit && check_action(viewer_actions()->find_first_action_of(*selected_unit)))
{
executing_actions_ = true;
viewer_actions()->execute(it);
}
else if (highlighter_ && check_action(viewer_actions()->get_position_of(highlighter_->get_execute_target())))
{
executing_actions_ = true;
viewer_actions()->execute(it);
}
else //we already check above for viewer_actions()->empty()
{
executing_actions_ = true;
viewer_actions()->execute_next();
}
} //Finalizer struct sets executing_actions_ to false
}
bool manager::allow_end_turn()
{
preparing_to_end_turn_ = true;
return execute_all_actions();
}
bool manager::execute_all_actions()
{
if (has_planned_unit_map())
{
ERR_WB << "Modifying action queue while temp modifiers are applied1!!!";
}
//exception-safety: finalizers set variables to false on destruction
//i.e. when method exits naturally or exception is thrown
variable_finalizer<bool> finalize_executing_actions(executing_actions_, false);
variable_finalizer<bool> finalize_executing_all_actions(executing_all_actions_, false);
validate_viewer_actions();
if(viewer_actions()->empty() || viewer_actions()->turn_size(0) == 0)
{
//No actions to execute, job done.
return true;
}
assert(can_enable_execution_hotkeys());
erase_temp_move();
// Build unit map once to ensure spent gold and other calculations are refreshed
set_planned_unit_map();
assert(has_planned_unit_map());
set_real_unit_map();
executing_actions_ = true;
executing_all_actions_ = true;
side_actions_ptr sa = viewer_actions();
if (has_planned_unit_map())
{
ERR_WB << "Modifying action queue while temp modifiers are applied!!!";
}
//LOG_WB << "Before executing all actions, " << *sa;
while (sa->turn_begin(0) != sa->turn_end(0))
{
bool action_successful = sa->execute(sa->begin());
// Interrupt on incomplete action
if (!action_successful)
{
return false;
}
}
return true;
}
void manager::contextual_delete()
{
validate_viewer_actions();
if(can_enable_modifier_hotkeys()) {
erase_temp_move();
action_ptr action;
side_actions::iterator it = viewer_actions()->end();
unit const* selected_unit = future_visible_unit(resources::controller->get_mouse_handler_base().get_selected_hex(), viewer_side());
if(selected_unit && (it = viewer_actions()->find_last_action_of(*selected_unit)) != viewer_actions()->end()) {
viewer_actions()->remove_action(it);
// TODO: Shouldn't we probably deselect the unit at this point?
} else if(highlighter_ && (action = highlighter_->get_delete_target()) && (it = viewer_actions()->get_position_of(action)) != viewer_actions()->end()) {
viewer_actions()->remove_action(it);
validate_viewer_actions();
highlighter_->set_mouseover_hex(highlighter_->get_mouseover_hex());
highlighter_->highlight();
} else { //we already check above for viewer_actions()->empty()
it = (viewer_actions()->end() - 1);
action = *it;
viewer_actions()->remove_action(it);
validate_viewer_actions();
}
}
}
void manager::contextual_bump_up_action()
{
validate_viewer_actions();
if(can_enable_reorder_hotkeys()) {
action_ptr action = highlighter_->get_bump_target();
if(action) {
viewer_actions()->bump_earlier(viewer_actions()->get_position_of(action));
validate_viewer_actions(); // Redraw arrows
}
}
}
void manager::contextual_bump_down_action()
{
validate_viewer_actions();
if(can_enable_reorder_hotkeys()) {
action_ptr action = highlighter_->get_bump_target();
if(action) {
viewer_actions()->bump_later(viewer_actions()->get_position_of(action));
validate_viewer_actions(); // Redraw arrows
}
}
}
bool manager::has_actions() const
{
assert(resources::gameboard);
return wb::has_actions();
}
bool manager::unit_has_actions(unit const* unit) const
{
assert(unit != nullptr);
assert(resources::gameboard);
return viewer_actions()->unit_has_actions(*unit);
}
int manager::get_spent_gold_for(int side)
{
if(wait_for_side_init_)
return 0;
return resources::gameboard->get_team(side).get_side_actions()->get_gold_spent();
}
bool manager::should_clear_undo() const
{
return resources::controller->is_networked_mp() && resources::controller->current_team().is_local();
}
void manager::options_dlg()
{
int v_side = viewer_side();
int selection = 0;
std::vector<team*> allies;
std::vector<std::string> options;
utils::string_map t_vars;
options.emplace_back(_("SHOW ALL allies plans"));
options.emplace_back(_("HIDE ALL allies plans"));
//populate list of networked allies
for(team &t : resources::gameboard->teams())
{
//Exclude enemies, AIs, and local players
if(t.is_enemy(v_side) || !t.is_network())
continue;
allies.push_back(&t);
t_vars["player"] = t.current_player();
std::size_t t_index = t.side()-1;
if(team_plans_hidden_[t_index])
options.emplace_back(VGETTEXT("Show plans for $player", t_vars));
else
options.emplace_back(VGETTEXT("Hide plans for $player", t_vars));
}
gui2::dialogs::simple_item_selector dlg("", _("Whiteboard Options"), options);
dlg.show();
selection = dlg.selected_index();
if(selection == -1)
return;
switch(selection)
{
case 0:
for(team* t : allies) {
team_plans_hidden_[t->side()-1]=false;
}
break;
case 1:
for(team* t : allies) {
team_plans_hidden_[t->side()-1]=true;
}
break;
default:
if(selection > 1)
{
std::size_t t_index = allies[selection-2]->side()-1;
//toggle ...
bool hidden = team_plans_hidden_[t_index];
team_plans_hidden_[t_index] = !hidden;
}
break;
}
update_plan_hiding();
}
void manager::set_planned_unit_map()
{
if (!can_modify_game_state()) {
LOG_WB << "Not building planned unit map: cannot modify game state now.";
return;
}
//any more than one reference means a lock on unit map was requested
if(unit_map_lock_.use_count() != 1) {
LOG_WB << "Not building planned unit map: unit map locked.";
return;
}
if (planned_unit_map_active_) {
WRN_WB << "Not building planned unit map: already set.";
return;
}
log_scope2(log_whiteboard, "Building planned unit map");
mapbuilder_.reset(new mapbuilder(resources::gameboard->units()));
mapbuilder_->build_map();
planned_unit_map_active_ = true;
}
void manager::set_real_unit_map()
{
if (planned_unit_map_active_)
{
assert(!executing_actions_);
assert(!wait_for_side_init_);
if(mapbuilder_)
{
log_scope2(log_whiteboard, "Restoring regular unit map.");
mapbuilder_.reset();
}
planned_unit_map_active_ = false;
}
else
{
LOG_WB << "Not disabling planned unit map: already disabled.";
}
}
void manager::validate_actions_if_needed()
{
if (gamestate_mutated_) {
validate_viewer_actions();
}
}
future_map::future_map():
initial_planned_unit_map_(resources::whiteboard && resources::whiteboard->has_planned_unit_map())
{
if (!resources::whiteboard)
return;
if (!initial_planned_unit_map_)
resources::whiteboard->set_planned_unit_map();
// check if if unit map was successfully applied
if (!resources::whiteboard->has_planned_unit_map()) {
DBG_WB << "Scoped future unit map failed to apply.";
}
}
future_map::~future_map()
{
try {
if (!resources::whiteboard)
return;
if (!initial_planned_unit_map_ && resources::whiteboard->has_planned_unit_map())
resources::whiteboard->set_real_unit_map();
} catch (...) {}
}
future_map_if_active::future_map_if_active():
initial_planned_unit_map_(resources::whiteboard && resources::whiteboard->has_planned_unit_map()),
whiteboard_active_(resources::whiteboard && resources::whiteboard->is_active())
{
if (!resources::whiteboard)
return;
if (!whiteboard_active_)
return;
if (!initial_planned_unit_map_)
resources::whiteboard->set_planned_unit_map();
// check if if unit map was successfully applied
if (!resources::whiteboard->has_planned_unit_map()) {
DBG_WB << "Scoped future unit map failed to apply.";
}
}
future_map_if_active::~future_map_if_active()
{
try {
if (!resources::whiteboard)
return;
if (!initial_planned_unit_map_ && resources::whiteboard->has_planned_unit_map())
resources::whiteboard->set_real_unit_map();
} catch (...) {}
}
real_map::real_map():
initial_planned_unit_map_(resources::whiteboard && resources::whiteboard->has_planned_unit_map()),
unit_map_lock_(resources::whiteboard ? resources::whiteboard->unit_map_lock_ : std::make_shared<bool>(false))
{
if (!resources::whiteboard)
return;
if (initial_planned_unit_map_)
resources::whiteboard->set_real_unit_map();
}
real_map::~real_map()
{
if (!resources::whiteboard)
return;
assert(!resources::whiteboard->has_planned_unit_map());
if (initial_planned_unit_map_)
{
resources::whiteboard->set_planned_unit_map();
}
}
} // end namespace wb