/
undo.cpp
483 lines (402 loc) · 14.7 KB
/
undo.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
/*
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
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
* Undoing, redoing.
*/
#include "actions/undo.hpp"
#include "game_board.hpp" // for game_board
#include "log.hpp" // for LOG_STREAM, logger, etc
#include "map/map.hpp" // for gamemap
#include "map/location.hpp" // for map_location, operator<<, etc
#include "mouse_handler_base.hpp" // for command_disabler
#include "preferences/general.hpp"
#include "recall_list_manager.hpp" // for recall_list_manager
#include "replay.hpp" // for recorder, replay
#include "replay_helper.hpp" // for replay_helper
#include "resources.hpp" // for screen, teams, units, etc
#include "synced_context.hpp" // for set_scontext_synced
#include "team.hpp" // for team
#include "units/unit.hpp" // for unit
#include "units/animation_component.hpp"
#include "units/id.hpp"
#include "units/map.hpp" // for unit_map, etc
#include "units/ptr.hpp" // for unit_const_ptr, unit_ptr
#include "units/types.hpp" // for unit_type, unit_type_data, etc
#include "whiteboard/manager.hpp" // for manager
#include "actions/create.hpp" // for find_recall_location, etc
#include "actions/move.hpp" // for get_village
#include "actions/vision.hpp" // for clearer_info, etc
#include "actions/shroud_clearing_action.hpp"
#include "actions/undo_dismiss_action.hpp"
#include "actions/undo_move_action.hpp"
#include "actions/undo_recall_action.hpp"
#include "actions/undo_recruit_action.hpp"
#include "actions/undo_update_shroud_action.hpp"
#include <algorithm> // for reverse
#include <cassert> // for assert
#include <ostream> // for operator<<, basic_ostream, etc
#include <set> // for set
static lg::log_domain log_engine("engine");
#define ERR_NG LOG_STREAM(err, log_engine)
#define LOG_NG LOG_STREAM(info, log_engine)
namespace actions {
/**
* Creates an undo_action based on a config.
* @return a pointer that must be deleted, or nullptr if the @a cfg could not be parsed.
*/
undo_action_base * undo_list::create_action(const config & cfg)
{
const std::string str = cfg["type"];
undo_action_base * res = nullptr;
// The general division of labor in this function is that the various
// constructors will parse the "unit" child config, while this function
// parses everything else.
if ( str == "move" ) {
res = new undo::move_action(cfg, cfg.child_or_empty("unit"),
cfg["starting_moves"],
map_location::parse_direction(cfg["starting_direction"]));
}
else if ( str == "recruit" ) {
// Validate the unit type.
const config & child = cfg.child("unit");
const unit_type * u_type = unit_types.find(child["type"]);
if ( !u_type ) {
// Bad data.
ERR_NG << "Invalid recruit found in [undo] or [redo]; unit type '"
<< child["type"] << "' was not found.\n";
return nullptr;
}
res = new undo::recruit_action(cfg, *u_type, map_location(cfg.child_or_empty("leader"), nullptr));
}
else if ( str == "recall" )
res = new undo::recall_action(cfg, map_location(cfg.child_or_empty("leader"), nullptr));
else if ( str == "dismiss" )
res = new undo::dismiss_action(cfg, cfg.child("unit"));
else if ( str == "auto_shroud" )
res = new undo::auto_shroud_action(cfg["active"].to_bool());
else if ( str == "update_shroud" )
res = new undo::update_shroud_action();
else if ( str == "dummy" )
res = new undo_dummy_action(cfg);
else
{
// Unrecognized type.
ERR_NG << "Unrecognized undo action type: " << str << "." << std::endl;
return nullptr;
}
return res;
}
/**
* Constructor.
* The config is allowed to be invalid.
*/
undo_list::undo_list(const config & cfg) :
undos_(), redos_(), side_(1), committed_actions_(false)
{
if ( cfg )
read(cfg);
}
/**
* Destructor.
*/
undo_list::~undo_list()
{
// Default destructor, but defined out-of-line to localize the templating.
// (Might make compiles faster.)
}
/**
* Adds an auto-shroud toggle to the undo stack.
*/
void undo_list::add_auto_shroud(bool turned_on)
{
/// @todo: Consecutive shroud actions can be collapsed into one.
// Do not call add(), as this should not clear the redo stack.
add(new undo::auto_shroud_action(turned_on));
}
void undo_list::add_dummy()
{
/// @todo: Consecutive shroud actions can be collapsed into one.
// Do not call add(), as this should not clear the redo stack.
add(new undo_dummy_action());
}
/**
* Adds a dismissal to the undo stack.
*/
void undo_list::add_dismissal(const unit_const_ptr u)
{
add(new undo::dismiss_action(u));
}
/**
* Adds a move to the undo stack.
*/
void undo_list::add_move(const unit_const_ptr u,
const std::vector<map_location>::const_iterator & begin,
const std::vector<map_location>::const_iterator & end,
int start_moves, int timebonus, int village_owner,
const map_location::DIRECTION dir)
{
add(new undo::move_action(u, begin, end, start_moves, timebonus, village_owner, dir));
}
/**
* Adds a recall to the undo stack.
*/
void undo_list::add_recall(const unit_const_ptr u, const map_location& loc,
const map_location& from, int orig_village_owner, bool time_bonus)
{
add(new undo::recall_action(u, loc, from, orig_village_owner, time_bonus));
}
/**
* Adds a recruit to the undo stack.
*/
void undo_list::add_recruit(const unit_const_ptr u, const map_location& loc,
const map_location& from, int orig_village_owner, bool time_bonus)
{
add(new undo::recruit_action(u, loc, from, orig_village_owner, time_bonus));
}
/**
* Adds a shroud update to the undo stack.
* This is called from within commit_vision(), so there should be no need
* for this to be publicly visible.
*/
void undo_list::add_update_shroud()
{
/// @todo: Consecutive shroud actions can be collapsed into one.
add(new undo::update_shroud_action());
}
/**
* Clears the stack of undoable (and redoable) actions.
* (Also handles updating fog/shroud if needed.)
* Call this if an action alters the game state, but add that action to the
* stack before calling this (if the action is a kind that can be undone).
* This may fire events and change the game state.
*/
void undo_list::clear()
{
// The fact that this function was called indicates that something was done.
// (Some actions, such as attacks, are never put on the stack.)
committed_actions_ = true;
// We can save some overhead by not calling apply_shroud_changes() for an
// empty stack.
if ( !undos_.empty() ) {
apply_shroud_changes();
undos_.clear();
}
// No special handling for redos, so just clear that stack.
redos_.clear();
}
/**
* Updates fog/shroud based on the undo stack, then updates stack as needed.
* Call this when "updating shroud now".
* This may fire events and change the game state.
* @param[in] is_replay Set to true when this is called during a replay.
*/
void undo_list::commit_vision()
{
// Update fog/shroud.
bool cleared_something = apply_shroud_changes();
if (cleared_something) {
// The actions that led to information being revealed can no longer
// be undone.
undos_.clear();
//undos_.erase(undos_.begin(), undos_.begin() + erase_to);
committed_actions_ = true;
}
}
/**
* Performs some initializations and error checks when starting a new side-turn.
* @param[in] side The side whose turn is about to start.
*/
void undo_list::new_side_turn(int side)
{
// Error checks.
if ( !undos_.empty() ) {
ERR_NG << "Undo stack not empty in new_side_turn()." << std::endl;
// At worst, someone missed some sighted events, so try to recover.
undos_.clear();
redos_.clear();
}
else if ( !redos_.empty() ) {
ERR_NG << "Redo stack not empty in new_side_turn()." << std::endl;
// Sloppy tracking somewhere, but not critically so.
redos_.clear();
}
// Reset the side.
side_ = side;
committed_actions_ = false;
}
/**
* Read the undo_list from the provided config.
* Currently, this is only used when the undo_list is empty, but in theory
* it could be used to append the config to the current data.
*/
void undo_list::read(const config & cfg)
{
// Merge header data.
side_ = cfg["side"].to_int(side_);
committed_actions_ = committed_actions_ || cfg["committed"].to_bool();
// Build the undo stack.
for (const config & child : cfg.child_range("undo")) {
try {
undo_action_base * action = create_action(child);
if ( action ) {
undos_.emplace_back(action);
}
} catch (const bad_lexical_cast &) {
ERR_NG << "Error when parsing undo list from config: bad lexical cast." << std::endl;
ERR_NG << "config was: " << child.debug() << std::endl;
ERR_NG << "Skipping this undo action..." << std::endl;
} catch (const config::error& e) {
ERR_NG << "Error when parsing undo list from config: " << e.what() << std::endl;
ERR_NG << "config was: " << child.debug() << std::endl;
ERR_NG << "Skipping this undo action..." << std::endl;
}
}
// Build the redo stack.
for (const config & child : cfg.child_range("redo")) {
try {
redos_.emplace_back(new config(child));
} catch (const bad_lexical_cast &) {
ERR_NG << "Error when parsing redo list from config: bad lexical cast." << std::endl;
ERR_NG << "config was: " << child.debug() << std::endl;
ERR_NG << "Skipping this redo action..." << std::endl;
} catch (const config::error& e) {
ERR_NG << "Error when parsing redo list from config: " << e.what() << std::endl;
ERR_NG << "config was: " << child.debug() << std::endl;
ERR_NG << "Skipping this redo action..." << std::endl;
}
}
}
/**
* Write the undo_list into the provided config.
*/
void undo_list::write(config & cfg) const
{
cfg["side"] = side_;
cfg["committed"] = committed_actions_;
for ( const auto& action_ptr : undos_)
action_ptr->write(cfg.add_child("undo"));
for ( const auto& cfg_ptr : redos_)
cfg.add_child("redo") = *cfg_ptr;
}
/**
* Undoes the top action on the undo stack.
*/
void undo_list::undo()
{
if ( undos_.empty() )
return;
const events::command_disabler disable_commands;
// Get the action to undo. (This will be placed on the redo stack, but
// only if the undo is successful.)
auto action = std::move(undos_.back());
undos_.pop_back();
if (undo_action* undoable_action = dynamic_cast<undo_action*>(action.get()))
{
int last_unit_id = resources::gameboard->unit_id_manager().get_save_id();
if ( !undoable_action->undo(side_) ) {
return;
}
if(last_unit_id - undoable_action->unit_id_diff < 0) {
ERR_NG << "Next unit id is below 0 after undoing" << std::endl;
}
resources::gameboard->unit_id_manager().set_save_id(last_unit_id - undoable_action->unit_id_diff);
// Bookkeeping.
redos_.emplace_back(new config());
resources::recorder->undo_cut(*redos_.back());
resources::whiteboard->on_gamestate_change();
}
else
{
//ignore this action, and undo the previous one.
config replay_data;
resources::recorder->undo_cut(replay_data);
undo();
resources::recorder->redo(replay_data);
undos_.emplace_back(std::move(action));
}
}
/**
* Redoes the top action on the redo stack.
*/
void undo_list::redo()
{
if ( redos_.empty() )
return;
const events::command_disabler disable_commands;
// Get the action to redo. (This will be placed on the undo stack, but
// only if the redo is successful.)
auto action = std::move(redos_.back());
redos_.pop_back();
const config& command_wml = action->child("command");
std::string commandname = command_wml.all_children_range().front().key;
const config& data = command_wml.all_children_range().front().cfg;
resources::recorder->redo(const_cast<const config&>(*action));
// synced_context::run readds the undo command with the normal undo_lis::add function which clears the
// redo stack which makes redoign of more than one move impossible. to work around that we save redo stack here and set it later.
redos_list temp;
temp.swap(redos_);
synced_context::run(commandname, data, /*use_undo*/ true, /*show*/ true);
temp.swap(redos_);
}
/**
* Applies the pending fog/shroud changes from the undo stack.
* Does nothing if the the current side does not use fog or shroud.
* @returns true if shroud or fog was cleared.
*/
bool undo_list::apply_shroud_changes() const
{
team &tm = resources::gameboard->get_team(side_);
// No need to do clearing if fog/shroud has been kept up-to-date.
if ( tm.auto_shroud_updates() || !tm.fog_or_shroud() ) {
return false;
}
shroud_clearer clearer;
bool cleared_shroud = false;
const std::size_t list_size = undos_.size();
// Loop through the list of undo_actions.
for( std::size_t i = 0; i != list_size; ++i ) {
if (const shroud_clearing_action* action = dynamic_cast<const shroud_clearing_action*>(undos_[i].get())) {
LOG_NG << "Turning an undo...\n";
// Clear the hexes this unit can see from each hex occupied during
// the action.
std::vector<map_location>::const_iterator step;
for (step = action->route.begin(); step != action->route.end(); ++step) {
// Clear the shroud, collecting new sighted events.
// (This can be made gradual by changing "true" to "false".)
if ( clearer.clear_unit(*step, tm, action->view_info, true) ) {
cleared_shroud = true;
}
}
}
}
if (!cleared_shroud) {
return false;
}
// If we clear fog or shroud outside a synced context we get OOS
// Note that it can happen that we call this function from ouside a synced context
// when we reload a game and want to prevent undoing. But in this case this is
// preceded by a manual update_shroud call so that cleared_shroud is false.
assert(synced_context::is_synced());
// The entire stack needs to be cleared in order to preserve replays.
// (The events that fired might depend on current unit positions.)
// (Also the events that did not fire might depend on unit positions (they whould have fired if the unit would have standed on different positions, for example this can happen if they have a [have_unit] in [filter_condition]))
// Update the display before pumping events.
clearer.invalidate_after_clear();
// Fire sighted events
if ( std::get<0>(clearer.fire_events() )) {
// Fix up the display in case WML changed stuff.
clear_shroud(side_);
}
return true;
}
}//namespace actions