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