The Battle for Wesnoth  1.19.0-dev
map_context.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2024
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 "gettext.hpp"
25 #include "gui/dialogs/message.hpp"
26 #include "map/label.hpp"
27 #include "preferences/editor.hpp"
29 #include "serialization/parser.hpp"
31 #include "team.hpp"
32 #include "units/unit.hpp"
33 #include "game_config_view.hpp"
34 
35 #include <boost/regex.hpp>
36 
37 namespace editor
38 {
40  : side(t.side())
41  , id(t.team_name())
42  , name(t.user_team_name())
43  , recruit_list(utils::join(t.recruits(), ","))
44  , gold(t.gold())
45  , income(t.base_income())
48  , fog(t.uses_fog())
49  , shroud(t.uses_shroud())
50  , share_vision(t.share_vision())
52  , no_leader(t.no_leader())
53  , hidden(t.hidden())
54 {
55 }
56 
57 const std::size_t map_context::max_action_stack_size_ = 100;
58 
59 map_context::map_context(const editor_map& map, bool pure_map, const config& schedule, const std::string& addon_id)
60  : filename_()
61  , map_data_key_()
62  , embedded_(false)
63  , pure_map_(pure_map)
64  , map_(map)
65  , undo_stack_()
66  , redo_stack_()
67  , actions_since_save_(0)
68  , starting_position_label_locs_()
69  , needs_reload_(false)
70  , needs_terrain_rebuild_(false)
71  , needs_labels_reset_(false)
72  , changed_locations_()
73  , everything_changed_(false)
74  , addon_id_(addon_id)
75  , previous_cfg_()
76  , scenario_id_()
77  , scenario_name_()
78  , scenario_description_()
79  , xp_mod_()
80  , victory_defeated_(true)
81  , random_time_(false)
82  , active_area_(-1)
83  , labels_(nullptr)
84  , units_()
85  , teams_()
86  , tod_manager_(new tod_manager(schedule))
87  , mp_settings_()
88  , game_classification_()
89  , music_tracks_()
90 {
91 }
92 
93 static std::string get_map_location(const std::string& file_contents, const std::string& attr)
94 {
95  std::size_t attr_name_start = file_contents.find(attr);
96  if(attr_name_start == std::string::npos) return "";
97 
98  std::size_t attr_value_start = file_contents.find("=", attr_name_start);
99  std::size_t line_end = file_contents.find("\n", attr_name_start);
100  if(line_end < attr_value_start) return "";
101 
102  attr_value_start++;
103  std::string attr_value = file_contents.substr(attr_value_start, line_end - attr_value_start);
104  std::string_view v2 = attr_value;
105  utils::trim(v2);
106 
107  return std::string(v2);
108 }
109 
110 map_context::map_context(const game_config_view& game_config, const std::string& filename, const std::string& addon_id)
111  : filename_(filename)
112  , map_data_key_()
113  , embedded_(false)
114  , pure_map_(false)
115  , map_()
116  , undo_stack_()
117  , redo_stack_()
118  , actions_since_save_(0)
119  , starting_position_label_locs_()
120  , needs_reload_(false)
121  , needs_terrain_rebuild_(false)
122  , needs_labels_reset_(false)
123  , changed_locations_()
124  , everything_changed_(false)
125  , addon_id_(addon_id)
126  , previous_cfg_()
127  , scenario_id_()
128  , scenario_name_()
129  , scenario_description_()
130  , xp_mod_()
131  , victory_defeated_(true)
132  , random_time_(false)
133  , active_area_(-1)
134  , labels_(nullptr)
135  , units_()
136  , teams_()
137  , tod_manager_(new tod_manager(game_config.find_mandatory_child("editor_times", "id", "empty")))
138  , mp_settings_()
139  , game_classification_()
140  , music_tracks_()
141 {
142  /*
143  * Overview of situations possibly found in the file:
144  *
145  * embedded_ - the map data is directly in the scenario file
146  * pure_map_ - the map data is in its own separate file (map_file, map_data+macro inclusion) or this is a .map file
147  *
148  * 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
149  *
150  * 0. Not a scenario or map file.
151  * 0.1 File not found
152  * 0.2 Map file empty
153  * 0.3 Not a .map or .cfg file
154  * 1. It's a .map file.
155  * * embedded_ = false
156  * * pure_map_ = true
157  * 2. A scenario embedding the map
158  * * embedded_ = true
159  * * pure_map_ = true
160  * The scenario-test.cfg for example.
161  * The map is written back to the file.
162  * 3. The map file is referenced by map_data={MACRO_ARGUEMENT}.
163  * * embedded_ = false
164  * * pure_map_ = true
165  * 4. The file contains an editor generated scenario file.
166  * * embedded_ = false
167  * * pure_map_ = false
168  * 5. The file is using map_file.
169  * 5.1 The file doesn't contain a macro and so can be loaded by the editor as a scenario
170  * * embedded_ = false
171  * * pure_map_ = false
172  * 5.2 The file contains a macro and so can't be loaded by the editor as a scenario
173  * * embedded_ = false
174  * * pure_map_ = true
175  */
176 
177  log_scope2(log_editor, "Loading file " + filename);
178 
179  // 0.1 File not found
180  if(!filesystem::file_exists(filename) || filesystem::is_directory(filename)) {
181  throw editor_map_load_exception(filename, _("File not found"));
182  }
183 
184  std::string file_string = filesystem::read_file(filename);
185 
186  // 0.2 Map file empty
187  if(file_string.empty()) {
188  std::string message = _("Empty file");
189  throw editor_map_load_exception(filename, message);
190  }
191 
192  // 0.3 Not a .map or .cfg file
193  if(!filesystem::ends_with(filename, ".map") && !filesystem::ends_with(filename, ".cfg")) {
194  std::string message = _("File does not have .map or .cfg extension");
195  throw editor_map_load_exception(filename, message);
196  }
197 
198  // 1.0 Pure map data
199  if(filesystem::ends_with(filename, ".map") || filesystem::ends_with(filename, ".mask")) {
200  LOG_ED << "Loading map or mask file";
201  map_ = editor_map::from_string(file_string); // throws on error
202  pure_map_ = true;
203 
205  } else {
206  // 4.0 old-style editor generated scenario which lacks a top-level tag
207  if(file_string.find("[multiplayer]") == std::string::npos &&
208  file_string.find("[scenario]") == std::string::npos &&
209  file_string.find("[test]") == std::string::npos) {
210  LOG_ED << "Loading generated scenario file";
211  try {
212  load_scenario();
213  } catch(const std::exception& e) {
214  throw editor_map_load_exception("load_scenario: old-style scenario", e.what());
215  }
217  } else {
218  std::string map_data_loc = get_map_location(file_string, "map_data");
219  std::string map_file_loc = get_map_location(file_string, "map_file");
220 
221  if(!map_data_loc.empty()) {
222  if(map_data_loc.find("\"{") == std::string::npos) {
223  // 2.0 Embedded pure map
224  LOG_ED << "Loading embedded map file";
225  embedded_ = true;
226  pure_map_ = true;
227  std::size_t start = file_string.find(map_data_loc)+1;
228  std::size_t length = file_string.find("\"", start)-start;
229  std::string map_data = file_string.substr(start, length);
230  map_ = editor_map::from_string(map_data);
232  } else {
233  // 3.0 Macro referenced pure map
234  const std::string& macro_argument = map_data_loc.substr(2, map_data_loc.size()-4);
235  LOG_ED << "Map looks like a scenario, trying {" << macro_argument << "}";
236 
238 
239  if(new_filename.empty()) {
240  std::string message = _("The map file looks like a scenario, but the map_data value does not point to an existing file")
241  + std::string("\n") + macro_argument;
242  throw editor_map_load_exception(filename, message);
243  }
244 
245  LOG_ED << "New filename is: " << new_filename;
246 
247  filename_ = new_filename;
248  file_string = filesystem::read_file(filename_);
249  map_ = editor_map::from_string(file_string);
250  pure_map_ = true;
251 
253  }
254  } else if(!map_file_loc.empty()) {
255  // 5.0 The file is using map_file.
256  try {
257  // 5.1 The file can be loaded by the editor as a scenario
258  if(file_string.find("<<") != std::string::npos) {
259  throw editor_map_load_exception(filename, _("Found the characters '<<' indicating inline lua is present - aborting"));
260  }
261  load_scenario();
262  } catch(const std::exception&) {
263  // 5.2 The file can't be loaded by the editor as a scenario, so try to just load the map
264  gui2::show_message(_("Error"), _("Failed to load the scenario, attempting to load only the map."), gui2::dialogs::message::auto_close);
265 
266  // NOTE: this means that loading the map file from a scenario where the maps are in nested directories under maps/ will not work
267  // this is done to address mainline scenarios referencing their maps as "multiplayer/maps/<map_file>.map"
268  // otherwise this results in the "multiplayer/maps/" part getting duplicated in the path and then not being found
269  std::string new_filename = filesystem::get_current_editor_dir(addon_id_) + "/maps/" + filesystem::base_name(map_file_loc);
270  if(!filesystem::file_exists(new_filename)) {
271  std::string message = _("The map file looks like a scenario, but the map_file value does not point to an existing file")
272  + std::string("\n") + new_filename;
273  throw editor_map_load_exception(filename, message);
274  }
275 
276  LOG_ED << "New filename is: " << new_filename;
277 
278  filename_ = new_filename;
279  file_string = filesystem::read_file(filename_);
280  map_ = editor_map::from_string(file_string);
281  pure_map_ = true;
282  }
283 
285  } else {
286  throw editor_map_load_exception(filename, _("Unable to parse file to find map data"));
287  }
288  }
289  }
290 }
291 
293 {
294  undo_stack_.clear();
295  redo_stack_.clear();
296 }
297 
299 {
300  teams_.emplace_back();
301 
302  config cfg;
303  cfg["side"] = teams_.size(); // side is 1-indexed, so we can just use size()
304  cfg["hidden"] = false;
305 
306  teams_.back().build(cfg, map());
307 
309 }
310 
312 {
313  assert(teams_.size() >= static_cast<unsigned int>(info.side));
314 
315  team& t = teams_[info.side - 1];
316  t.change_team(info.id, info.name);
317  t.set_recruits(utils::split_set(info.recruit_list, ','));
318  t.have_leader(!info.no_leader);
319  t.change_controller(info.controller);
320  t.set_gold(info.gold);
321  t.set_base_income(info.income);
322  t.set_hidden(info.hidden);
323  t.set_fog(info.fog);
324  t.set_shroud(info.shroud);
325  t.set_share_vision(info.share_vision);
326  t.set_village_gold(info.village_income);
327  t.set_village_support(info.village_support);
328 
330 }
331 
332 void map_context::set_scenario_setup(const std::string& id,
333  const std::string& name,
334  const std::string& description,
335  int turns,
336  int xp_mod,
337  bool victory_defeated,
338  bool random_time)
339 {
340  scenario_id_ = id;
341  scenario_name_ = name;
342  scenario_description_ = description;
343  random_time_ = random_time;
345  tod_manager_->set_number_of_turns(turns);
346  xp_mod_ = xp_mod;
348 }
349 
351 {
352  tod_manager_->set_current_time(time);
353  if(!pure_map_) {
355  }
356 }
357 
359 {
360  tod_manager_->remove_time_area(index);
361  active_area_--;
363 }
364 
365 void map_context::replace_schedule(const std::vector<time_of_day>& schedule)
366 {
367  tod_manager_->replace_schedule(schedule);
368  if(!pure_map_) {
370  }
371 }
372 
373 void map_context::replace_local_schedule(const std::vector<time_of_day>& schedule)
374 {
375  tod_manager_->replace_local_schedule(schedule, active_area_);
376  if(!pure_map_) {
378  }
379 }
380 
382 {
383  config cfg;
384  config& multiplayer = cfg.add_child("multiplayer");
385  multiplayer.append_attributes(old_scenario);
386  std::string map_data = multiplayer["map_data"];
387  std::string separate_map_file = filesystem::get_current_editor_dir(addon_id_) + "/maps/" + filesystem::base_name(filename_, true) + ".map";
388 
389  // check that there's embedded map data, since that's how the editor used to save scenarios
390  if(!map_data.empty()) {
391  // 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
392  if(filesystem::file_exists(separate_map_file)) {
394  }
395  multiplayer["id"] = filesystem::base_name(separate_map_file, true);
396 
397  filesystem::write_file(separate_map_file, map_data);
398  multiplayer.remove_attribute("map_data");
399  multiplayer["map_file"] = filesystem::base_name(separate_map_file);
400  } else {
401  ERR_ED << "Cannot convert " << filename_ << " due to missing map_data attribute.";
402  throw editor_map_load_exception("load_scenario: no embedded map_data attribute found in old-style scenario", filename_);
403  }
404 
405  config& event = multiplayer.add_child("event");
406  event["name"] = "prestart";
407  event["id"] = "editor_event-prestart";
408 
409  // for all children that aren't [side] or [time], move them to an event
410  // for [side]:
411  // keep all attributes in [side]
412  // also keep any [village]s in [side]
413  // move all other children to the start [event]
414  // if [unit], set the unit's side
415  // for [time]:
416  // keep under [multiplayer]
417  for(const config::any_child child : old_scenario.all_children_range()) {
418  if(child.key != "side" && child.key != "time") {
419  config& c = event.add_child(child.key);
420  c.append_attributes(child.cfg);
421  c.append_children(child.cfg);
422  } else if(child.key == "side") {
423  config& c = multiplayer.add_child("side");
424  c.append_attributes(child.cfg);
425  for(const config::any_child side_child : child.cfg.all_children_range()) {
426  if(side_child.key == "village") {
427  config& c1 = c.add_child("village");
428  c1.append_attributes(side_child.cfg);
429  } else {
430  config& c1 = event.add_child(side_child.key);
431  c1.append_attributes(side_child.cfg);
432  if(side_child.key == "unit") {
433  c1["side"] = child.cfg["side"];
434  }
435  }
436  }
437  } else if(child.key == "time") {
438  config& c = multiplayer.add_child("time");
439  c.append_attributes(child.cfg);
440  }
441  }
442 
443  return cfg;
444 }
445 
447 {
448  config scen;
449  read(scen, *(preprocess_file(filename_)));
450 
451  config scenario;
452  if(scen.has_child("scenario")) {
453  scenario = scen.mandatory_child("scenario");
454  } else if(scen.has_child("multiplayer")) {
455  scenario = scen.mandatory_child("multiplayer");
456  } else if(scen.has_child("test")) {
457  scenario = scen.mandatory_child("test");
458  } else {
459  ERR_ED << "Found no [scenario], [multiplayer], or [test] tag in " << filename_ << ", assuming old-style editor scenario and defaulting to [multiplayer]";
460  scen = convert_scenario(scen);
461  scenario = scen.mandatory_child("multiplayer");
462  }
463 
464  scenario_id_ = scenario["id"].str();
465  scenario_name_ = scenario["name"].str();
466  scenario_description_ = scenario["description"].str();
467 
468  if(const config::attribute_value* experience_modifier = scenario.get("experience_modifier")) {
469  xp_mod_ = experience_modifier->to_int();
470  }
471  victory_defeated_ = scenario["victory_when_enemies_defeated"].to_bool(true);
472  random_time_ = scenario["random_start_time"].to_bool(false);
473 
474  if(!scenario["map_data"].str().empty()) {
475  map_ = editor_map::from_string(scenario["map_data"]); // throws on error
476  } else if(!scenario["map_file"].str().empty()) {
478  } else {
479  throw editor_map_load_exception("load_scenario: no map_file or map_data attribute found", filename_);
480  }
481 
482  for(config& side : scenario.child_range("side")) {
483  teams_.emplace_back();
484  teams_.back().build(side, map_);
485  if(!side["recruit"].str().empty()) {
486  teams_.back().set_recruits(utils::split_set(side["recruit"].str(), ','));
487  }
488  }
489 
490  tod_manager_.reset(new tod_manager(scenario));
491 
492  auto event = scenario.find_child("event", "id", "editor_event-start");
493  if(!event) {
494  event = scenario.find_child("event", "id", "editor_event-prestart");
495  }
496  if(event) {
497  config& evt = event.value();
498 
499  labels_.read(evt);
500 
501  for(const config& time_area : evt.child_range("time_area")) {
502  tod_manager_->add_time_area(map_, time_area);
503  }
504 
505  for(const config& item : evt.child_range("item")) {
506  const map_location loc(item);
507  overlays_[loc].push_back(overlay(item));
508  }
509 
510  for(const config& music : evt.child_range("music")) {
511  music_tracks_.emplace(music["name"], sound::music_track(music));
512  }
513 
514  for(config& a_unit : evt.child_range("unit")) {
515  units_.insert(unit::create(a_unit, true));
516  }
517  }
518 
519  previous_cfg_ = scen;
520 }
521 
523 {
524  return map_.set_selection(tod_manager_->get_area_by_index(index));
525 }
526 
527 void map_context::draw_terrain(const t_translation::terrain_code& terrain, const map_location& loc, bool one_layer_only)
528 {
529  t_translation::terrain_code full_terrain = one_layer_only
530  ? terrain
532 
533  draw_terrain_actual(full_terrain, loc, one_layer_only);
534 }
535 
537  const t_translation::terrain_code& terrain, const map_location& loc, bool one_layer_only)
538 {
539  if(!map_.on_board_with_border(loc)) {
540  // requests for painting off the map are ignored in set_terrain anyway,
541  // but ideally we should not have any
542  LOG_ED << "Attempted to draw terrain off the map (" << loc << ")";
543  return;
544  }
545 
546  t_translation::terrain_code old_terrain = map_.get_terrain(loc);
547 
548  if(terrain != old_terrain) {
549  if(terrain.base == t_translation::NO_LAYER) {
551  } else if(one_layer_only) {
553  } else {
554  map_.set_terrain(loc, terrain);
555  }
556 
558  }
559 }
560 
562  const t_translation::terrain_code& terrain, const std::set<map_location>& locs, bool one_layer_only)
563 {
564  t_translation::terrain_code full_terrain = one_layer_only
565  ? terrain
567 
568  for(const map_location& loc : locs) {
569  draw_terrain_actual(full_terrain, loc, one_layer_only);
570  }
571 }
572 
574 {
575  everything_changed_ = false;
576  changed_locations_.clear();
577 }
578 
580 {
581  if(!everything_changed()) {
582  changed_locations_.insert(loc);
583  }
584 }
585 
586 void map_context::add_changed_location(const std::set<map_location>& locs)
587 {
588  if(!everything_changed()) {
589  changed_locations_.insert(locs.begin(), locs.end());
590  }
591 }
592 
594 {
595  everything_changed_ = true;
596 }
597 
599 {
600  return everything_changed_;
601 }
602 
604 {
605  disp.labels().clear_all();
607 }
608 
610 {
611  std::set<map_location> new_label_locs = map_.set_starting_position_labels(disp);
612  starting_position_label_locs_.insert(new_label_locs.begin(), new_label_locs.end());
613 }
614 
616 {
619  set_needs_labels_reset(false);
620 }
621 
623 {
624  config scen;
625 
626  // Textdomain
627  std::string current_textdomain = "wesnoth-"+addon_id_;
628 
629  // the state of the previous scenario cfg
630  // if it exists, alter specific parts of it (sides, times, and editor events) rather than replacing it entirely
631  if(previous_cfg_) {
632  scen = *previous_cfg_;
633  }
634 
635  // if this has [multiplayer], use [multiplayer]
636  // else if this has [scenario], use [scenario]
637  // else if this has [test], use [test]
638  // else if none, add a [multiplayer]
639  config& scenario = scen.has_child("multiplayer")
640  ? scen.mandatory_child("multiplayer")
641  : scen.has_child("scenario")
642  ? scen.mandatory_child("scenario")
643  : scen.has_child("test")
644  ? scen.mandatory_child("test")
645  : scen.add_child("multiplayer");
646 
647  scenario.remove_children("side");
648  scenario.remove_children("event", [](config cfg){return cfg["id"].str() == "editor_event-start" || cfg["id"].str() == "editor_event-prestart";});
649 
650  scenario["id"] = scenario_id_;
651  scenario["name"] = t_string(scenario_name_, current_textdomain);
652  scenario["description"] = t_string(scenario_description_, current_textdomain);
653 
654  if(xp_mod_) {
655  scenario["experience_modifier"] = *xp_mod_;
656  }
657  if(victory_defeated_) {
658  scenario["victory_when_enemies_defeated"] = *victory_defeated_;
659  }
660  scenario["random_start_time"] = random_time_;
661 
662  // write out the map data
663  scenario["map_file"] = scenario_id_ + ".map";
665 
666  // find or add the editor's start event
667  config& event = scenario.add_child("event");
668  event["name"] = "prestart";
669  event["id"] = "editor_event-prestart";
670  event["priority"] = 1000;
671 
672  // write out all the scenario data below
673 
674  // [time]s and [time_area]s
675  // put the [time_area]s into the event to keep as much editor-specific stuff separated in its own event as possible
676  config times = tod_manager_->to_config(current_textdomain);
677  times.remove_attribute("turn_at");
678  times.remove_attribute("it_is_a_new_turn");
679  if(scenario["turns"].to_int() == -1) {
680  times.remove_attribute("turns");
681  } else {
682  scenario["turns"] = times["turns"];
683  }
684 
685  for(const config& time : times.child_range("time")) {
686  config& t = scenario.add_child("time");
687  t.append(time);
688  }
689  for(const config& time_area : times.child_range("time_area")) {
690  config& t = event.add_child("time_area");
691  t.append(time_area);
692  }
693 
694  // [label]s
695  labels_.write(event);
696 
697  // [item]s
698  for(const auto& overlay_pair : overlays_) {
699  for(const overlay& o : overlay_pair.second) {
700  config& item = event.add_child("item");
701 
702  // Write x,y location
703  overlay_pair.first.write(item);
704 
705  // These should always have a value
706  item["image"] = o.image;
707  item["visible_in_fog"] = o.visible_in_fog;
708 
709  // Optional keys
710  item["id"].write_if_not_empty(o.id);
711  item["name"].write_if_not_empty(t_string(o.name, current_textdomain));
712  item["team_name"].write_if_not_empty(o.team_name);
713  item["halo"].write_if_not_empty(o.halo);
714  if(o.submerge) {
715  item["submerge"] = o.submerge;
716  }
717  }
718  }
719 
720  // [music]s
721  for(const music_map::value_type& track : music_tracks_) {
722  track.second.write(event, true);
723  }
724 
725  // [unit]s
726  for(const auto& unit : units_) {
727  config& u = event.add_child("unit");
728 
729  unit.get_location().write(u);
730 
731  u["side"] = unit.side();
732  u["type"] = unit.type_id();
733  u["name"].write_if_not_empty(t_string(unit.name(), current_textdomain));
734  u["facing"] = map_location::write_direction(unit.facing());
735 
736  if(!boost::regex_match(unit.id(), boost::regex(".*-[0-9]+"))) {
737  u["id"] = unit.id();
738  }
739 
740  if(unit.can_recruit()) {
741  u["canrecruit"] = unit.can_recruit();
742  }
743 
744  if(unit.unrenamable()) {
745  u["unrenamable"] = unit.unrenamable();
746  }
747  }
748 
749  // [side]s
750  for(const auto& team : teams_) {
751  config& side = scenario.add_child("side");
752 
753  side["side"] = scenario.child_count("side");
754  side["hidden"] = team.hidden();
755 
756  side["controller"] = side_controller::get_string(team.controller());
757  side["no_leader"] = team.no_leader();
758 
759  side["team_name"] = team.team_name();
760  side["user_team_name"].write_if_not_empty(t_string(team.user_team_name(), current_textdomain));
761  if(team.recruits().size() > 0) {
762  side["recruit"] = utils::join(team.recruits(), ",");
763  side["faction"] = "Custom";
764  }
765 
766  side["fog"] = team.uses_fog();
767  side["shroud"] = team.uses_shroud();
768  side["share_vision"] = team_shared_vision::get_string(team.share_vision());
769 
770  side["gold"] = team.gold();
771  side["income"] = team.base_income();
772 
773  for(const map_location& village : team.villages()) {
774  village.write(side.add_child("village"));
775  }
776  }
777 
778  previous_cfg_ = scen;
779  return scen;
780 }
781 
782 void map_context::save_schedule(const std::string& schedule_id, const std::string& schedule_name)
783 {
784  // Textdomain
785  std::string current_textdomain = "wesnoth-"+addon_id_;
786 
787  // Path to schedule.cfg
788  std::string schedule_path = filesystem::get_current_editor_dir(addon_id_) + "/utils/schedule.cfg";
789 
790  // Create schedule config
791  config schedule;
792  try {
793  if (filesystem::file_exists(schedule_path)) {
794  /* If exists, read the schedule.cfg
795  * and insert [editor_times] block at correct place */
797  editor_map["EDITOR"] = preproc_define("true");
798  read(schedule, *(preprocess_file(schedule_path, &editor_map)));
799  }
800  } catch(const filesystem::io_exception& e) {
801  utils::string_map symbols;
802  symbols["msg"] = e.what();
803  //TODO : Needs to be replaced with a better message later.
804  const std::string msg = VGETTEXT("Could not save the scenario: $msg", symbols);
806  }
807 
808  config& editor_times = schedule.add_child("editor_times");
809 
810  editor_times["id"] = schedule_id;
811  editor_times["name"] = t_string(schedule_name, current_textdomain);
812  config times = tod_manager_->to_config(current_textdomain);
813  for(const config& time : times.child_range("time")) {
814  config& t = editor_times.add_child("time");
815  t.append(time);
816  }
817 
818  // Write to file
819  try {
820  std::stringstream wml_stream;
821 
822  wml_stream
823  << "#\n"
824  << "# This file was generated using the scenario editor.\n"
825  << "#\n"
826  << "#ifdef EDITOR\n";
827 
828  {
829  config_writer out(wml_stream, false);
830  out.write(schedule);
831  }
832 
833  wml_stream << "#endif";
834 
835  if(!wml_stream.str().empty()) {
836  filesystem::write_file(schedule_path, wml_stream.str());
837  }
838 
839  } catch(const filesystem::io_exception& e) {
840  utils::string_map symbols;
841  symbols["msg"] = e.what();
842  //TODO : Needs to be replaced with a better message later.
843  const std::string msg = VGETTEXT("Could not save the scenario: $msg", symbols);
844 
846  }
847 }
848 
850 {
851  assert(!is_embedded());
852 
853  if(scenario_id_.empty()) {
855  }
856 
857  if(scenario_name_.empty()) {
859  }
860 
861  try {
862  std::stringstream wml_stream;
863  wml_stream
864  << "# This file was generated using the scenario editor.\n"
865  << "#\n"
866  << "# If you edit this file by hand, then do not use macros.\n"
867  << "# The editor doesn't support macros, and so using them will result in only being able to edit the map.\n"
868  << "# 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"
869  << "# Any manual changes made to those will be lost.\n"
870  << "\n";
871  {
872  config_writer out(wml_stream, false);
873  out.write(to_config());
874  }
875 
876  if(!wml_stream.str().empty()) {
877  filesystem::write_file(get_filename(), wml_stream.str());
878  }
879 
880  clear_modified();
881  } catch(const filesystem::io_exception& e) {
882  utils::string_map symbols;
883  symbols["msg"] = e.what();
884  const std::string msg = VGETTEXT("Could not save the scenario: $msg", symbols);
885 
887  }
888 
889  // After saving the map as a scenario, it's no longer a pure map.
890  pure_map_ = false;
891 }
892 
894 {
895  std::string map_data = map_.write();
896 
897  try {
898  if(!is_embedded()) {
900  } else {
901  std::string map_string = filesystem::read_file(get_filename());
902 
903  boost::regex rexpression_map_data(R"((.*map_data\s*=\s*")(.+?)(".*))");
904  boost::smatch matched_map_data;
905 
906  if(boost::regex_search(map_string, matched_map_data, rexpression_map_data,
907  boost::regex_constants::match_not_dot_null)) {
908  std::stringstream ss;
909  ss << matched_map_data[1];
910  ss << map_data;
911  ss << matched_map_data[3];
912 
914  } else {
915  throw editor_map_save_exception(_("Could not save into scenario"));
916  }
917  }
918 
920 
921  clear_modified();
922  } catch(const filesystem::io_exception& e) {
923  utils::string_map symbols;
924  symbols["msg"] = e.what();
925  const std::string msg = VGETTEXT("Could not save the map: $msg", symbols);
926 
928  }
929 }
930 
932 {
933  if(map_.h() != map.h() || map_.w() != map.w()) {
935  } else {
937  }
938 
939  map_ = map;
940 }
941 
943 {
944  LOG_ED << "Performing action " << action.get_id() << ": " << action.get_name() << ", actions count is "
945  << action.get_instance_count();
946  auto undo = action.perform(*this);
947  if(actions_since_save_ < 0) {
948  // set to a value that will make it impossible to get to zero, as at this point
949  // it is no longer possible to get back the original map state using undo/redo
950  actions_since_save_ = 1 + undo_stack_.size();
951  }
952 
954 
955  undo_stack_.emplace_back(std::move(undo));
956 
958 
959  redo_stack_.clear();
960 }
961 
963 {
964  LOG_ED << "Performing (partial) action " << action.get_id() << ": " << action.get_name() << ", actions count is "
965  << action.get_instance_count();
966  if(!can_undo()) {
967  throw editor_logic_exception("Empty undo stack in perform_partial_action()");
968  }
969 
970  editor_action_chain* undo_chain = dynamic_cast<editor_action_chain*>(last_undo_action());
971  if(undo_chain == nullptr) {
972  throw editor_logic_exception("Last undo action not a chain in perform_partial_action()");
973  }
974 
975  auto undo = action.perform(*this);
976 
977  // actions_since_save_ += action.action_count();
978  undo_chain->prepend_action(std::move(undo));
979 
980  redo_stack_.clear();
981 }
982 
984 {
985  return actions_since_save_ != 0;
986 }
987 
989 {
991 }
992 
994 {
996 }
997 
999 {
1000  return !undo_stack_.empty();
1001 }
1002 
1004 {
1005  return !redo_stack_.empty();
1006 }
1007 
1009 {
1010  return undo_stack_.empty() ? nullptr : undo_stack_.back().get();
1011 }
1012 
1014 {
1015  return redo_stack_.empty() ? nullptr : redo_stack_.back().get();
1016 }
1017 
1019 {
1020  return undo_stack_.empty() ? nullptr : undo_stack_.back().get();
1021 }
1022 
1024 {
1025  return redo_stack_.empty() ? nullptr : redo_stack_.back().get();
1026 }
1027 
1029 {
1030  LOG_ED << "undo() beg, undo stack is " << undo_stack_.size() << ", redo stack " << redo_stack_.size();
1031 
1032  if(can_undo()) {
1035  } else {
1036  WRN_ED << "undo() called with an empty undo stack";
1037  }
1038 
1039  LOG_ED << "undo() end, undo stack is " << undo_stack_.size() << ", redo stack " << redo_stack_.size();
1040 }
1041 
1043 {
1044  LOG_ED << "redo() beg, undo stack is " << undo_stack_.size() << ", redo stack " << redo_stack_.size();
1045 
1046  if(can_redo()) {
1049  } else {
1050  WRN_ED << "redo() called with an empty redo stack";
1051  }
1052 
1053  LOG_ED << "redo() end, undo stack is " << undo_stack_.size() << ", redo stack " << redo_stack_.size();
1054 }
1055 
1057 {
1058  // callers should check for these conditions
1059  if(!can_undo()) {
1060  throw editor_logic_exception("Empty undo stack in partial_undo()");
1061  }
1062 
1063  editor_action_chain* undo_chain = dynamic_cast<editor_action_chain*>(last_undo_action());
1064  if(undo_chain == nullptr) {
1065  throw editor_logic_exception("Last undo action not a chain in partial undo");
1066  }
1067 
1068  // a partial undo performs the first action form the current action's action_chain that would be normally performed
1069  // i.e. the *first* one.
1070  const auto first_action_in_chain = undo_chain->pop_first_action();
1071  if(undo_chain->empty()) {
1073  undo_stack_.pop_back();
1074  }
1075 
1076  redo_stack_.emplace_back(first_action_in_chain->perform(*this));
1077  // actions_since_save_ -= last_redo_action()->action_count();
1078 }
1079 
1081 {
1082  undo_stack_.clear();
1083  redo_stack_.clear();
1084 }
1085 
1087 {
1088  if(stack.size() > max_action_stack_size_) {
1089  stack.pop_front();
1090  }
1091 }
1092 
1094 {
1095  assert(!from.empty());
1096 
1097  std::unique_ptr<editor_action> action;
1098  action.swap(from.back());
1099 
1100  from.pop_back();
1101 
1102  auto reverse_action = action->perform(*this);
1103  to.emplace_back(std::move(reverse_action));
1104 
1105  trim_stack(to);
1106 }
1107 
1109 {
1110  return is_pure_map() ? _("New Map") : _("New Scenario");
1111 }
1112 
1113 } // end namespace editor
std::string filename_
Definition: action_wml.cpp:538
double t
Definition: astarsearch.cpp:63
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:159
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:367
std::size_t child_count(config_key_type key) const
Definition: config.cpp:297
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:787
bool has_child(config_key_type key) const
Determine whether a config has a child or not.
Definition: config.cpp:317
const_all_children_itors all_children_range() const
In-order iteration over all children.
Definition: config.cpp:887
child_itors child_range(config_key_type key)
Definition: config.cpp:273
void append_attributes(const config &cfg)
Adds attributes from cfg.
Definition: config.cpp:190
void remove_attribute(config_key_type key)
Definition: config.cpp:160
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:656
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:687
config & add_child(config_key_type key)
Definition: config.cpp:441
Sort-of-Singleton that many classes, both GUI and non-GUI, use to access the game data.
Definition: display.hpp:88
map_labels & labels()
Definition: display.cpp:2619
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:70
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_
void save_scenario()
Saves the scenario under the current filename.
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.
void save_map()
Saves the map 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)
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)
void save_schedule(const std::string &schedule_id, const std::string &schedule_name)
Save custom time of day schedule in the utils directory.
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:301
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:389
std::string write() const
Definition: map.cpp:209
const terrain_type & get_terrain_info(const t_translation::terrain_code &terrain) const
Definition: map.cpp:97
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:396
@ 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:74
bool uses_shroud() const
Definition: team.hpp:303
const std::string & team_name() const
Definition: team.hpp:282
bool no_leader() const
Definition: team.hpp:327
team_shared_vision::type share_vision() const
Definition: team.hpp:377
const std::set< map_location > & villages() const
Definition: team.hpp:170
int gold() const
Definition: team.hpp:175
side_controller::type controller() const
Definition: team.hpp:241
int base_income() const
Definition: team.hpp:177
bool uses_fog() const
Definition: team.hpp:304
bool hidden() const
Definition: team.hpp:333
const std::set< std::string > & recruits() const
Definition: team.hpp:209
const t_string & user_team_name() const
Definition: team.hpp:283
t_translation::terrain_code terrain_with_default_base() const
Definition: terrain.cpp:295
umap_retval_pair_t insert(unit_ptr p)
Inserts the unit pointed to by p into the map.
Definition: map.cpp:135
This class represents a single unit of a specific type.
Definition: unit.hpp:133
static unit_ptr create(const config &cfg, bool use_traits=false, const vconfig *vcfg=nullptr)
Initializes a unit from a config.
Definition: unit.hpp:201
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:436
const std::string & type_id() const
The id of this unit's type.
Definition: unit.cpp:1957
bool can_recruit() const
Whether this unit can recruit other units - ie, are they a leader unit.
Definition: unit.hpp:612
const std::string & id() const
Gets this unit's id.
Definition: unit.hpp:380
int side() const
The side this unit belongs to.
Definition: unit.hpp:343
const t_string & name() const
Gets this unit's translatable display name.
Definition: unit.hpp:403
const map_location & get_location() const
The current map location this unit is at.
Definition: unit.hpp:1378
map_location::DIRECTION facing() const
The current direction this unit is facing within its hex.
Definition: unit.hpp:1394
std::string id
Text to match against addon_info.tags()
Definition: manager.cpp:207
#define log_scope2(domain, description)
Definition: log.hpp:275
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:93
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:318
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:545
std::string get_current_editor_dir(const std::string &addon_id)
Game configuration data as global variables.
Definition: build_info.cpp:60
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:150
std::pair< std::string, unsigned > item
Definition: help_impl.hpp:412
logger & info()
Definition: log.cpp:314
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:522
bool shroud()
Definition: game.cpp:532
int village_gold()
Definition: game.cpp:648
int turns()
Definition: game.cpp:542
::tod_manager * tod_manager
Definition: resources.cpp:29
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:70
void trim(std::string_view &s)
std::set< std::string > split_set(std::string_view s, char sep, const int flags)
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:109
filesystem::scoped_istream preprocess_file(const std::string &fname, preproc_map *defines)
Function to use the WML preprocessor on a file.
std::map< std::string, struct preproc_define > preproc_map
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:627
config & cfg
Definition: config.hpp:685
editor_team_info(const team &t)
Definition: map_context.cpp:39
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:211
static std::string write_direction(DIRECTION dir)
Definition: location.cpp:140
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