The Battle for Wesnoth  1.17.21+dev
map_context.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2023
3  by Tomasz Sniatowski <kailoran@gmail.com>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #define GETTEXT_DOMAIN "wesnoth-editor"
17 
19 
20 #include "display.hpp"
21 #include "editor/action/action.hpp"
22 #include "filesystem.hpp"
23 #include "formula/string_utils.hpp"
24 #include "game_board.hpp"
25 #include "gettext.hpp"
26 #include "gui/dialogs/message.hpp"
27 #include "map/exception.hpp"
28 #include "map/label.hpp"
29 #include "preferences/editor.hpp"
31 #include "serialization/parser.hpp"
32 #include "team.hpp"
33 #include "units/unit.hpp"
34 #include "game_config_view.hpp"
35 
36 #include <boost/regex.hpp>
37 
38 namespace editor
39 {
41  : side(t.side())
42  , id(t.team_name())
43  , name(t.user_team_name())
44  , recruit_list(utils::join(t.recruits(), ","))
45  , gold(t.gold())
46  , income(t.base_income())
49  , fog(t.uses_fog())
50  , shroud(t.uses_shroud())
51  , share_vision(t.share_vision())
53  , no_leader(t.no_leader())
54  , hidden(t.hidden())
55 {
56 }
57 
58 const std::size_t map_context::max_action_stack_size_ = 100;
59 
60 map_context::map_context(const editor_map& map, bool pure_map, const config& schedule, const std::string& addon_id)
61  : filename_()
62  , map_data_key_()
63  , embedded_(false)
64  , pure_map_(pure_map)
65  , map_(map)
66  , undo_stack_()
67  , redo_stack_()
68  , actions_since_save_(0)
69  , starting_position_label_locs_()
70  , needs_reload_(false)
71  , needs_terrain_rebuild_(false)
72  , needs_labels_reset_(false)
73  , changed_locations_()
74  , everything_changed_(false)
75  , addon_id_(addon_id)
76  , previous_cfg_()
77  , scenario_id_()
78  , scenario_name_()
79  , scenario_description_()
80  , xp_mod_()
81  , victory_defeated_(true)
82  , random_time_(false)
83  , active_area_(-1)
84  , labels_(nullptr)
85  , units_()
86  , teams_()
87  , tod_manager_(new tod_manager(schedule))
88  , mp_settings_()
89  , game_classification_()
90  , music_tracks_()
91 {
92 }
93 
94 static std::string get_map_location(const std::string& file_contents, const std::string& attr)
95 {
96  std::size_t attr_name_start = file_contents.find(attr);
97  if(attr_name_start == std::string::npos) return "";
98 
99  std::size_t attr_value_start = file_contents.find("=", attr_name_start);
100  std::size_t line_end = file_contents.find("\n", attr_name_start);
101  if(line_end < attr_value_start) return "";
102 
103  attr_value_start++;
104  std::string attr_value = file_contents.substr(attr_value_start, line_end - attr_value_start);
105  std::string_view v2 = attr_value;
106  utils::trim(v2);
107 
108  return std::string(v2);
109 }
110 
111 map_context::map_context(const game_config_view& game_config, const std::string& filename, const std::string& addon_id)
112  : filename_(filename)
113  , map_data_key_()
114  , embedded_(false)
115  , pure_map_(false)
116  , map_()
117  , undo_stack_()
118  , redo_stack_()
119  , actions_since_save_(0)
120  , starting_position_label_locs_()
121  , needs_reload_(false)
122  , needs_terrain_rebuild_(false)
123  , needs_labels_reset_(false)
124  , changed_locations_()
125  , everything_changed_(false)
126  , addon_id_(addon_id)
127  , previous_cfg_()
128  , scenario_id_()
129  , scenario_name_()
130  , scenario_description_()
131  , xp_mod_()
132  , victory_defeated_(true)
133  , random_time_(false)
134  , active_area_(-1)
135  , labels_(nullptr)
136  , units_()
137  , teams_()
138  , tod_manager_(new tod_manager(game_config.find_mandatory_child("editor_times", "id", "empty")))
139  , mp_settings_()
140  , game_classification_()
141  , music_tracks_()
142 {
143  /*
144  * Overview of situations possibly found in the file:
145  *
146  * embedded_ - the map data is directly in the scenario file
147  * pure_map_ - the map data is in its own separate file (map_file, map_data+macro inclusion) or this is a .map file
148  *
149  * an editor-generated file uses neither of these and is its own thing - it's not embedded (since the editor now saves using map_file) and it's not a pure map since there's also scenario data involved
150  *
151  * 0. Not a scenario or map file.
152  * 0.1 File not found
153  * 0.2 Map file empty
154  * 0.3 Not a .map or .cfg file
155  * 1. It's a .map file.
156  * * embedded_ = false
157  * * pure_map_ = true
158  * 2. A scenario embedding the map
159  * * embedded_ = true
160  * * pure_map_ = true
161  * The scenario-test.cfg for example.
162  * The map is written back to the file.
163  * 3. The map file is referenced by map_data={MACRO_ARGUEMENT}.
164  * * embedded_ = false
165  * * pure_map_ = true
166  * 4. The file contains an editor generated scenario file.
167  * * embedded_ = false
168  * * pure_map_ = false
169  * 5. The file is using map_file.
170  * 5.1 The file doesn't contain a macro and so can be loaded by the editor as a scenario
171  * * embedded_ = false
172  * * pure_map_ = false
173  * 5.2 The file contains a macro and so can't be loaded by the editor as a scenario
174  * * embedded_ = false
175  * * pure_map_ = true
176  */
177 
178  log_scope2(log_editor, "Loading file " + filename);
179 
180  // 0.1 File not found
181  if(!filesystem::file_exists(filename) || filesystem::is_directory(filename)) {
182  throw editor_map_load_exception(filename, _("File not found"));
183  }
184 
185  std::string file_string = filesystem::read_file(filename);
186 
187  // 0.2 Map file empty
188  if(file_string.empty()) {
189  std::string message = _("Empty file");
190  throw editor_map_load_exception(filename, message);
191  }
192 
193  // 0.3 Not a .map or .cfg file
194  if(!filesystem::ends_with(filename, ".map") && !filesystem::ends_with(filename, ".cfg")) {
195  std::string message = _("File does not have .map or .cfg extension");
196  throw editor_map_load_exception(filename, message);
197  }
198 
199  // 1.0 Pure map data
200  if(filesystem::ends_with(filename, ".map") || filesystem::ends_with(filename, ".mask")) {
201  LOG_ED << "Loading map or mask file";
202  map_ = editor_map::from_string(file_string); // throws on error
203  pure_map_ = true;
204 
206  } else {
207  // 4.0 old-style editor generated scenario which lacks a top-level tag
208  if(file_string.find("[multiplayer]") == std::string::npos &&
209  file_string.find("[scenario]") == std::string::npos &&
210  file_string.find("[test]") == std::string::npos) {
211  LOG_ED << "Loading generated scenario file";
212  try {
213  load_scenario();
214  } catch(const std::exception& e) {
215  throw editor_map_load_exception("load_scenario: old-style scenario", e.what());
216  }
218  } else {
219  std::string map_data_loc = get_map_location(file_string, "map_data");
220  std::string map_file_loc = get_map_location(file_string, "map_file");
221 
222  if(!map_data_loc.empty()) {
223  if(map_data_loc.find("\"{") == std::string::npos) {
224  // 2.0 Embedded pure map
225  LOG_ED << "Loading embedded map file";
226  embedded_ = true;
227  pure_map_ = true;
228  std::size_t start = file_string.find(map_data_loc)+1;
229  std::size_t length = file_string.find("\"", start)-start;
230  std::string map_data = file_string.substr(start, length);
231  map_ = editor_map::from_string(map_data);
233  } else {
234  // 3.0 Macro referenced pure map
235  const std::string& macro_argument = map_data_loc.substr(2, map_data_loc.size()-4);
236  LOG_ED << "Map looks like a scenario, trying {" << macro_argument << "}";
237 
239 
240  if(new_filename.empty()) {
241  std::string message = _("The map file looks like a scenario, but the map_data value does not point to an existing file")
242  + std::string("\n") + macro_argument;
243  throw editor_map_load_exception(filename, message);
244  }
245 
246  LOG_ED << "New filename is: " << new_filename;
247 
248  filename_ = new_filename;
249  file_string = filesystem::read_file(filename_);
250  map_ = editor_map::from_string(file_string);
251  pure_map_ = true;
252 
254  }
255  } else if(!map_file_loc.empty()) {
256  // 5.0 The file is using map_file.
257  try {
258  // 5.1 The file can be loaded by the editor as a scenario
259  if(file_string.find("<<") != std::string::npos) {
260  throw editor_map_load_exception(filename, _("Found the characters '<<' indicating inline lua is present - aborting"));
261  }
262  load_scenario();
263  } catch(const std::exception&) {
264  // 5.2 The file can't be loaded by the editor as a scenario, so try to just load the map
265  gui2::show_message(_("Error"), _("Failed to load the scenario, attempting to load only the map."), gui2::dialogs::message::auto_close);
266 
267  // NOTE: this means that loading the map file from a scenario where the maps are in nested directories under maps/ will not work
268  // this is done to address mainline scenarios referencing their maps as "multiplayer/maps/<map_file>.map"
269  // otherwise this results in the "multiplayer/maps/" part getting duplicated in the path and then not being found
270  std::string new_filename = filesystem::get_current_editor_dir(addon_id_) + "/maps/" + filesystem::base_name(map_file_loc);
271  if(!filesystem::file_exists(new_filename)) {
272  std::string message = _("The map file looks like a scenario, but the map_file value does not point to an existing file")
273  + std::string("\n") + new_filename;
274  throw editor_map_load_exception(filename, message);
275  }
276 
277  LOG_ED << "New filename is: " << new_filename;
278 
279  filename_ = new_filename;
280  file_string = filesystem::read_file(filename_);
281  map_ = editor_map::from_string(file_string);
282  pure_map_ = true;
283  }
284 
286  } else {
287  throw editor_map_load_exception(filename, _("Unable to parse file to find map data"));
288  }
289  }
290  }
291 }
292 
294 {
295  undo_stack_.clear();
296  redo_stack_.clear();
297 }
298 
300 {
301  teams_.emplace_back();
302 
303  config cfg;
304  cfg["side"] = teams_.size(); // side is 1-indexed, so we can just use size()
305  cfg["hidden"] = false;
306 
307  teams_.back().build(cfg, map());
308 
310 }
311 
313 {
314  assert(teams_.size() >= static_cast<unsigned int>(info.side));
315 
316  team& t = teams_[info.side - 1];
317  t.change_team(info.id, info.name);
318  t.set_recruits(utils::set_split(info.recruit_list, ','));
319  t.have_leader(!info.no_leader);
320  t.change_controller(info.controller);
321  t.set_gold(info.gold);
322  t.set_base_income(info.income);
323  t.set_hidden(info.hidden);
324  t.set_fog(info.fog);
325  t.set_shroud(info.shroud);
326  t.set_share_vision(info.share_vision);
327  t.set_village_gold(info.village_income);
328  t.set_village_support(info.village_support);
329 
331 }
332 
333 void map_context::set_scenario_setup(const std::string& id,
334  const std::string& name,
335  const std::string& description,
336  int turns,
337  int xp_mod,
338  bool victory_defeated,
339  bool random_time)
340 {
341  scenario_id_ = id;
342  scenario_name_ = name;
343  scenario_description_ = description;
344  random_time_ = random_time;
346  tod_manager_->set_number_of_turns(turns);
347  xp_mod_ = xp_mod;
349 }
350 
352 {
353  tod_manager_->set_current_time(time);
354  if(!pure_map_) {
356  }
357 }
358 
360 {
361  tod_manager_->remove_time_area(index);
362  active_area_--;
364 }
365 
366 void map_context::replace_schedule(const std::vector<time_of_day>& schedule)
367 {
368  tod_manager_->replace_schedule(schedule);
369  if(!pure_map_) {
371  }
372 }
373 
374 void map_context::replace_local_schedule(const std::vector<time_of_day>& schedule)
375 {
376  tod_manager_->replace_local_schedule(schedule, active_area_);
377  if(!pure_map_) {
379  }
380 }
381 
383 {
384  config cfg;
385  config& multiplayer = cfg.add_child("multiplayer");
386  multiplayer.append_attributes(old_scenario);
387  std::string map_data = multiplayer["map_data"];
388  std::string separate_map_file = filesystem::get_current_editor_dir(addon_id_) + "/maps/" + filesystem::base_name(filename_, true) + ".map";
389 
390  // check that there's embedded map data, since that's how the editor used to save scenarios
391  if(!map_data.empty()) {
392  // check if a .map file already exists as a separate standalone .map in the editor folders or if a .map file already exists in the add-on
393  if(filesystem::file_exists(separate_map_file)) {
395  }
396  multiplayer["id"] = filesystem::base_name(separate_map_file, true);
397 
398  filesystem::write_file(separate_map_file, map_data);
399  multiplayer.remove_attribute("map_data");
400  multiplayer["map_file"] = filesystem::base_name(separate_map_file);
401  } else {
402  ERR_ED << "Cannot convert " << filename_ << " due to missing map_data attribute.";
403  throw editor_map_load_exception("load_scenario: no embedded map_data attribute found in old-style scenario", filename_);
404  }
405 
406  config& event = multiplayer.add_child("event");
407  event["name"] = "start";
408  event["id"] = "editor_event-start";
409 
410  // for all children that aren't [side] or [time], move them to an event
411  // for [side]:
412  // keep all attributes in [side]
413  // also keep any [village]s in [side]
414  // move all other children to the start [event]
415  // if [unit], set the unit's side
416  // for [time]:
417  // keep under [multiplayer]
418  for(const config::any_child child : old_scenario.all_children_range()) {
419  if(child.key != "side" && child.key != "time") {
420  config& c = event.add_child(child.key);
421  c.append_attributes(child.cfg);
422  c.append_children(child.cfg);
423  } else if(child.key == "side") {
424  config& c = multiplayer.add_child("side");
425  c.append_attributes(child.cfg);
426  for(const config::any_child side_child : child.cfg.all_children_range()) {
427  if(side_child.key == "village") {
428  config& c1 = c.add_child("village");
429  c1.append_attributes(side_child.cfg);
430  } else {
431  config& c1 = event.add_child(side_child.key);
432  c1.append_attributes(side_child.cfg);
433  if(side_child.key == "unit") {
434  c1["side"] = child.cfg["side"];
435  }
436  }
437  }
438  } else if(child.key == "time") {
439  config& c = multiplayer.add_child("time");
440  c.append_attributes(child.cfg);
441  }
442  }
443 
444  return cfg;
445 }
446 
448 {
449  config scen;
450  read(scen, *(preprocess_file(filename_)));
451 
452  config scenario;
453  if(scen.has_child("scenario")) {
454  scenario = scen.mandatory_child("scenario");
455  } else if(scen.has_child("multiplayer")) {
456  scenario = scen.mandatory_child("multiplayer");
457  } else if(scen.has_child("test")) {
458  scenario = scen.mandatory_child("test");
459  } else {
460  ERR_ED << "Found no [scenario], [multiplayer], or [test] tag in " << filename_ << ", assuming old-style editor scenario and defaulting to [multiplayer]";
461  scen = convert_scenario(scen);
462  scenario = scen.mandatory_child("multiplayer");
463  }
464 
465  scenario_id_ = scenario["id"].str();
466  scenario_name_ = scenario["name"].str();
467  scenario_description_ = scenario["description"].str();
468 
469  if(const config::attribute_value* experience_modifier = scenario.get("experience_modifier")) {
470  xp_mod_ = experience_modifier->to_int();
471  }
472  victory_defeated_ = scenario["victory_when_enemies_defeated"].to_bool(true);
473  random_time_ = scenario["random_start_time"].to_bool(false);
474 
475  if(!scenario["map_data"].str().empty()) {
476  map_ = editor_map::from_string(scenario["map_data"]); // throws on error
477  } else if(!scenario["map_file"].str().empty()) {
479  } else {
480  throw editor_map_load_exception("load_scenario: no map_file or map_data attribute found", filename_);
481  }
482 
483  for(config& side : scenario.child_range("side")) {
484  teams_.emplace_back();
485  teams_.back().build(side, map_);
486  if(!side["recruit"].str().empty()) {
487  teams_.back().set_recruits(utils::set_split(side["recruit"], ','));
488  }
489  }
490 
491  tod_manager_.reset(new tod_manager(scenario));
492 
493  auto event = scenario.find_child("event", "id", "editor_event-start");
494  if(event) {
495  config& evt = event.value();
496 
497  labels_.read(evt);
498 
499  for(const config& time_area : evt.child_range("time_area")) {
500  tod_manager_->add_time_area(map_, time_area);
501  }
502 
503  for(const config& item : evt.child_range("item")) {
504  const map_location loc(item);
505  overlays_[loc].push_back(overlay(item));
506  }
507 
508  for(const config& music : evt.child_range("music")) {
509  music_tracks_.emplace(music["name"], sound::music_track(music));
510  }
511 
512  for(config& a_unit : evt.child_range("unit")) {
513  units_.insert(unit::create(a_unit, true));
514  }
515  }
516 
517  previous_cfg_ = scen;
518 }
519 
521 {
522  return map_.set_selection(tod_manager_->get_area_by_index(index));
523 }
524 
525 void map_context::draw_terrain(const t_translation::terrain_code& terrain, const map_location& loc, bool one_layer_only)
526 {
527  t_translation::terrain_code full_terrain = one_layer_only
528  ? terrain
530 
531  draw_terrain_actual(full_terrain, loc, one_layer_only);
532 }
533 
535  const t_translation::terrain_code& terrain, const map_location& loc, bool one_layer_only)
536 {
537  if(!map_.on_board_with_border(loc)) {
538  // requests for painting off the map are ignored in set_terrain anyway,
539  // but ideally we should not have any
540  LOG_ED << "Attempted to draw terrain off the map (" << loc << ")";
541  return;
542  }
543 
544  t_translation::terrain_code old_terrain = map_.get_terrain(loc);
545 
546  if(terrain != old_terrain) {
547  if(terrain.base == t_translation::NO_LAYER) {
549  } else if(one_layer_only) {
551  } else {
552  map_.set_terrain(loc, terrain);
553  }
554 
556  }
557 }
558 
560  const t_translation::terrain_code& terrain, const std::set<map_location>& locs, bool one_layer_only)
561 {
562  t_translation::terrain_code full_terrain = one_layer_only
563  ? terrain
565 
566  for(const map_location& loc : locs) {
567  draw_terrain_actual(full_terrain, loc, one_layer_only);
568  }
569 }
570 
572 {
573  everything_changed_ = false;
574  changed_locations_.clear();
575 }
576 
578 {
579  if(!everything_changed()) {
580  changed_locations_.insert(loc);
581  }
582 }
583 
584 void map_context::add_changed_location(const std::set<map_location>& locs)
585 {
586  if(!everything_changed()) {
587  changed_locations_.insert(locs.begin(), locs.end());
588  }
589 }
590 
592 {
593  everything_changed_ = true;
594 }
595 
597 {
598  return everything_changed_;
599 }
600 
602 {
603  disp.labels().clear_all();
605 }
606 
608 {
609  std::set<map_location> new_label_locs = map_.set_starting_position_labels(disp);
610  starting_position_label_locs_.insert(new_label_locs.begin(), new_label_locs.end());
611 }
612 
614 {
617  set_needs_labels_reset(false);
618 }
619 
621 {
622  config scen;
623 
624  // the state of the previous scenario cfg
625  // if it exists, alter specific parts of it (sides, times, and editor events) rather than replacing it entirely
626  if(previous_cfg_) {
627  scen = *previous_cfg_;
628  }
629 
630  // if this has [multiplayer], use [multiplayer]
631  // else if this has [scenario], use [scenario]
632  // else if this has [test], use [test]
633  // else if none, add a [multiplayer]
634  config& scenario = scen.has_child("multiplayer")
635  ? scen.mandatory_child("multiplayer")
636  : scen.has_child("scenario")
637  ? scen.mandatory_child("scenario")
638  : scen.has_child("test")
639  ? scen.mandatory_child("test")
640  : scen.add_child("multiplayer");
641 
642  scenario.remove_children("side");
643  scenario.remove_children("event", [](config cfg){return cfg["id"].str() == "editor_event-start";});
644 
645  scenario["id"] = scenario_id_;
646  scenario["name"] = t_string(scenario_name_);
647  scenario["description"] = scenario_description_;
648 
649  if(xp_mod_) {
650  scenario["experience_modifier"] = *xp_mod_;
651  }
652  if(victory_defeated_) {
653  scenario["victory_when_enemies_defeated"] = *victory_defeated_;
654  }
655  scenario["random_start_time"] = random_time_;
656 
657  // write out the map data
658  scenario["map_file"] = scenario_id_ + ".map";
660 
661  // find or add the editor's start event
662  config& event = scenario.add_child("event");
663  event["name"] = "start";
664  event["id"] = "editor_event-start";
665 
666  // write out all the scenario data below
667 
668  // [time]s and [time_area]s
669  // put the [time_area]s into the event to keep as much editor-specific stuff separated in its own event as possible
670  config times = tod_manager_->to_config();
671  times.remove_attribute("turn_at");
672  times.remove_attribute("it_is_a_new_turn");
673  if(scenario["turns"].to_int() == -1) {
674  times.remove_attribute("turns");
675  } else {
676  scenario["turns"] = times["turns"];
677  }
678 
679  for(const config& time : times.child_range("time")) {
680  config& t = scenario.add_child("time");
681  t.append(time);
682  }
683  for(const config& time_area : times.child_range("time_area")) {
684  config& t = event.add_child("time_area");
685  t.append(time_area);
686  }
687 
688  // [label]s
689  labels_.write(event);
690 
691  // [item]s
692  for(const auto& overlay_pair : overlays_) {
693  for(const overlay& o : overlay_pair.second) {
694  config& item = event.add_child("item");
695 
696  // Write x,y location
697  overlay_pair.first.write(item);
698 
699  // These should always have a value
700  item["image"] = o.image;
701  item["visible_in_fog"] = o.visible_in_fog;
702 
703  // Optional keys
704  item["id"].write_if_not_empty(o.id);
705  item["name"].write_if_not_empty(o.name);
706  item["team_name"].write_if_not_empty(o.team_name);
707  item["halo"].write_if_not_empty(o.halo);
708  if(o.submerge) {
709  item["submerge"] = o.submerge;
710  }
711  }
712  }
713 
714  // [music]s
715  for(const music_map::value_type& track : music_tracks_) {
716  track.second.write(event, true);
717  }
718 
719  // [unit]s
720  for(const auto& unit : units_) {
721  config& u = event.add_child("unit");
722 
723  unit.get_location().write(u);
724 
725  u["side"] = unit.side();
726  u["type"] = unit.type_id();
727  u["name"].write_if_not_empty(unit.name());
728  u["facing"] = map_location::write_direction(unit.facing());
729 
730  if(!boost::regex_match(unit.id(), boost::regex(".*-[0-9]+"))) {
731  u["id"] = unit.id();
732  }
733 
734  if(unit.can_recruit()) {
735  u["canrecruit"] = unit.can_recruit();
736  }
737 
738  if(unit.unrenamable()) {
739  u["unrenamable"] = unit.unrenamable();
740  }
741  }
742 
743  // [side]s
744  for(const auto& team : teams_) {
745  config& side = scenario.add_child("side");
746 
747  side["side"] = scenario.child_count("side");
748  side["hidden"] = team.hidden();
749 
750  side["controller"] = side_controller::get_string(team.controller());
751  side["no_leader"] = team.no_leader();
752 
753  side["team_name"] = team.team_name();
754  side["user_team_name"].write_if_not_empty(team.user_team_name());
755  if(team.recruits().size() > 0) {
756  side["recruit"] = utils::join(team.recruits(), ",");
757  side["faction"] = "Custom";
758  }
759 
760  side["fog"] = team.uses_fog();
761  side["shroud"] = team.uses_shroud();
762  side["share_vision"] = team_shared_vision::get_string(team.share_vision());
763 
764  side["gold"] = team.gold();
765  side["income"] = team.base_income();
766 
767  for(const map_location& village : team.villages()) {
768  village.write(side.add_child("village"));
769  }
770  }
771 
772  previous_cfg_ = scen;
773  return scen;
774 }
775 
777 {
778  assert(!is_embedded());
779 
780  if(scenario_id_.empty()) {
782  }
783 
784  if(scenario_name_.empty()) {
786  }
787 
788  try {
789  std::stringstream wml_stream;
790  wml_stream
791  << "# This file was generated using the scenario editor.\n"
792  << "#\n"
793  << "# If you edit this file by hand, then do not use macros.\n"
794  << "# The editor doesn't support macros, and so using them will result in only being able to edit the map.\n"
795  << "# Additionally, the contents of all [side] and [time] tags as well as any events that have an id starting with 'editor_event-' are replaced entirely.\n"
796  << "# Any manual changes made to those will be lost.\n"
797  << "\n";
798  {
799  config_writer out(wml_stream, false);
800  out.write(to_config());
801  }
802 
803  if(!wml_stream.str().empty()) {
804  filesystem::write_file(get_filename(), wml_stream.str());
805  }
806 
807  clear_modified();
808  } catch(const filesystem::io_exception& e) {
809  utils::string_map symbols;
810  symbols["msg"] = e.what();
811  const std::string msg = VGETTEXT("Could not save the scenario: $msg", symbols);
812 
814  }
815 
816  // After saving the map as a scenario, it's no longer a pure map.
817  pure_map_ = false;
818 
819  // TODO the return value of this method does not need to be boolean.
820  // We either return true or there is an exception thrown.
821  return true;
822 }
823 
825 {
826  std::string map_data = map_.write();
827 
828  try {
829  if(!is_embedded()) {
831  } else {
832  std::string map_string = filesystem::read_file(get_filename());
833 
834  boost::regex rexpression_map_data(R"""((.*map_data\s*=\s*")(.+?)(".*))""");
835  boost::smatch matched_map_data;
836 
837  if(boost::regex_search(map_string, matched_map_data, rexpression_map_data,
838  boost::regex_constants::match_not_dot_null)) {
839  std::stringstream ss;
840  ss << matched_map_data[1];
841  ss << map_data;
842  ss << matched_map_data[3];
843 
845  } else {
846  throw editor_map_save_exception(_("Could not save into scenario"));
847  }
848  }
849 
851 
852  clear_modified();
853  } catch(const filesystem::io_exception& e) {
854  utils::string_map symbols;
855  symbols["msg"] = e.what();
856  const std::string msg = VGETTEXT("Could not save the map: $msg", symbols);
857 
859  }
860 
861  // TODO the return value of this method does not need to be boolean.
862  // We either return true or there is an exception thrown.
863  return true;
864 }
865 
867 {
868  if(map_.h() != map.h() || map_.w() != map.w()) {
870  } else {
872  }
873 
874  map_ = map;
875 }
876 
878 {
879  LOG_ED << "Performing action " << action.get_id() << ": " << action.get_name() << ", actions count is "
880  << action.get_instance_count();
881  auto undo = action.perform(*this);
882  if(actions_since_save_ < 0) {
883  // set to a value that will make it impossible to get to zero, as at this point
884  // it is no longer possible to get back the original map state using undo/redo
885  actions_since_save_ = 1 + undo_stack_.size();
886  }
887 
889 
890  undo_stack_.emplace_back(std::move(undo));
891 
893 
894  redo_stack_.clear();
895 }
896 
898 {
899  LOG_ED << "Performing (partial) action " << action.get_id() << ": " << action.get_name() << ", actions count is "
900  << action.get_instance_count();
901  if(!can_undo()) {
902  throw editor_logic_exception("Empty undo stack in perform_partial_action()");
903  }
904 
905  editor_action_chain* undo_chain = dynamic_cast<editor_action_chain*>(last_undo_action());
906  if(undo_chain == nullptr) {
907  throw editor_logic_exception("Last undo action not a chain in perform_partial_action()");
908  }
909 
910  auto undo = action.perform(*this);
911 
912  // actions_since_save_ += action.action_count();
913  undo_chain->prepend_action(std::move(undo));
914 
915  redo_stack_.clear();
916 }
917 
919 {
920  return actions_since_save_ != 0;
921 }
922 
924 {
926 }
927 
929 {
931 }
932 
934 {
935  return !undo_stack_.empty();
936 }
937 
939 {
940  return !redo_stack_.empty();
941 }
942 
944 {
945  return undo_stack_.empty() ? nullptr : undo_stack_.back().get();
946 }
947 
949 {
950  return redo_stack_.empty() ? nullptr : redo_stack_.back().get();
951 }
952 
954 {
955  return undo_stack_.empty() ? nullptr : undo_stack_.back().get();
956 }
957 
959 {
960  return redo_stack_.empty() ? nullptr : redo_stack_.back().get();
961 }
962 
964 {
965  LOG_ED << "undo() beg, undo stack is " << undo_stack_.size() << ", redo stack " << redo_stack_.size();
966 
967  if(can_undo()) {
970  } else {
971  WRN_ED << "undo() called with an empty undo stack";
972  }
973 
974  LOG_ED << "undo() end, undo stack is " << undo_stack_.size() << ", redo stack " << redo_stack_.size();
975 }
976 
978 {
979  LOG_ED << "redo() beg, undo stack is " << undo_stack_.size() << ", redo stack " << redo_stack_.size();
980 
981  if(can_redo()) {
984  } else {
985  WRN_ED << "redo() called with an empty redo stack";
986  }
987 
988  LOG_ED << "redo() end, undo stack is " << undo_stack_.size() << ", redo stack " << redo_stack_.size();
989 }
990 
992 {
993  // callers should check for these conditions
994  if(!can_undo()) {
995  throw editor_logic_exception("Empty undo stack in partial_undo()");
996  }
997 
998  editor_action_chain* undo_chain = dynamic_cast<editor_action_chain*>(last_undo_action());
999  if(undo_chain == nullptr) {
1000  throw editor_logic_exception("Last undo action not a chain in partial undo");
1001  }
1002 
1003  // a partial undo performs the first action form the current action's action_chain that would be normally performed
1004  // i.e. the *first* one.
1005  const auto first_action_in_chain = undo_chain->pop_first_action();
1006  if(undo_chain->empty()) {
1008  undo_stack_.pop_back();
1009  }
1010 
1011  redo_stack_.emplace_back(first_action_in_chain->perform(*this));
1012  // actions_since_save_ -= last_redo_action()->action_count();
1013 }
1014 
1016 {
1017  undo_stack_.clear();
1018  redo_stack_.clear();
1019 }
1020 
1022 {
1023  if(stack.size() > max_action_stack_size_) {
1024  stack.pop_front();
1025  }
1026 }
1027 
1029 {
1030  assert(!from.empty());
1031 
1032  std::unique_ptr<editor_action> action;
1033  action.swap(from.back());
1034 
1035  from.pop_back();
1036 
1037  auto reverse_action = action->perform(*this);
1038  to.emplace_back(std::move(reverse_action));
1039 
1040  trim_stack(to);
1041 }
1042 
1044 {
1045  return is_pure_map() ? _("New Map") : _("New Scenario");
1046 }
1047 
1048 } // end namespace editor
std::string filename_
Definition: action_wml.cpp:555
double t
Definition: astarsearch.cpp:65
Variant for storing WML attributes.
Class for writing a config out to a file in pieces.
void write(const config &cfg)
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:161
config & mandatory_child(config_key_type key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition: config.cpp:371
std::size_t child_count(config_key_type key) const
Definition: config.cpp:301
optional_config_impl< config > find_child(config_key_type key, const std::string &name, const std::string &value)
Returns the first child of tag key with a name attribute containing value.
Definition: config.cpp:791
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:321
const_all_children_itors all_children_range() const
In-order iteration over all children.
Definition: config.cpp:891
child_itors child_range(config_key_type key)
Definition: config.cpp:277
void append_attributes(const config &cfg)
Adds attributes from cfg.
Definition: config.cpp:194
void remove_attribute(config_key_type key)
Definition: config.cpp:164
void remove_children(config_key_type key, std::function< bool(const config &)> p=[](config){return true;})
Removes all children with tag key for which p returns true.
Definition: config.cpp:660
const attribute_value * get(config_key_type key) const
Returns a pointer to the attribute with the given key or nullptr if it does not exist.
Definition: config.cpp:691
config & add_child(config_key_type key)
Definition: config.cpp:445
Sort-of-Singleton that many classes, both GUI and non-GUI, use to access the game data.
Definition: display.hpp:87
map_labels & labels()
Definition: display.cpp:2572
Container action wrapping several actions into one.
Definition: action.hpp:88
std::unique_ptr< editor_action > pop_first_action()
Remove the first added action and return it, transferring ownership to the caller.
Definition: action.cpp:142
void prepend_action(std::unique_ptr< editor_action > a)
Add an action at the beginning of the chain.
Definition: action.cpp:121
Base class for all editor actions.
Definition: action_base.hpp:42
int get_id() const
Debugging aid.
Definition: action_base.hpp:93
virtual const std::string & get_name() const
Definition: action_base.hpp:75
virtual std::unique_ptr< editor_action > perform(map_context &) const
Perform the action, returning an undo action that, when performed, shall reverse any effects of this ...
Definition: action.cpp:64
static int get_instance_count()
Debugging aid.
This class adds extra editor-specific functionality to a normal gamemap.
Definition: editor_map.hpp:71
bool set_selection(const std::set< map_location > &area)
Select the given area.
Definition: editor_map.cpp:168
std::set< map_location > set_starting_position_labels(display &disp)
Set labels for staring positions in the given display object.
Definition: editor_map.cpp:136
static editor_map from_string(const std::string &data)
Wrapper around editor_map(cfg, data) that catches possible exceptions and wraps them in a editor_map_...
Definition: editor_map.cpp:55
std::set< map_location > starting_position_label_locs_
Cache of set starting position labels.
std::string filename_
The actual filename of this map.
void perform_partial_action(const editor_action &action)
Performs a partial action, assumes that the top undo action has been modified to maintain coherent st...
std::optional< int > xp_mod_
bool modified() const
bool pure_map_
Whether the map context refers to a file containing only the pure map data.
action_stack redo_stack_
The redo stack.
std::unique_ptr< tod_manager > tod_manager_
bool embedded_
Whether the map context refers to a map embedded in a scenario file.
void set_needs_labels_reset(bool value=true)
Setter for the labels reset flag.
void set_needs_reload(bool value=true)
Setter for the reload flag.
std::string scenario_name_
void clear_starting_position_labels(display &disp)
editor_map map_
The map object of this map_context.
void new_side()
Adds a new side to the map.
static const std::size_t max_action_stack_size_
Action stack (i.e.
action_stack undo_stack_
The undo stack.
editor_action * last_undo_action()
std::optional< config > previous_cfg_
void redo()
Re-does a previously undid action, and puts it back in the undo stack.
std::set< map_location > changed_locations_
void set_starting_time(int time)
void draw_terrain(const t_translation::terrain_code &terrain, const map_location &loc, bool one_layer_only=false)
Draw a terrain on a single location on the map.
void set_map(const editor_map &map)
bool select_area(int index)
Select the nth tod area.
bool can_redo() const
void set_side_setup(editor_team_info &info)
void trim_stack(action_stack &stack)
Checks if an action stack reached its capacity and removes the front element if so.
bool can_undo() const
int actions_since_save_
Number of actions performed since the map was saved.
std::string scenario_description_
std::vector< team > teams_
void perform_action_between_stacks(action_stack &from, action_stack &to)
Perform an action at the back of one stack, and then move it to the back of the other stack.
void perform_action(const editor_action &action)
Performs an action (thus modifying the map).
void undo()
Un-does the last action, and puts it in the redo stack for a possible redo.
void replace_schedule(const std::vector< time_of_day > &schedule)
void remove_area(int index)
void clear_undo_redo()
Clear the undo and redo stacks.
bool is_embedded() const
virtual ~map_context()
Map context destructor.
void draw_terrain_actual(const t_translation::terrain_code &terrain, const map_location &loc, bool one_layer_only=false)
Actual drawing function used by both overloaded variants of draw_terrain.
void clear_changed_locations()
map_context(const map_context &)=delete
void add_to_recent_files()
Adds the map to the editor's recent files list.
std::string scenario_id_
void replace_local_schedule(const std::vector< time_of_day > &schedule)
Replace the [time]s of the currently active area.
bool save_scenario()
Saves the scenario under the current filename.
void reset_starting_position_labels(display &disp)
editor_action * last_redo_action()
void partial_undo()
Un-does a single step from a undo action chain.
virtual const editor_map & map() const override
Const map accessor.
void set_needs_terrain_rebuild(bool value=true)
Setter for the terrain rebuild flag.
config convert_scenario(const config &old_scenario)
Convert an old-style editor scenario config to a config with a top level [multiplayer] tag.
bool everything_changed() const
const t_string get_default_context_name() const
const std::string & get_filename() const
void set_starting_position_labels(display &disp)
bool save_map()
Saves the map under the current filename.
std::optional< bool > victory_defeated_
void clear_modified()
Clear the modified state.
overlay_map overlays_
bool is_pure_map() const
bool victory_defeated() const
std::string addon_id_
void set_scenario_setup(const std::string &id, const std::string &name, const std::string &description, int turns, int xp_mod, bool victory_defeated, bool random_time)
void add_changed_location(const map_location &loc)
A class grating read only view to a vector of config objects, viewed as one config with all children ...
terrain_code get_terrain(const map_location &loc) const
Looks up terrain at a particular location.
Definition: map.cpp:302
int w() const
Effective map width.
Definition: map.hpp:50
int h() const
Effective map height.
Definition: map.hpp:53
bool on_board_with_border(const map_location &loc) const
Definition: map.cpp:390
std::string write() const
Definition: map.cpp:210
const terrain_type & get_terrain_info(const t_translation::terrain_code &terrain) const
Definition: map.cpp:98
void set_terrain(const map_location &loc, const terrain_code &terrain, const terrain_type_data::merge_mode mode=terrain_type_data::BOTH, bool replace_if_failed=false) override
Clobbers over the terrain at location 'loc', with the given terrain.
Definition: map.cpp:397
@ auto_close
Enables auto close.
Definition: message.hpp:71
void write(config &res) const
Definition: label.cpp:80
void clear_all()
Definition: label.cpp:240
void read(const config &cfg)
Definition: label.cpp:92
Internal representation of music tracks.
This class stores all the data for a single 'side' (in game nomenclature).
Definition: team.hpp:76
bool uses_shroud() const
Definition: team.hpp:305
const std::string & team_name() const
Definition: team.hpp:284
bool no_leader() const
Definition: team.hpp:329
team_shared_vision::type share_vision() const
Definition: team.hpp:379
const std::set< map_location > & villages() const
Definition: team.hpp:172
int gold() const
Definition: team.hpp:177
side_controller::type controller() const
Definition: team.hpp:243
int base_income() const
Definition: team.hpp:179
bool uses_fog() const
Definition: team.hpp:306
bool hidden() const
Definition: team.hpp:335
const std::set< std::string > & recruits() const
Definition: team.hpp:211
const t_string & user_team_name() const
Definition: team.hpp:285
t_translation::terrain_code terrain_with_default_base() const
Definition: terrain.cpp:297
umap_retval_pair_t insert(unit_ptr p)
Inserts the unit pointed to by p into the map.
Definition: map.cpp:134
This class represents a single unit of a specific type.
Definition: unit.hpp:134
static unit_ptr create(const config &cfg, bool use_traits=false, const vconfig *vcfg=nullptr)
Initializes a unit from a config.
Definition: unit.hpp:202
Editor action classes.
#define LOG_ED
lg::log_domain log_editor
#define ERR_ED
#define WRN_ED
Declarations for File-IO.
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
static std::string _(const char *str)
Definition: gettext.hpp:93
bool unrenamable() const
Whether this unit can be renamed.
Definition: unit.hpp:437
const std::string & type_id() const
The id of this unit's type.
Definition: unit.cpp:1935
bool can_recruit() const
Whether this unit can recruit other units - ie, are they a leader unit.
Definition: unit.hpp:613
const std::string & id() const
Gets this unit's id.
Definition: unit.hpp:381
int side() const
The side this unit belongs to.
Definition: unit.hpp:344
const t_string & name() const
Gets this unit's translatable display name.
Definition: unit.hpp:404
const map_location & get_location() const
The current map location this unit is at.
Definition: unit.hpp:1358
map_location::DIRECTION facing() const
The current direction this unit is facing within its hex.
Definition: unit.hpp:1374
std::string id
Text to match against addon_info.tags()
Definition: manager.cpp:215
#define log_scope2(domain, description)
Definition: log.hpp:242
Manage the empty-palette in the editor.
Definition: action.cpp:31
static std::string get_map_location(const std::string &file_contents, const std::string &attr)
Definition: map_context.cpp:94
std::deque< std::unique_ptr< editor_action > > action_stack
Action stack typedef.
EXIT_STATUS start(bool clear_id, const std::string &filename, bool take_screenshot, const std::string &screenshot_filename)
Main interface for launching the editor from the title screen.
std::string base_name(const std::string &file, const bool remove_extension)
Returns the base filename of a file, with directory name stripped.
static bool file_exists(const bfs::path &fpath)
Definition: filesystem.cpp:321
bool is_directory(const std::string &fname)
Returns true if the given file is a directory.
std::string get_wml_location(const std::string &filename, const std::string &current_dir)
Returns a complete path to the actual WML file or directory or an empty string if the file isn't pres...
std::string read_file(const std::string &fname)
Basic disk I/O - read file.
bool ends_with(const std::string &str, const std::string &suffix)
void write_file(const std::string &fname, const std::string &data, std::ios_base::openmode mode)
Throws io_exception if an error occurs.
std::string get_short_wml_path(const std::string &filename)
Returns a short path to filename, skipping the (user) data directory.
std::string directory_name(const std::string &file)
Returns the directory name of a file, with filename stripped.
std::string get_next_filename(const std::string &name, const std::string &extension)
Get the next free filename using "name + number (3 digits) + extension" maximum 1000 files then start...
Definition: filesystem.cpp:548
std::string get_current_editor_dir(const std::string &addon_id)
Game configuration data as global variables.
Definition: build_info.cpp:63
int village_income
Definition: game_config.cpp:37
int village_support
Definition: game_config.cpp:38
void show_message(const std::string &title, const std::string &msg, const std::string &button_caption, const bool auto_close, const bool message_use_markup, const bool title_use_markup)
Shows a message to the user.
Definition: message.cpp:151
std::pair< std::string, unsigned > item
Definition: help_impl.hpp:414
logger & info()
Definition: log.cpp:238
void add_recent_files_entry(const std::string &path)
Adds an entry to the recent files list.
Definition: editor.cpp:124
bool fog()
Definition: game.cpp:525
bool shroud()
Definition: game.cpp:535
int village_gold()
Definition: game.cpp:651
int turns()
Definition: game.cpp:545
::tod_manager * tod_manager
Definition: resources.cpp:30
const ter_layer NO_LAYER
Definition: translation.hpp:40
std::size_t index(const std::string &str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:72
void trim(std::string_view &s)
std::set< std::string > set_split(const std::string &val, const char c=',', const int flags=REMOVE_EMPTY|STRIP_SPACES)
Splits a (comma-)separated string into a set of pieces.
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
std::map< std::string, t_string > string_map
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:110
filesystem::scoped_istream preprocess_file(const std::string &fname, preproc_map *defines)
Function to use the WML preprocessor on a file.
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:627
config & cfg
Definition: config.hpp:687
editor_team_info(const team &t)
Definition: map_context.cpp:40
An exception object used when an IO error occurs.
Definition: filesystem.hpp:64
Encapsulates the map of the game.
Definition: location.hpp:38
void write(config &cfg) const
Definition: location.cpp:212
static std::string write_direction(DIRECTION dir)
Definition: location.cpp:141
std::string image
Definition: overlay.hpp:55
std::string team_name
Definition: overlay.hpp:57
float submerge
Definition: overlay.hpp:63
t_string name
Definition: overlay.hpp:58
std::string id
Definition: overlay.hpp:59
std::string halo
Definition: overlay.hpp:56
bool visible_in_fog
Definition: overlay.hpp:62
static std::string get_string(enum_type key)
Converts a enum to its string equivalent.
Definition: enum_base.hpp:46
A terrain string which is converted to a terrain is a string with 1 or 2 layers the layers are separa...
Definition: translation.hpp:49
mock_char c
#define e