1 /*
2 Copyright (C) 2010 - 2018 by Gabriel Morin <gabrielmorin (at) gmail (dot) com>
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 /**
16 * @file
17 */
18
19 #include "whiteboard/manager.hpp"
20
21 #include "whiteboard/action.hpp"
22 #include "whiteboard/highlighter.hpp"
23 #include "whiteboard/mapbuilder.hpp"
24 #include "whiteboard/move.hpp"
25 #include "whiteboard/attack.hpp"
26 #include "whiteboard/recall.hpp"
27 #include "whiteboard/recruit.hpp"
28 #include "whiteboard/side_actions.hpp"
29 #include "whiteboard/utility.hpp"
30
31 #include "actions/create.hpp"
32 #include "actions/undo.hpp"
33 #include "arrow.hpp"
34 #include "chat_events.hpp"
35 #include "fake_unit_manager.hpp"
36 #include "fake_unit_ptr.hpp"
37 #include "formula/string_utils.hpp"
38 #include "game_board.hpp"
39 #include "preferences/game.hpp"
40 #include "game_state.hpp"
41 #include "gettext.hpp"
42 #include "gui/dialogs/simple_item_selector.hpp"
43 #include "key.hpp"
44 #include "pathfind/pathfind.hpp"
45 #include "play_controller.hpp"
46 #include "replay_helper.hpp"
47 #include "resources.hpp"
48 #include "synced_context.hpp"
49 #include "team.hpp"
50 #include "units/unit.hpp"
51 #include "units/animation_component.hpp"
52 #include "units/udisplay.hpp"
53
54 #include "utils/functional.hpp"
55
56 #include <sstream>
57
58 namespace wb {
59
manager()60 manager::manager():
61 active_(false),
62 inverted_behavior_(false),
63 self_activate_once_(true),
64 #if 0
65 print_help_once_(true),
66 #endif
67 wait_for_side_init_(true),
68 planned_unit_map_active_(false),
69 executing_actions_(false),
70 executing_all_actions_(false),
71 preparing_to_end_turn_(false),
72 gamestate_mutated_(false),
73 activation_state_lock_(new bool),
74 unit_map_lock_(new bool),
75 mapbuilder_(),
76 highlighter_(),
77 route_(),
78 move_arrows_(),
79 fake_units_(),
80 temp_move_unit_underlying_id_(0),
81 key_poller_(new CKey),
82 hidden_unit_hexes_(),
83 net_buffer_(resources::gameboard->teams().size()),
84 team_plans_hidden_(resources::gameboard->teams().size()),
85 units_owning_moves_()
86 {
87 if(preferences::hide_whiteboard()) {
88 team_plans_hidden_.flip();
89 }
90 LOG_WB << "Manager initialized.\n";
91 }
92
~manager()93 manager::~manager()
94 {
95 LOG_WB << "Manager destroyed.\n";
96 }
97
98 //Used for chat-spamming debug info
99 #if 0
100 static void print_to_chat(const std::string& title, const std::string& message)
101 {
102 display::get_singleton()->add_chat_message(time(nullptr), title, 0, message,
103 events::chat_handler::MESSAGE_PRIVATE, false);
104 }
105 #endif
106
print_help_once()107 void manager::print_help_once()
108 {
109 #if 0
110 if (!print_help_once_)
111 return;
112 else
113 print_help_once_ = false;
114
115 print_to_chat("whiteboard", std::string("Type :wb to activate/deactivate planning mode.")
116 + " Hold TAB to temporarily deactivate/activate it.");
117 std::stringstream hotkeys;
118 const hotkey::hotkey_item& hk_execute = hotkey::get_hotkey(hotkey::HOTKEY_WB_EXECUTE_ACTION);
119 if(!hk_execute.null()) {
120 //print_to_chat("[execute action]", "'" + hk_execute.get_name() + "'");
121 hotkeys << "Execute: " << hk_execute.get_name() << ", ";
122 }
123 const hotkey::hotkey_item& hk_execute_all = hotkey::get_hotkey(hotkey::HOTKEY_WB_EXECUTE_ALL_ACTIONS);
124 if(!hk_execute_all.null()) {
125 //print_to_chat("[execute action]", "'" + hk_execute_all.get_name() + "'");
126 hotkeys << "Execute all: " << hk_execute_all.get_name() << ", ";
127 }
128 const hotkey::hotkey_item& hk_delete = hotkey::get_hotkey(hotkey::HOTKEY_WB_DELETE_ACTION);
129 if(!hk_delete.null()) {
130 //print_to_chat("[delete action]", "'" + hk_delete.get_name() + "'");
131 hotkeys << "Delete: " << hk_delete.get_name() << ", ";
132 }
133 const hotkey::hotkey_item& hk_bump_up = hotkey::get_hotkey(hotkey::HOTKEY_WB_BUMP_UP_ACTION);
134 if(!hk_bump_up.null()) {
135 //print_to_chat("[move action earlier in queue]", "'" + hk_bump_up.get_name() + "'");
136 hotkeys << "Move earlier: " << hk_bump_up.get_name() << ", ";
137 }
138 const hotkey::hotkey_item& hk_bump_down = hotkey::get_hotkey(hotkey::HOTKEY_WB_BUMP_DOWN_ACTION);
139 if(!hk_bump_down.null()) {
140 //print_to_chat("[move action later in queue]", "'" + hk_bump_down.get_name() + "'");
141 hotkeys << "Move later: " << hk_bump_down.get_name() << ", ";
142 }
143 print_to_chat("HOTKEYS:", hotkeys.str() + "\n");
144 #endif
145 }
146
can_modify_game_state() const147 bool manager::can_modify_game_state() const
148 {
149 if(wait_for_side_init_
150 || resources::gameboard == nullptr
151 || executing_actions_
152 || resources::gameboard->is_observer()
153 || resources::controller->is_linger_mode())
154 {
155 return false;
156 }
157 else
158 {
159 return true;
160 }
161 }
162
can_activate() const163 bool manager::can_activate() const
164 {
165 //any more than one reference means a lock on whiteboard state was requested
166 if(!activation_state_lock_.unique())
167 return false;
168
169 return can_modify_game_state();
170 }
171
set_active(bool active)172 void manager::set_active(bool active)
173 {
174 if(!can_activate())
175 {
176 active_ = false;
177 LOG_WB << "Whiteboard can't be activated now.\n";
178 }
179 else if (active != active_)
180 {
181 active_ = active;
182 erase_temp_move();
183
184 if (active_)
185 {
186 if(should_clear_undo()) {
187 if(!resources::controller->current_team().auto_shroud_updates()) {
188 synced_context::run_and_throw("update_shroud", replay_helper::get_update_shroud());
189 synced_context::run_and_throw("auto_shroud", replay_helper::get_auto_shroud(true));
190 }
191 resources::undo_stack->clear();
192 }
193 validate_viewer_actions();
194 LOG_WB << "Whiteboard activated! " << *viewer_actions() << "\n";
195 create_temp_move();
196 } else {
197 LOG_WB << "Whiteboard deactivated!\n";
198 }
199 }
200 }
201
set_invert_behavior(bool invert)202 void manager::set_invert_behavior(bool invert)
203 {
204 //any more than one reference means a lock on whiteboard state was requested
205 if(!activation_state_lock_.unique())
206 return;
207
208 bool block_whiteboard_activation = false;
209 if(!can_activate())
210 {
211 block_whiteboard_activation = true;
212 }
213
214 if (invert)
215 {
216 if (!inverted_behavior_)
217 {
218 if (active_)
219 {
220 DBG_WB << "Whiteboard deactivated temporarily.\n";
221 inverted_behavior_ = true;
222 set_active(false);
223 }
224 else if (!block_whiteboard_activation)
225 {
226 DBG_WB << "Whiteboard activated temporarily.\n";
227 inverted_behavior_ = true;
228 set_active(true);
229 }
230 }
231 }
232 else
233 {
234 if (inverted_behavior_)
235 {
236 if (active_)
237 {
238 DBG_WB << "Whiteboard set back to deactivated status.\n";
239 inverted_behavior_ = false;
240 set_active(false);
241 }
242 else if (!block_whiteboard_activation)
243 {
244 DBG_WB << "Whiteboard set back to activated status.\n";
245 inverted_behavior_ = false;
246 set_active(true);
247 }
248 }
249 }
250 }
251
can_enable_execution_hotkeys() const252 bool manager::can_enable_execution_hotkeys() const
253 {
254 return can_enable_modifier_hotkeys() && viewer_side() == resources::controller->current_side()
255 && viewer_actions()->turn_size(0) > 0;
256 }
257
can_enable_modifier_hotkeys() const258 bool manager::can_enable_modifier_hotkeys() const
259 {
260 return can_modify_game_state() && !viewer_actions()->empty();
261 }
262
can_enable_reorder_hotkeys() const263 bool manager::can_enable_reorder_hotkeys() const
264 {
265 return can_enable_modifier_hotkeys() && highlighter_ && highlighter_->get_bump_target();
266 }
267
allow_leader_to_move(const unit & leader) const268 bool manager::allow_leader_to_move(const unit& leader) const
269 {
270 if(!has_actions())
271 return true;
272
273 //Look for another leader on another keep in the same castle
274 { wb::future_map future; // start planned unit map scope
275
276 // TODO: when the game executes all whiteboard moves at turn end applying the future map
277 // will fail because we are currently executing actions, and if one of those actions
278 // was a movement of the leader this function will be called, resulting the the error
279 // mesage below, we silence that message for now by adding (!executing_actions_)
280 //
281 // Also this check is generally flawed, for example it could happen that the leader found
282 // by find_backup_leader would be moved to that location _after_ the unit would be recruited
283 // It could also happen that the original leader can be moved back to that location before
284 // the unit is recruited.
285 if(!has_planned_unit_map() && !executing_actions_) {
286 WRN_WB << "Unable to build future map to determine whether leader's allowed to move." << std::endl;
287 }
288 if(find_backup_leader(leader))
289 return true;
290 } // end planned unit map scope
291
292 if(viewer_actions()->empty()) {
293 return true;
294 }
295
296 //Look for planned recruits that depend on this leader
297 for(action_const_ptr action : *viewer_actions())
298 {
299 recruit_const_ptr recruit = std::dynamic_pointer_cast<class recruit const>(action);
300 recall_const_ptr recall = std::dynamic_pointer_cast<class recall const>(action);
301 if(recruit || recall)
302 {
303 map_location const target_hex = recruit?recruit->get_recruit_hex():recall->get_recall_hex();
304 if (dynamic_cast<game_state&>(*resources::filter_con).can_recruit_on(leader, target_hex))
305 return false;
306 }
307 }
308 return true;
309 }
310
on_init_side()311 void manager::on_init_side()
312 {
313 //Turn should never start with action auto-execution already enabled!
314 assert(!executing_all_actions_ && !executing_actions_);
315
316 update_plan_hiding(); /* validates actions */
317 wait_for_side_init_ = false;
318 LOG_WB << "on_init_side()\n";
319
320 if (self_activate_once_ && preferences::enable_whiteboard_mode_on_start())
321 {
322 self_activate_once_ = false;
323 set_active(true);
324 }
325 }
326
on_finish_side_turn(int side)327 void manager::on_finish_side_turn(int side)
328 {
329 preparing_to_end_turn_ = false;
330 wait_for_side_init_ = true;
331 if(side == viewer_side() && !viewer_actions()->empty()) {
332 viewer_actions()->synced_turn_shift();
333 }
334 highlighter_.reset();
335 erase_temp_move();
336 LOG_WB << "on_finish_side_turn()\n";
337 }
338
pre_delete_action(action_ptr)339 void manager::pre_delete_action(action_ptr)
340 {
341 }
342
post_delete_action(action_ptr action)343 void manager::post_delete_action(action_ptr action)
344 {
345 // The fake unit representing the destination of a chain of planned moves should have the regular animation.
346 // If the last remaining action of the unit that owned this move is a move as well,
347 // adjust its appearance accordingly.
348
349 side_actions_ptr side_actions = resources::gameboard->teams().at(action->team_index()).get_side_actions();
350
351 unit_ptr actor = action->get_unit();
352 if(actor) { // The unit might have died following the execution of an attack
353 side_actions::iterator action_it = side_actions->find_last_action_of(*actor);
354 if(action_it != side_actions->end()) {
355 move_ptr move = std::dynamic_pointer_cast<class move>(*action_it);
356 if(move && move->get_fake_unit()) {
357 move->get_fake_unit()->anim_comp().set_standing(true);
358 }
359 }
360 }
361 }
362
hide_all_plans()363 static void hide_all_plans()
364 {
365 for(team& t : resources::gameboard->teams()){
366 t.get_side_actions()->hide();
367 }
368 }
369
370 /* private */
update_plan_hiding(size_t team_index)371 void manager::update_plan_hiding(size_t team_index)
372 {
373 //We don't control the "viewing" side ... we're probably an observer
374 if(!resources::gameboard->teams().at(team_index).is_local_human())
375 hide_all_plans();
376 else // normal circumstance
377 {
378 for(team& t : resources::gameboard->teams())
379 {
380 //make sure only appropriate teams are hidden
381 if(!t.is_network_human())
382 team_plans_hidden_[t.side()-1] = false;
383
384 if(t.is_enemy(team_index+1) || team_plans_hidden_[t.side()-1])
385 t.get_side_actions()->hide();
386 else
387 t.get_side_actions()->show();
388 }
389 }
390 validate_viewer_actions();
391 }
update_plan_hiding()392 void manager::update_plan_hiding()
393 {update_plan_hiding(viewer_team());}
394
on_viewer_change(size_t team_index)395 void manager::on_viewer_change(size_t team_index)
396 {
397 if(!wait_for_side_init_)
398 update_plan_hiding(team_index);
399 }
400
on_change_controller(int side,const team & t)401 void manager::on_change_controller(int side, const team& t)
402 {
403 wb::side_actions& sa = *t.get_side_actions();
404 if(t.is_local_human()) // we own this side now
405 {
406 //tell everyone to clear this side's actions -- we're starting anew
407 queue_net_cmd(sa.team_index(),sa.make_net_cmd_clear());
408 sa.clear();
409 //refresh the hidden_ attribute of every team's side_actions
410 update_plan_hiding();
411 }
412 else if(t.is_local_ai() || t.is_network_ai()) // no one owns this side anymore
413 sa.clear(); // clear its plans away -- the ai doesn't plan ... yet
414 else if(t.is_network()) // Another client is taking control of the side
415 {
416 if(side==viewer_side()) // They're taking OUR side away!
417 hide_all_plans(); // give up knowledge of everyone's plans, in case we became an observer
418
419 //tell them our plans -- they may not have received them up to this point
420 size_t num_teams = resources::gameboard->teams().size();
421 for(size_t i=0; i<num_teams; ++i)
422 {
423 team& local_team = resources::gameboard->teams().at(i);
424 if(local_team.is_local_human() && !local_team.is_enemy(side))
425 queue_net_cmd(i,local_team.get_side_actions()->make_net_cmd_refresh());
426 }
427 }
428 }
429
on_kill_unit()430 void manager::on_kill_unit()
431 {
432 if(highlighter_ != nullptr) {
433 highlighter_->set_selection_candidate(unit_ptr());
434 }
435 }
436
current_side_has_actions()437 bool manager::current_side_has_actions()
438 {
439 if(current_side_actions()->empty()) {
440 return false;
441 }
442
443 side_actions::range_t range = current_side_actions()->iter_turn(0);
444 return range.first != range.second; //non-empty range
445 }
446
validate_viewer_actions()447 void manager::validate_viewer_actions()
448 {
449 LOG_WB << "'gamestate_mutated_' flag dirty, validating actions.\n";
450 gamestate_mutated_ = false;
451 if(has_planned_unit_map()) {
452 real_map();
453 } else {
454 future_map();
455 }
456 }
457
458 //helper fcn
draw_numbers(const map_location & hex,side_actions::numbers_t numbers)459 static void draw_numbers(const map_location& hex, side_actions::numbers_t numbers)
460 {
461 std::vector<int>& numbers_to_draw = numbers.numbers_to_draw;
462 std::vector<size_t>& team_numbers = numbers.team_numbers;
463 int& main_number = numbers.main_number;
464 std::set<size_t>& secondary_numbers = numbers.secondary_numbers;
465
466 const double x_offset_base = 0.0;
467 const double y_offset_base = 0.2;
468 //position 0,0 in the hex is the upper left corner
469 //0.8 = horizontal coord., close to the right side of the hex
470 const double x_origin = 0.8 - numbers_to_draw.size() * x_offset_base;
471 //0.5 = halfway in the hex vertically
472 const double y_origin = 0.5 - numbers_to_draw.size() * (y_offset_base / 2);
473 double x_offset = 0, y_offset = 0;
474
475 size_t size = numbers_to_draw.size();
476 for(size_t i=0; i<size; ++i)
477 {
478 int number = numbers_to_draw[i];
479
480 std::string number_text = std::to_string(number);
481 size_t font_size;
482 if (static_cast<int>(i) == main_number) {
483 font_size = 19;
484 }
485 else if (secondary_numbers.find(i)!=secondary_numbers.end()) {
486 font_size = 17;
487 }
488 else {
489 font_size = 15;
490 }
491
492 color_t color = team::get_side_color(static_cast<int>(team_numbers[i]+1));
493 const double x_in_hex = x_origin + x_offset;
494 const double y_in_hex = y_origin + y_offset;
495 display::get_singleton()->draw_text_in_hex(hex, display::LAYER_ACTIONS_NUMBERING,
496 number_text, font_size, color, x_in_hex, y_in_hex);
497 x_offset += x_offset_base;
498 y_offset += y_offset_base;
499 }
500 }
501
502
503 namespace
504 {
505 //Helper struct that finds all units teams whose planned actions are currently visible
506 //Only used by manager::pre_draw().
507 //Note that this structure is used as a functor.
508 struct move_owners_finder: public visitor
509 {
510
511 public:
move_owners_finderwb::__anon9bf82fd60111::move_owners_finder512 move_owners_finder(): move_owners_() { }
513
operator ()wb::__anon9bf82fd60111::move_owners_finder514 void operator()(action* action) {
515 action->accept(*this);
516 }
517
get_units_owning_moveswb::__anon9bf82fd60111::move_owners_finder518 const std::set<size_t>& get_units_owning_moves() {
519 return move_owners_;
520 }
521
visitwb::__anon9bf82fd60111::move_owners_finder522 virtual void visit(move_ptr move) {
523 if(size_t id = move->get_unit_id()) {
524 move_owners_.insert(id);
525 }
526 }
527
visitwb::__anon9bf82fd60111::move_owners_finder528 virtual void visit(attack_ptr attack) {
529 //also add attacks if they have an associated move
530 if(attack->get_route().steps.size() >= 2) {
531 if(size_t id = attack->get_unit_id()) {
532 move_owners_.insert(id);
533 }
534 }
535 }
visitwb::__anon9bf82fd60111::move_owners_finder536 virtual void visit(recruit_ptr){}
visitwb::__anon9bf82fd60111::move_owners_finder537 virtual void visit(recall_ptr){}
visitwb::__anon9bf82fd60111::move_owners_finder538 virtual void visit(suppose_dead_ptr){}
539
540 private:
541 std::set<size_t> move_owners_;
542 };
543 }
544
pre_draw()545 void manager::pre_draw()
546 {
547 if (can_modify_game_state() && has_actions() && unit_map_lock_.unique()) {
548 move_owners_finder move_finder;
549 for_each_action(std::ref(move_finder));
550 units_owning_moves_ = move_finder.get_units_owning_moves();
551
552 for (size_t unit_id : units_owning_moves_) {
553 unit_map::iterator unit_iter = resources::gameboard->units().find(unit_id);
554 if(unit_iter.valid()) {
555 ghost_owner_unit(&*unit_iter);
556 }
557 }
558 }
559 }
560
post_draw()561 void manager::post_draw()
562 {
563 for (size_t unit_id : units_owning_moves_)
564 {
565 unit_map::iterator unit_iter = resources::gameboard->units().find(unit_id);
566 if (unit_iter.valid()) {
567 unghost_owner_unit(&*unit_iter);
568 }
569 }
570 units_owning_moves_.clear();
571 }
572
draw_hex(const map_location & hex)573 void manager::draw_hex(const map_location& hex)
574 {
575 /**
576 * IMPORTANT: none of the code in this method can call anything which would
577 * cause a hex to be invalidated (i.e. by calling in turn any variant of display::invalidate()).
578 * Doing so messes up the iterator currently going over the list of invalidated hexes to draw.
579 */
580
581 if (!wait_for_side_init_ && has_actions())
582 {
583 //call draw() for all actions
584 for_each_action(std::bind(&action::draw_hex, std::placeholders::_1, hex));
585
586 //Info about the action numbers to be displayed on screen.
587 side_actions::numbers_t numbers;
588 for (team& t : resources::gameboard->teams())
589 {
590 side_actions& sa = *t.get_side_actions();
591 if(!sa.hidden())
592 sa.get_numbers(hex,numbers);
593 }
594 draw_numbers(hex,numbers); // helper fcn
595 }
596
597 }
598
on_mouseover_change(const map_location & hex)599 void manager::on_mouseover_change(const map_location& hex)
600 {
601
602 map_location selected_hex = resources::controller->get_mouse_handler_base().get_selected_hex();
603 bool hex_has_unit;
604 { wb::future_map future; // start planned unit map scope
605 hex_has_unit = resources::gameboard->units().find(selected_hex) != resources::gameboard->units().end();
606 } // end planned unit map scope
607 if (!((selected_hex.valid() && hex_has_unit)
608 || has_temp_move() || wait_for_side_init_ || executing_actions_))
609 {
610 if (!highlighter_)
611 {
612 highlighter_.reset(new highlighter(viewer_actions()));
613 }
614 highlighter_->set_mouseover_hex(hex);
615 highlighter_->highlight();
616 }
617 }
618
on_gamestate_change()619 void manager::on_gamestate_change()
620 {
621 DBG_WB << "Manager received gamestate change notification.\n";
622 // if on_gamestate_change() is called while the future unit map is applied,
623 // it means that the future unit map scope is used where it shouldn't be.
624 assert(!planned_unit_map_active_);
625 // Set mutated flag so action queue gets validated on next future map build
626 gamestate_mutated_ = true;
627 //Clear exclusive draws that might not get a chance to be cleared the normal way
628 display::get_singleton()->clear_exclusive_draws();
629 }
630
send_network_data()631 void manager::send_network_data()
632 {
633 size_t size = net_buffer_.size();
634 for(size_t team_index=0; team_index<size; ++team_index)
635 {
636 config& buf_cfg = net_buffer_[team_index];
637
638 if(buf_cfg.empty())
639 continue;
640
641 config packet;
642 config& wb_cfg = packet.add_child("whiteboard",buf_cfg);
643 wb_cfg["side"] = static_cast<int>(team_index+1);
644 wb_cfg["to_sides"] = resources::gameboard->teams().at(team_index).allied_human_teams();
645
646 buf_cfg.clear();
647
648 resources::controller->send_to_wesnothd(packet, "whiteboard");
649
650 size_t count = wb_cfg.child_count("net_cmd");
651 LOG_WB << "Side " << (team_index+1) << " sent wb data (" << count << " cmds).\n";
652 }
653 }
654
process_network_data(const config & cfg)655 void manager::process_network_data(const config& cfg)
656 {
657 if(const config& wb_cfg = cfg.child("whiteboard"))
658 {
659 size_t count = wb_cfg.child_count("net_cmd");
660 LOG_WB << "Received wb data (" << count << ").\n";
661
662 team& team_from = resources::gameboard->get_team(wb_cfg["side"]);
663 for(const side_actions::net_cmd& cmd : wb_cfg.child_range("net_cmd"))
664 team_from.get_side_actions()->execute_net_cmd(cmd);
665 }
666 }
667
queue_net_cmd(size_t team_index,const side_actions::net_cmd & cmd)668 void manager::queue_net_cmd(size_t team_index, const side_actions::net_cmd& cmd)
669 {
670 assert(team_index < net_buffer_.size());
671 net_buffer_[team_index].add_child("net_cmd",cmd);
672 }
673
create_temp_move()674 void manager::create_temp_move()
675 {
676 route_.reset();
677
678 /*
679 * CHECK PRE-CONDITIONS
680 * (This section has multiple return paths.)
681 */
682
683 if ( !active_ || !can_modify_game_state() )
684 return;
685
686 const pathfind::marked_route& route =
687 resources::controller->get_mouse_handler_base().get_current_route();
688
689 if (route.steps.empty() || route.steps.size() < 2) return;
690
691 unit* temp_moved_unit =
692 future_visible_unit(resources::controller->get_mouse_handler_base().get_selected_hex(), viewer_side());
693 if (!temp_moved_unit) temp_moved_unit =
694 future_visible_unit(resources::controller->get_mouse_handler_base().get_last_hex(), viewer_side());
695 if (!temp_moved_unit) return;
696 if (temp_moved_unit->side() != display::get_singleton()->viewing_side()) return;
697
698 /*
699 * DONE CHECKING PRE-CONDITIONS, CREATE THE TEMP MOVE
700 * (This section has only one return path.)
701 */
702
703 temp_move_unit_underlying_id_ = temp_moved_unit->underlying_id();
704
705 //@todo: May be appropriate to replace these separate components by a temporary
706 // wb::move object
707
708 route_.reset(new pathfind::marked_route(route));
709 //NOTE: route_->steps.back() = dst, and route_->steps.front() = src
710
711 size_t turn = 0;
712 std::vector<map_location>::iterator prev_itor = route.steps.begin();
713 std::vector<map_location>::iterator curr_itor = prev_itor;
714 std::vector<map_location>::iterator end_itor = route.steps.end();
715 for(; curr_itor!=end_itor; ++curr_itor)
716 {
717 const map_location& hex = *curr_itor;
718
719 //search for end-of-turn marks
720 pathfind::marked_route::mark_map::const_iterator w =
721 route.marks.find(hex);
722 if(w != route.marks.end() && w->second.turns > 0)
723 {
724 turn = w->second.turns-1;
725
726 if(turn >= move_arrows_.size())
727 move_arrows_.resize(turn+1);
728 if(turn >= fake_units_.size())
729 fake_units_.resize(turn+1);
730
731 arrow_ptr& move_arrow = move_arrows_[turn];
732 fake_unit_ptr& fake_unit = fake_units_[turn];
733
734 if(!move_arrow)
735 {
736 // Create temp arrow
737 move_arrow.reset(new arrow());
738 move_arrow->set_color(team::get_side_color_id(
739 viewer_side()));
740 move_arrow->set_style(arrow::STYLE_HIGHLIGHTED);
741 }
742
743 arrow_path_t path(prev_itor,curr_itor+1);
744 move_arrow->set_path(path);
745
746 if(path.size() >= 2)
747 {
748 // Bug #20299 demonstrates a situation where an incorrect fake/ghosted unit can be used.
749 // So before assuming that a pre-existing fake_unit can be re-used, check that its ID matches the unit being moved.
750 if(!fake_unit || fake_unit.get_unit_ptr()->id() != temp_moved_unit->id())
751 {
752 // Create temp ghost unit
753 fake_unit = fake_unit_ptr(temp_moved_unit->clone(), resources::fake_units);
754 fake_unit->anim_comp().set_ghosted(true);
755 }
756
757 unit_display::move_unit(path, fake_unit.get_unit_ptr(), false); //get facing right
758 fake_unit->anim_comp().invalidate(*game_display::get_singleton());
759 fake_unit->set_location(*curr_itor);
760 fake_unit->anim_comp().set_ghosted(true);
761 }
762 else //zero-hex path -- don't bother drawing a fake unit
763 fake_unit.reset();
764
765 prev_itor = curr_itor;
766 }
767 }
768 //in case path shortens on next step and one ghosted unit has to be removed
769 int ind = fake_units_.size() - 1;
770 fake_units_[ind]->anim_comp().invalidate(*game_display::get_singleton());
771 //toss out old arrows and fake units
772 move_arrows_.resize(turn+1);
773 fake_units_.resize(turn+1);
774 }
775
erase_temp_move()776 void manager::erase_temp_move()
777 {
778 move_arrows_.clear();
779 for(const fake_unit_ptr& tmp : fake_units_) {
780 if(tmp) {
781 tmp->anim_comp().invalidate(*game_display::get_singleton());
782 }
783 }
784 fake_units_.clear();
785 route_.reset();
786 temp_move_unit_underlying_id_ = 0;
787 }
788
save_temp_move()789 void manager::save_temp_move()
790 {
791 if (has_temp_move() && !executing_actions_ && !resources::controller->is_linger_mode())
792 {
793 side_actions& sa = *viewer_actions();
794 unit* u = future_visible_unit(route_->steps.front());
795 assert(u);
796 size_t first_turn = sa.get_turn_num_of(*u);
797
798 validate_viewer_actions();
799
800 assert(move_arrows_.size() == fake_units_.size());
801 size_t size = move_arrows_.size();
802 for(size_t i=0; i<size; ++i)
803 {
804 arrow_ptr move_arrow = move_arrows_[i];
805 if(!arrow::valid_path(move_arrow->get_path()))
806 continue;
807
808 size_t turn = first_turn + i;
809
810 //@todo Using a marked_route here is wrong, since right now it's not marked
811 //either switch over to a plain route for planned moves, or mark it correctly
812 pathfind::marked_route route;
813 route.steps = move_arrow->get_path();
814 // path_cost() is incomplete as it for example doesn't handle skirmisher, we let the move action generate the costs on it own.
815 // route.move_cost = path_cost(route.steps,*u);
816 route.move_cost = -1;
817
818 sa.queue_move(turn, *u, route, move_arrow, std::move(fake_units_[i]));
819 }
820 erase_temp_move();
821
822 LOG_WB << *viewer_actions() << "\n";
823 print_help_once();
824 }
825 }
826
get_temp_move_unit() const827 unit_map::iterator manager::get_temp_move_unit() const
828 {
829 return resources::gameboard->units().find(temp_move_unit_underlying_id_);
830 }
831
save_temp_attack(const map_location & attacker_loc,const map_location & defender_loc,int weapon_choice)832 void manager::save_temp_attack(const map_location& attacker_loc, const map_location& defender_loc, int weapon_choice)
833 {
834 if (active_ && !executing_actions_ && !resources::controller->is_linger_mode())
835 {
836 assert(weapon_choice >= 0);
837
838 arrow_ptr move_arrow;
839 fake_unit_ptr* fake_unit = nullptr;
840 map_location source_hex;
841
842 if (route_ && !route_->steps.empty())
843 {
844 //attack-move
845 assert(move_arrows_.size() == 1);
846 assert(fake_units_.size() == 1);
847 move_arrow = move_arrows_.front();
848 fake_unit = &fake_units_.front();
849
850 assert(route_->steps.back() == attacker_loc);
851 source_hex = route_->steps.front();
852
853 (**fake_unit).anim_comp().set_disabled_ghosted(true);
854 }
855 else
856 {
857 //simple attack
858 move_arrow.reset(new arrow);
859 source_hex = attacker_loc;
860 route_.reset(new pathfind::marked_route);
861 // We'll pass as parameter a one-hex route with no marks.
862 route_->steps.push_back(attacker_loc);
863 }
864
865 unit* attacking_unit = future_visible_unit(source_hex);
866 assert(attacking_unit);
867
868 validate_viewer_actions();
869
870 side_actions& sa = *viewer_actions();
871 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());
872
873 print_help_once();
874
875 display::get_singleton()->invalidate(defender_loc);
876 display::get_singleton()->invalidate(attacker_loc);
877 erase_temp_move();
878 LOG_WB << *viewer_actions() << "\n";
879 }
880 }
881
save_recruit(const std::string & name,int side_num,const map_location & recruit_hex)882 bool manager::save_recruit(const std::string& name, int side_num, const map_location& recruit_hex)
883 {
884 bool created_planned_recruit = false;
885
886 if (active_ && !executing_actions_ && !resources::controller->is_linger_mode()) {
887 if (side_num != display::get_singleton()->viewing_side())
888 {
889 LOG_WB <<"manager::save_recruit called for a different side than viewing side.\n";
890 created_planned_recruit = false;
891 }
892 else
893 {
894 side_actions& sa = *viewer_actions();
895 unit* recruiter;
896 { wb::future_map raii;
897 recruiter = find_recruiter(side_num-1,recruit_hex);
898 } // end planned unit map scope
899 assert(recruiter);
900 size_t turn = sa.get_turn_num_of(*recruiter);
901 sa.queue_recruit(turn,name,recruit_hex);
902 created_planned_recruit = true;
903
904 print_help_once();
905 }
906 }
907 return created_planned_recruit;
908 }
909
save_recall(const unit & unit,int side_num,const map_location & recall_hex)910 bool manager::save_recall(const unit& unit, int side_num, const map_location& recall_hex)
911 {
912 bool created_planned_recall = false;
913
914 if (active_ && !executing_actions_ && !resources::controller->is_linger_mode())
915 {
916 if (side_num != display::get_singleton()->viewing_side())
917 {
918 LOG_WB <<"manager::save_recall called for a different side than viewing side.\n";
919 created_planned_recall = false;
920 }
921 else
922 {
923 side_actions& sa = *viewer_actions();
924 size_t turn = sa.num_turns();
925 if(turn > 0)
926 --turn;
927 sa.queue_recall(turn,unit,recall_hex);
928 created_planned_recall = true;
929
930 print_help_once();
931 }
932 }
933 return created_planned_recall;
934 }
935
save_suppose_dead(unit & curr_unit,const map_location & loc)936 void manager::save_suppose_dead(unit& curr_unit, const map_location& loc)
937 {
938 if(active_ && !executing_actions_ && !resources::controller->is_linger_mode())
939 {
940 validate_viewer_actions();
941 side_actions& sa = *viewer_actions();
942 sa.queue_suppose_dead(sa.get_turn_num_of(curr_unit),curr_unit,loc);
943 }
944 }
945
contextual_execute()946 void manager::contextual_execute()
947 {
948 validate_viewer_actions();
949 if (can_enable_execution_hotkeys())
950 {
951 erase_temp_move();
952
953 //For exception-safety, this struct sets executing_actions_ to false on destruction.
954 variable_finalizer<bool> finally(executing_actions_, false);
955
956 action_ptr action;
957 side_actions::iterator it = viewer_actions()->end();
958 unit const* selected_unit = future_visible_unit(resources::controller->get_mouse_handler_base().get_selected_hex(), viewer_side());
959 if (selected_unit &&
960 (it = viewer_actions()->find_first_action_of(*selected_unit)) != viewer_actions()->end())
961 {
962 executing_actions_ = true;
963 viewer_actions()->execute(it);
964 }
965 else if (highlighter_ && (action = highlighter_->get_execute_target()) &&
966 (it = viewer_actions()->get_position_of(action)) != viewer_actions()->end())
967 {
968 executing_actions_ = true;
969 viewer_actions()->execute(it);
970 }
971 else //we already check above for viewer_actions()->empty()
972 {
973 executing_actions_ = true;
974 viewer_actions()->execute_next();
975 }
976 } //Finalizer struct sets executing_actions_ to false
977 }
978
allow_end_turn()979 bool manager::allow_end_turn()
980 {
981 preparing_to_end_turn_ = true;
982 return execute_all_actions();
983 }
984
execute_all_actions()985 bool manager::execute_all_actions()
986 {
987 if (has_planned_unit_map())
988 {
989 ERR_WB << "Modifying action queue while temp modifiers are applied1!!!" << std::endl;
990 }
991 //exception-safety: finalizers set variables to false on destruction
992 //i.e. when method exits naturally or exception is thrown
993 variable_finalizer<bool> finalize_executing_actions(executing_actions_, false);
994 variable_finalizer<bool> finalize_executing_all_actions(executing_all_actions_, false);
995
996 validate_viewer_actions();
997 if(viewer_actions()->empty() || viewer_actions()->turn_size(0) == 0)
998 {
999 //No actions to execute, job done.
1000 return true;
1001 }
1002
1003 assert(can_enable_execution_hotkeys());
1004
1005 erase_temp_move();
1006
1007 // Build unit map once to ensure spent gold and other calculations are refreshed
1008 set_planned_unit_map();
1009 assert(has_planned_unit_map());
1010 set_real_unit_map();
1011
1012 executing_actions_ = true;
1013 executing_all_actions_ = true;
1014
1015 side_actions_ptr sa = viewer_actions();
1016
1017 if (has_planned_unit_map())
1018 {
1019 ERR_WB << "Modifying action queue while temp modifiers are applied!!!" << std::endl;
1020 }
1021
1022 //LOG_WB << "Before executing all actions, " << *sa << "\n";
1023
1024 while (sa->turn_begin(0) != sa->turn_end(0))
1025 {
1026 bool action_successful = sa->execute(sa->begin());
1027
1028 // Interrupt on incomplete action
1029 if (!action_successful)
1030 {
1031 return false;
1032 }
1033 }
1034 return true;
1035 }
1036
contextual_delete()1037 void manager::contextual_delete()
1038 {
1039 validate_viewer_actions();
1040 if(can_enable_modifier_hotkeys()) {
1041 erase_temp_move();
1042
1043 action_ptr action;
1044 side_actions::iterator it = viewer_actions()->end();
1045 unit const* selected_unit = future_visible_unit(resources::controller->get_mouse_handler_base().get_selected_hex(), viewer_side());
1046 if(selected_unit && (it = viewer_actions()->find_last_action_of(*selected_unit)) != viewer_actions()->end()) {
1047 viewer_actions()->remove_action(it);
1048 ///@todo Shouldn't we probably deselect the unit at this point?
1049 } else if(highlighter_ && (action = highlighter_->get_delete_target()) && (it = viewer_actions()->get_position_of(action)) != viewer_actions()->end()) {
1050 viewer_actions()->remove_action(it);
1051 validate_viewer_actions();
1052 highlighter_->set_mouseover_hex(highlighter_->get_mouseover_hex());
1053 highlighter_->highlight();
1054 } else { //we already check above for viewer_actions()->empty()
1055 it = (viewer_actions()->end() - 1);
1056 action = *it;
1057 viewer_actions()->remove_action(it);
1058 validate_viewer_actions();
1059 }
1060 }
1061 }
1062
contextual_bump_up_action()1063 void manager::contextual_bump_up_action()
1064 {
1065 validate_viewer_actions();
1066 if(can_enable_reorder_hotkeys()) {
1067 action_ptr action = highlighter_->get_bump_target();
1068 if(action) {
1069 viewer_actions()->bump_earlier(viewer_actions()->get_position_of(action));
1070 validate_viewer_actions(); // Redraw arrows
1071 }
1072 }
1073 }
1074
contextual_bump_down_action()1075 void manager::contextual_bump_down_action()
1076 {
1077 validate_viewer_actions();
1078 if(can_enable_reorder_hotkeys()) {
1079 action_ptr action = highlighter_->get_bump_target();
1080 if(action) {
1081 viewer_actions()->bump_later(viewer_actions()->get_position_of(action));
1082 validate_viewer_actions(); // Redraw arrows
1083 }
1084 }
1085 }
1086
has_actions() const1087 bool manager::has_actions() const
1088 {
1089 assert(resources::gameboard);
1090 return wb::has_actions();
1091 }
1092
unit_has_actions(unit const * unit) const1093 bool manager::unit_has_actions(unit const* unit) const
1094 {
1095 assert(unit != nullptr);
1096 assert(resources::gameboard);
1097 return viewer_actions()->unit_has_actions(*unit);
1098 }
1099
get_spent_gold_for(int side)1100 int manager::get_spent_gold_for(int side)
1101 {
1102 if(wait_for_side_init_)
1103 return 0;
1104
1105 return resources::gameboard->get_team(side).get_side_actions()->get_gold_spent();
1106 }
1107
should_clear_undo() const1108 bool manager::should_clear_undo() const
1109 {
1110 return resources::controller->is_networked_mp() && resources::controller->current_team().is_local();
1111 }
1112
options_dlg()1113 void manager::options_dlg()
1114 {
1115 int v_side = viewer_side();
1116
1117 int selection = 0;
1118
1119 std::vector<team*> allies;
1120 std::vector<std::string> options;
1121 utils::string_map t_vars;
1122
1123 options.emplace_back(_("SHOW ALL allies’ plans"));
1124 options.emplace_back(_("HIDE ALL allies’ plans"));
1125
1126 //populate list of networked allies
1127 for(team &t : resources::gameboard->teams())
1128 {
1129 //Exclude enemies, AIs, and local players
1130 if(t.is_enemy(v_side) || !t.is_network())
1131 continue;
1132
1133 allies.push_back(&t);
1134
1135 t_vars["player"] = t.current_player();
1136 size_t t_index = t.side()-1;
1137 if(team_plans_hidden_[t_index])
1138 options.emplace_back(VGETTEXT("Show plans for $player", t_vars));
1139 else
1140 options.emplace_back(VGETTEXT("Hide plans for $player", t_vars));
1141 }
1142
1143 gui2::dialogs::simple_item_selector dlg("", _("Whiteboard Options"), options);
1144 dlg.show();
1145 selection = dlg.selected_index();
1146
1147 if(selection == -1)
1148 return;
1149
1150 switch(selection)
1151 {
1152 case 0:
1153 for(team* t : allies) {
1154 team_plans_hidden_[t->side()-1]=false;
1155 }
1156 break;
1157 case 1:
1158 for(team* t : allies) {
1159 team_plans_hidden_[t->side()-1]=true;
1160 }
1161 break;
1162 default:
1163 if(selection > 1)
1164 {
1165 size_t t_index = allies[selection-2]->side()-1;
1166 //toggle ...
1167 bool hidden = team_plans_hidden_[t_index];
1168 team_plans_hidden_[t_index] = !hidden;
1169 }
1170 break;
1171 }
1172 update_plan_hiding();
1173 }
1174
set_planned_unit_map()1175 void manager::set_planned_unit_map()
1176 {
1177 if (!can_modify_game_state()) {
1178 LOG_WB << "Not building planned unit map: cannot modify game state now.\n";
1179 return;
1180 }
1181 //any more than one reference means a lock on unit map was requested
1182 if(!unit_map_lock_.unique()) {
1183 LOG_WB << "Not building planned unit map: unit map locked.\n";
1184 return;
1185 }
1186 if (planned_unit_map_active_) {
1187 WRN_WB << "Not building planned unit map: already set." << std::endl;
1188 return;
1189 }
1190
1191 log_scope2("whiteboard", "Building planned unit map");
1192 mapbuilder_.reset(new mapbuilder(resources::gameboard->units()));
1193 mapbuilder_->build_map();
1194
1195 planned_unit_map_active_ = true;
1196 }
1197
set_real_unit_map()1198 void manager::set_real_unit_map()
1199 {
1200 if (planned_unit_map_active_)
1201 {
1202 assert(!executing_actions_);
1203 assert(!wait_for_side_init_);
1204 if(mapbuilder_)
1205 {
1206 log_scope2("whiteboard", "Restoring regular unit map.");
1207 mapbuilder_.reset();
1208 }
1209 planned_unit_map_active_ = false;
1210 }
1211 else
1212 {
1213 LOG_WB << "Not disabling planned unit map: already disabled.\n";
1214 }
1215 }
1216
validate_actions_if_needed()1217 void manager::validate_actions_if_needed()
1218 {
1219 if (gamestate_mutated_) {
1220 validate_viewer_actions();
1221 }
1222 }
1223
future_map()1224 future_map::future_map():
1225 initial_planned_unit_map_(resources::whiteboard && resources::whiteboard->has_planned_unit_map())
1226 {
1227 if (!resources::whiteboard)
1228 return;
1229 if (!initial_planned_unit_map_)
1230 resources::whiteboard->set_planned_unit_map();
1231 // check if if unit map was successfully applied
1232 if (!resources::whiteboard->has_planned_unit_map()) {
1233 DBG_WB << "Scoped future unit map failed to apply.\n";
1234 }
1235 }
1236
~future_map()1237 future_map::~future_map()
1238 {
1239 try {
1240 if (!resources::whiteboard)
1241 return;
1242 if (!initial_planned_unit_map_ && resources::whiteboard->has_planned_unit_map())
1243 resources::whiteboard->set_real_unit_map();
1244 } catch (...) {}
1245 }
1246
future_map_if_active()1247 future_map_if_active::future_map_if_active():
1248 initial_planned_unit_map_(resources::whiteboard && resources::whiteboard->has_planned_unit_map()),
1249 whiteboard_active_(resources::whiteboard && resources::whiteboard->is_active())
1250 {
1251 if (!resources::whiteboard)
1252 return;
1253 if (!whiteboard_active_)
1254 return;
1255 if (!initial_planned_unit_map_)
1256 resources::whiteboard->set_planned_unit_map();
1257 // check if if unit map was successfully applied
1258 if (!resources::whiteboard->has_planned_unit_map()) {
1259 DBG_WB << "Scoped future unit map failed to apply.\n";
1260 }
1261 }
1262
~future_map_if_active()1263 future_map_if_active::~future_map_if_active()
1264 {
1265 try {
1266 if (!resources::whiteboard)
1267 return;
1268 if (!initial_planned_unit_map_ && resources::whiteboard->has_planned_unit_map())
1269 resources::whiteboard->set_real_unit_map();
1270 } catch (...) {}
1271 }
1272
1273
real_map()1274 real_map::real_map():
1275 initial_planned_unit_map_(resources::whiteboard && resources::whiteboard->has_planned_unit_map()),
1276 unit_map_lock_(resources::whiteboard ? resources::whiteboard->unit_map_lock_ : std::make_shared<bool>(false))
1277 {
1278 if (!resources::whiteboard)
1279 return;
1280 if (initial_planned_unit_map_)
1281 resources::whiteboard->set_real_unit_map();
1282 }
1283
~real_map()1284 real_map::~real_map()
1285 {
1286 if (!resources::whiteboard)
1287 return;
1288 assert(!resources::whiteboard->has_planned_unit_map());
1289 if (initial_planned_unit_map_)
1290 {
1291 resources::whiteboard->set_planned_unit_map();
1292 }
1293 }
1294
1295 } // end namespace wb
1296