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