The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
map_context.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2008 - 2025
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"
22#include "filesystem.hpp"
24#include "gettext.hpp"
27#include "map/label.hpp"
32#include "team.hpp"
33#include "units/unit.hpp"
34
35#include <boost/algorithm/string/predicate.hpp>
36#include <boost/algorithm/string/trim.hpp>
37#include <boost/regex.hpp>
38
39namespace editor
40{
42 : side(t.side())
43 , id(t.team_name())
44 , name(t.user_team_name())
45 , recruit_list(utils::join(t.recruits(), ","))
46 , gold(t.gold())
47 , income(t.base_income())
48 , village_income(t.village_gold())
49 , village_support(t.village_support())
50 , fog(t.uses_fog())
51 , shroud(t.uses_shroud())
52 , share_vision(t.share_vision())
53 , controller(t.controller())
54 , no_leader(t.no_leader())
55 , hidden(t.hidden())
56{
57}
58
59const std::size_t map_context::max_action_stack_size_ = 100;
60
61namespace {
62 static const int editor_team_default_gold = 100;
63}
64
65map_context::map_context(const editor_map& map, bool pure_map, const std::string& addon_id)
66 : filename_()
67 , map_data_key_()
68 , embedded_(false)
69 , pure_map_(pure_map)
70 , map_(map)
71 , undo_stack_()
72 , redo_stack_()
73 , actions_since_save_(0)
74 , needs_reload_(false)
75 , needs_terrain_rebuild_(false)
76 , needs_labels_reset_(false)
77 , everything_changed_(false)
78 , changed_locations_()
79 , starting_position_label_locs_()
80 , addon_id_(addon_id)
81 , previous_cfg_()
82 , scenario_id_()
83 , scenario_name_()
84 , scenario_description_()
85 , xp_mod_()
86 , victory_defeated_(true)
87 , random_time_(false)
88 , active_area_(-1)
89 , labels_(nullptr)
90 , units_()
91 , teams_()
92 , tod_manager_(new tod_manager)
93 , mp_settings_()
94 , game_classification_()
95 , music_tracks_()
96 , last_map_generator_(nullptr)
97{
98}
99
100// Look for attr in file_contents, if there is an equal sign on the same line assume that what follows to the line end is the attribute value
101static std::string get_map_location(const std::string& file_contents, const std::string& attr)
102{
103 std::size_t attr_name_start = file_contents.find(attr);
104 if(attr_name_start == std::string::npos) return "";
105
106 std::size_t attr_value_start = file_contents.find("=", attr_name_start);
107 std::size_t line_end = file_contents.find("\n", attr_name_start);
108 if(line_end < attr_value_start) return ""; // assume not both values are string::npos
109
110 attr_value_start++;
111 std::string attr_value = file_contents.substr(attr_value_start, line_end - attr_value_start);
112
113 return boost::trim_copy(attr_value);
114}
115
116map_context::map_context(const std::string& filename, const std::string& addon_id)
118 , map_data_key_()
119 , embedded_(false)
120 , pure_map_(false)
121 , map_()
122 , undo_stack_()
123 , redo_stack_()
124 , actions_since_save_(0)
125 , needs_reload_(false)
126 , needs_terrain_rebuild_(false)
127 , needs_labels_reset_(false)
128 , everything_changed_(false)
129 , changed_locations_()
130 , starting_position_label_locs_()
131 , addon_id_(addon_id)
132 , previous_cfg_()
133 , scenario_id_()
134 , scenario_name_()
135 , scenario_description_()
136 , xp_mod_()
137 , victory_defeated_(true)
138 , random_time_(false)
139 , active_area_(-1)
140 , labels_(nullptr)
141 , units_()
142 , teams_()
143 , tod_manager_(new tod_manager)
144 , mp_settings_()
145 , game_classification_()
146 , music_tracks_()
147 , last_map_generator_(nullptr)
148{
149 /*
150 * Overview of situations possibly found in the file:
151 *
152 * embedded_ - the map data is directly in the scenario file
153 * pure_map_ - the map data is in its own separate file (map_file, map_data+macro inclusion) or this is a .map file
154 *
155 * 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
156 *
157 * 0. Not a scenario or map file.
158 * 0.1 File not found
159 * 0.2 Map file empty
160 * 0.3 Not a .map or .cfg file
161 * 1. It's a .map file.
162 * * embedded_ = false
163 * * pure_map_ = true
164 * 2. A scenario embedding the map
165 * * embedded_ = true
166 * * pure_map_ = true
167 * The scenario-test.cfg for example.
168 * The map is written back to the file.
169 * 3. The map file is referenced by map_data={MACRO_ARGUEMENT}.
170 * * embedded_ = false
171 * * pure_map_ = true
172 * 4. The file contains an editor generated scenario file.
173 * * embedded_ = false
174 * * pure_map_ = false
175 * 5. The file is using map_file.
176 * 5.1 The file doesn't contain a macro and so can be loaded by the editor as a scenario
177 * * embedded_ = false
178 * * pure_map_ = false
179 * 5.2 The file contains a macro and so can't be loaded by the editor as a scenario
180 * * embedded_ = false
181 * * pure_map_ = true
182 */
183
184 log_scope2(log_editor, "Loading file " + filename);
185
186 // 0.1 File not found
188 throw editor_map_load_exception(filename, _("File not found"));
189 }
190
192
193 // 0.2 Map file empty
194 if(file_string.empty()) {
195 std::string message = _("Empty file");
196 throw editor_map_load_exception(filename, message);
197 }
198
199 // 0.3 Not a .map or .cfg file
203 {
204 std::string message = _("File does not have .map, .cfg, or .mask extension");
205 throw editor_map_load_exception(filename, message);
206 }
207
208 // 1.0 Pure map data
211 LOG_ED << "Loading map or mask file";
212 map_ = editor_map::from_string(file_string); // throws on error
213 pure_map_ = true;
214
216 return;
217 }
218
219 // 2.0 try loading cfg file
220 LOG_ED << "Try loading scenario file";
221 bool loaded = false;
222 try {
224 } catch(const std::exception& e) {
226 symbols["error"] = e.what();
227 const std::string msg = VGETTEXT("Failed to load the scenario: $error\n\nAttempting to load only the map.", symbols);
229
230 std::string map_data_loc = get_map_location(file_string, "map_data");
231 std::string map_file_loc = get_map_location(file_string, "map_file");
232
233 if(!map_data_loc.empty()) {
234 if(map_data_loc.find("\"{") == std::string::npos) {
235 // 2.1 Embedded pure map
236 LOG_ED << "Loading embedded map_data";
237 embedded_ = true;
238 pure_map_ = true;
239 // build on sloppy parsing from get_map_location
240 std::size_t start = file_string.find(map_data_loc)+1; // assume that the "parsed" attribute value is not found earlier in the scenario file and assume it starts with a double quote
241 std::size_t length = file_string.find("\"", start)-start; // assume the attribute value ends with a double quote and does not contain one
242 std::string map_data = file_string.substr(start, length);
243 map_ = editor_map::from_string(map_data);
244 } else {
245 // 2.2 Macro referenced pure map
246 // assume `map_data = "{...}"` so skip the quotes and curly braces
247 const std::string& macro_argument = map_data_loc.substr(2, map_data_loc.size()-4);
248 LOG_ED << "Map looks like a scenario, trying {" << macro_argument << "}";
249
251
252 if(!new_filename) {
253 std::string message = _("The file looks like a scenario, but the map_data attribute does not point to an existing file") + std::string("\n") + macro_argument;
254 throw editor_map_load_exception(filename, message);
255 }
256
257 LOG_ED << "New filename is: " << new_filename.value();
258
259 pure_map_ = true;
260 filename_ = new_filename.value();
262 }
263 } else if(!map_file_loc.empty()) {
264 // 2.3 map_file referenced pure map
265
268 std::string message = _("The file looks like a scenario, but the map_file attribute does not point to an existing file") + std::string("\n") + map_file_loc;
269 throw editor_map_load_exception(filename, message);
270 }
271
272 LOG_ED << "New filename is: " << new_filename;
273
274 pure_map_ = true;
277 } else {
278 // 2.4 could not load the scenario or find the map data in it; throw error from load_scenario
279 throw editor_map_load_exception(filename, e.what());
280 }
281 }
282 if(!loaded) {
283 throw editor_map_load_exception(filename, "Aborted");
284 }
286}
287
289{
290 undo_stack_.clear();
291 redo_stack_.clear();
292}
293
295{
296 teams_.emplace_back();
297
298 config cfg;
299 cfg["side"] = teams_.size(); // side is 1-indexed, so we can just use size()
300 cfg["hidden"] = false;
302
303 teams_.back().build(cfg, map());
304
306}
307
309{
310 assert(teams_.size() >= static_cast<unsigned int>(info.side));
311
312 team& t = teams_[info.side - 1];
313 t.change_team(info.id, info.name);
314 t.set_recruits(utils::split_set(info.recruit_list, ','));
315 t.have_leader(!info.no_leader);
316 t.change_controller(info.controller);
317 t.set_gold(info.gold);
318 t.set_base_income(info.income);
319 t.set_hidden(info.hidden);
320 t.set_fog(info.fog);
321 t.set_shroud(info.shroud);
322 t.set_share_vision(info.share_vision);
323 t.set_village_gold(info.village_income);
324 t.set_village_support(info.village_support);
325
327}
328
329void map_context::set_scenario_setup(const std::string& id,
330 const std::string& name,
331 const std::string& description,
332 int turns,
333 int xp_mod,
334 bool victory_defeated,
335 bool random_time)
336{
338 scenario_name_ = name;
339 scenario_description_ = description;
342 tod_manager_->set_number_of_turns(turns);
343 xp_mod_ = xp_mod;
345}
346
348{
349 tod_manager_->set_current_time(time);
350 if(!pure_map_) {
352 }
353}
354
356{
357 tod_manager_->remove_time_area(index);
358 active_area_--;
360}
361
362void map_context::replace_schedule(const std::vector<time_of_day>& schedule)
363{
364 tod_manager_->replace_schedule(schedule);
365 if(!pure_map_) {
367 }
368}
369
370void map_context::replace_local_schedule(const std::vector<time_of_day>& schedule)
371{
372 tod_manager_->replace_local_schedule(schedule, active_area_);
373 if(!pure_map_) {
375 }
376}
377
379{
380 config cfg;
381 config& multiplayer = cfg.add_child("multiplayer");
382 multiplayer.append_attributes(old_scenario);
383 std::string map_data = multiplayer["map_data"];
385
386 // check that there's embedded map data, since that's how the editor used to save scenarios
387 if(!map_data.empty()) {
388 // 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
391 }
392 multiplayer["id"] = filesystem::base_name(separate_map_file, true);
393
395 multiplayer.remove_attribute("map_data");
396 multiplayer["map_file"] = filesystem::base_name(separate_map_file);
397 } else {
398 ERR_ED << "Cannot convert " << filename_ << " due to missing map_data attribute.";
399 throw editor_map_load_exception("load_scenario: no embedded map_data attribute found in old-style scenario", filename_);
400 }
401
402 config& event = multiplayer.add_child("event");
403 event["name"] = "prestart";
404 event["id"] = "editor_event-prestart";
405
406 // for all children that aren't [side] or [time], move them to an event
407 // for [side]:
408 // keep all attributes in [side]
409 // also keep any [village]s in [side]
410 // move all other children to the start [event]
411 // if [unit], set the unit's side
412 // for [time]:
413 // keep under [multiplayer]
414 for(const auto [child_key, child_cfg]: old_scenario.all_children_view()) {
415 if(child_key != "side" && child_key != "time") {
416 config& c = event.add_child(child_key);
418 c.append_children(child_cfg);
419 } else if(child_key == "side") {
420 config& c = multiplayer.add_child("side");
422 for(const auto [side_key, side_cfg] : child_cfg.all_children_view()) {
423 if(side_key == "village") {
424 config& c1 = c.add_child("village");
425 c1.append_attributes(side_cfg);
426 } else {
427 config& c1 = event.add_child(side_key);
428 c1.append_attributes(side_cfg);
429 if(side_key == "unit") {
430 c1["side"] = child_cfg["side"];
431 }
432 }
433 }
434 } else if(child_key == "time") {
435 config& c = multiplayer.add_child("time");
437 }
438 }
439
440 return cfg;
441}
442
444{
445 config scen = io::read(*preprocess_file(filename_)); // this call might throw anyway so ask only after trying
447 if(!boost::algorithm::starts_with(scenario_text, "# This file was generated using the scenario editor.")) {
448 int res = gui2::show_message(_("Confirm"), "This file was not generated by the scenario editor. Saving it may have unwanted consequences including removal of all preprocessor constructs like comments and macro use. Do you want to continue?", gui2::dialogs::message::yes_no_buttons);
449 if(res != gui2::retval::OK) {
450 return false;
451 }
452 }
453
454 config scenario;
455 if(scen.has_child("scenario")) {
456 scenario = scen.mandatory_child("scenario");
457 } else if(scen.has_child("multiplayer")) {
458 scenario = scen.mandatory_child("multiplayer");
459 } else if(scen.has_child("test")) {
460 scenario = scen.mandatory_child("test");
461 } else {
462 ERR_ED << "Found no [scenario], [multiplayer], or [test] tag in " << filename_ << ", assuming old-style editor scenario and defaulting to [multiplayer]";
464 scenario = scen.mandatory_child("multiplayer");
465 }
466
467 scenario_id_ = scenario["id"].str();
468 scenario_name_ = scenario["name"].str();
469 scenario_description_ = scenario["description"].str();
470
471 if(const config::attribute_value* experience_modifier = scenario.get("experience_modifier")) {
472 xp_mod_ = experience_modifier->to_int();
473 }
474 victory_defeated_ = scenario["victory_when_enemies_defeated"].to_bool(true);
475 random_time_ = scenario["random_start_time"].to_bool(false);
476
477 if(!scenario["map_data"].str().empty()) {
478 map_ = editor_map::from_string(scenario["map_data"].str()); // throws on error
479 } else if(!scenario["map_file"].str().empty()) {
481 } else {
482 throw editor_map_load_exception("load_scenario: no map_file or map_data attribute found", filename_);
483 }
484
485 for(config& side : scenario.child_range("side")) {
486 teams_.emplace_back();
487 teams_.back().build(side, map_);
488 if(!side["recruit"].str().empty()) {
489 teams_.back().set_recruits(utils::split_set(side["recruit"].str(), ','));
490 }
491 }
492
493 tod_manager_.reset(new tod_manager(scenario));
494
495 auto event = scenario.find_child("event", "id", "editor_event-start");
496 if(!event) {
497 event = scenario.find_child("event", "id", "editor_event-prestart");
498 }
499 if(event) {
500 config& evt = event.value();
501
503
504 for(const config& time_area : evt.child_range("time_area")) {
505 tod_manager_->add_time_area(map_, time_area);
506 }
507
508 for(const config& item : evt.child_range("item")) {
509 const map_location loc(item);
510 overlays_[loc].push_back(overlay(item));
511 }
512
513 for(const config& music : evt.child_range("music")) {
514 music_tracks_.emplace_back(sound::music_track::create(music));
515 }
516
517 for(config& a_unit : evt.child_range("unit")) {
519 }
520 }
521
523 return true;
524}
525
527{
528 map_.set_selection(tod_manager_->get_area_by_index(index));
529}
530
531bool map_context::playlist_contains(const std::shared_ptr<sound::music_track>& track) const
532{
533 return utils::contains(music_tracks_, track);
534}
535
536void map_context::toggle_track(const std::shared_ptr<sound::music_track>& track)
537{
538 if(playlist_contains(track)) {
539 music_tracks_.remove(track);
540 } else {
541 music_tracks_.push_back(track);
542 }
543}
544
553
555 const t_translation::terrain_code& terrain, const map_location& loc, bool one_layer_only)
556{
558 // requests for painting off the map are ignored in set_terrain anyway,
559 // but ideally we should not have any
560 LOG_ED << "Attempted to draw terrain off the map (" << loc << ")";
561 return;
562 }
563
565
566 if(terrain != old_terrain) {
567 if(terrain.base == t_translation::NO_LAYER) {
569 } else if(one_layer_only) {
571 } else {
572 map_.set_terrain(loc, terrain);
573 }
574
576 }
577}
578
580 const t_translation::terrain_code& terrain, const std::set<map_location>& locs, bool one_layer_only)
581{
583 ? terrain
585
586 for(const map_location& loc : locs) {
588 }
589}
590
596
598{
599 if(!everything_changed()) {
600 changed_locations_.insert(loc);
601 }
602}
603
604void map_context::add_changed_location(const std::set<map_location>& locs)
605{
606 if(!everything_changed()) {
607 changed_locations_.insert(locs.begin(), locs.end());
608 }
609}
610
615
617{
618 return everything_changed_;
619}
620
626
632
639
641{
642 config scen;
643
644 // Textdomain
645 std::string current_textdomain = "wesnoth-"+addon_id_;
646
647 // the state of the previous scenario cfg
648 // if it exists, alter specific parts of it (sides, times, and editor events) rather than replacing it entirely
649 if(previous_cfg_) {
651 }
652
653 // if this has [multiplayer], use [multiplayer]
654 // else if this has [scenario], use [scenario]
655 // else if this has [test], use [test]
656 // else if none, add a [multiplayer]
657 config& scenario = scen.has_child("multiplayer")
658 ? scen.mandatory_child("multiplayer")
659 : scen.has_child("scenario")
660 ? scen.mandatory_child("scenario")
661 : scen.has_child("test")
662 ? scen.mandatory_child("test")
663 : scen.add_child("multiplayer");
664
665 scenario.remove_children("side");
666 scenario.remove_children("event", [](const config& cfg) {
667 return cfg["id"].str() == "editor_event-start" || cfg["id"].str() == "editor_event-prestart";
668 });
669 scenario.remove_children("time");
670
671 scenario["id"] = scenario_id_;
672 scenario["name"] = t_string(scenario_name_, current_textdomain);
673 scenario["description"] = t_string(scenario_description_, current_textdomain);
674
675 if(xp_mod_) {
676 scenario["experience_modifier"] = *xp_mod_;
677 }
679 scenario["victory_when_enemies_defeated"] = *victory_defeated_;
680 }
681 scenario["random_start_time"] = random_time_;
682
683 // write out the map data
684 scenario["map_file"] = scenario_id_ + filesystem::map_extension;
686
687 // find or add the editor's start event
688 config& event = scenario.add_child("event");
689 event["name"] = "prestart";
690 event["id"] = "editor_event-prestart";
691 event["priority"] = 1000;
692
693 // write out all the scenario data below
694
695 // [time]s and [time_area]s
696 config times = tod_manager_->to_config(current_textdomain);
697
698 // TODO: random_start_time is written separately above. Should we use the value from the ToD manager?
699 times.remove_attributes("turn_at", "it_is_a_new_turn", "random_start_time");
700
701 if(times["turns"].to_int() == -1) {
702 times.remove_attribute("turns");
703 }
704
705 if(times["current_time"].to_int() == 0) {
706 times.remove_attribute("current_time");
707 }
708
709 scenario.merge_attributes(times);
710 scenario.append_children_by_move(times, "time");
711
712 // put the [time_area]s into the event to keep as much editor-specific stuff separated in its own event as possible
713 event.append_children_by_move(times, "time_area");
714
715 // [label]s
716 labels_.write(event);
717
718 // [item]s
719 for(const auto& overlay_pair : overlays_) {
720 for(const overlay& o : overlay_pair.second) {
721 config& item = event.add_child("item");
722
723 // Write x,y location
724 overlay_pair.first.write(item);
725
726 // These should always have a value
727 item["image"] = o.image;
728 item["visible_in_fog"] = o.visible_in_fog;
729
730 // Optional keys
731 item["id"].write_if_not_empty(o.id);
732 item["name"].write_if_not_empty(t_string(o.name, current_textdomain));
733 item["team_name"].write_if_not_empty(o.team_name);
734 item["halo"].write_if_not_empty(o.halo);
735 if(o.submerge) {
736 item["submerge"] = o.submerge;
737 }
738 }
739 }
740
741 // [music]s
742 for(const auto& track : music_tracks_) {
743 track->write(event, true);
744 }
745
746 // [unit]s
748 preprocess_file(game_config::path + "/data/core/macros/traits.cfg", traits_map);
749
750 for(const auto& unit : units_) {
751 config& u = event.add_child("unit");
752
754
755 u["side"] = unit.side();
756 u["type"] = unit.type_id();
757 u["name"].write_if_not_empty(t_string(unit.name(), current_textdomain));
759
760 if(!boost::regex_match(unit.id(), boost::regex(".*-[0-9]+"))) {
761 u["id"] = unit.id();
762 }
763
764 if(unit.can_recruit()) {
765 u["canrecruit"] = unit.can_recruit();
766 }
767
768 if(unit.unrenamable()) {
769 u["unrenamable"] = unit.unrenamable();
770 }
771
772 config& mods = u.add_child("modifications");
773 if(unit.loyal()) {
774 mods.append(io::read(*preprocess_string("{TRAIT_LOYAL}", "wesnoth-help", traits_map)));
775 }
776 //TODO this entire block could also be replaced by unit.write(u, true)
777 //however, the resultant config is massive and contains many attributes we don't need.
778 //need to find a middle ground here.
779 }
780
781 // [side]s
782 for(const auto& team : teams_) {
783 config& side = scenario.add_child("side");
784
785 side["side"] = scenario.child_count("side");
786 side["hidden"] = team.hidden();
787
788 side["controller"] = side_controller::get_string(team.controller());
789 side["no_leader"] = team.no_leader();
790
791 side["team_name"] = team.team_name();
792 side["user_team_name"].write_if_not_empty(t_string(team.user_team_name(), current_textdomain));
793 if(team.recruits().size() > 0) {
794 side["recruit"] = utils::join(team.recruits(), ",");
795 side["faction"] = "Custom";
796 }
797
798 side["fog"] = team.uses_fog();
799 side["shroud"] = team.uses_shroud();
800 side["share_vision"] = team_shared_vision::get_string(team.share_vision());
801
802 side["gold"] = team.gold();
803 side["income"] = team.raw_income();
804
805 for(const map_location& village : team.villages()) {
806 village.write(side.add_child("village"));
807 }
808 }
809
811 return scen;
812}
813
814void map_context::save_schedule(const std::string& schedule_id, const std::string& schedule_name)
815{
816 // Textdomain
817 std::string current_textdomain = "wesnoth-"+addon_id_;
818
819 // Path to schedule.cfg
820 std::string schedule_path = filesystem::get_current_editor_dir(addon_id_) + "/utils/schedule.cfg";
821
822 // Create schedule config
823 config schedule;
824 try {
826 /* If exists, read the schedule.cfg
827 * and insert [editor_times] block at correct place */
829 editor_map.try_emplace("EDITOR");
831 }
832 } catch(const filesystem::io_exception& e) {
834 symbols["msg"] = e.what();
835 const std::string msg = VGETTEXT("Could not save time schedule: $msg", symbols);
836 throw editor_map_save_exception(msg);
837 }
838
839 config& editor_times = schedule.add_child("editor_times");
840
843 config times = tod_manager_->to_config(current_textdomain);
844 for(const config& time : times.child_range("time")) {
845 config& t = editor_times.add_child("time");
846 t.append(time);
847 }
848
849 // Write to file
850 try {
851 std::stringstream wml_stream;
852
854 << "#textdomain " << current_textdomain << "\n"
855 << "#\n"
856 << "# This file was generated using the scenario editor.\n"
857 << "#\n"
858 << "#ifdef EDITOR\n";
859
860 {
862 out.write(schedule);
863 }
864
865 wml_stream << "#endif";
866
867 if(!wml_stream.str().empty()) {
869 gui2::show_transient_message("", _("Time schedule saved."));
870 }
871
872 } catch(const filesystem::io_exception& e) {
874 symbols["msg"] = e.what();
875 const std::string msg = VGETTEXT("Could not save time schedule: $msg", symbols);
876 throw editor_map_save_exception(msg);
877 }
878}
879
881{
883
884 if(scenario_id_.empty()) {
886 }
887
888 if(scenario_name_.empty()) {
890 }
891
892 try {
893 std::stringstream wml_stream;
895 << "# This file was generated using the scenario editor.\n"
896 << "#\n"
897 << "# If you edit this file by hand, then do not expect common macros to be available when the editor loads the file.\n"
898 << "# Also expect any preprocessor constructs (which includes macros and comments) to be expanded/vanish on saving.\n"
899 << "# 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"
900 << "# Any manual changes made to those will be lost.\n"
901 << "\n";
902 {
904 out.write(to_config(), true);
905 }
906
907 if(!wml_stream.str().empty()) {
909 }
910
912 } catch(const filesystem::io_exception& e) {
914 symbols["msg"] = e.what();
915 const std::string msg = VGETTEXT("Could not save the scenario: $msg", symbols);
916
917 throw editor_map_save_exception(msg);
918 }
919
920 // After saving the map as a scenario, it's no longer a pure map.
921 pure_map_ = false;
922}
923
925{
926 std::string map_data = map_.write();
927
928 try {
929 if(!is_embedded()) {
931 } else {
933
934 boost::regex rexpression_map_data(R"((.*map_data\s*=\s*")(.+?)(".*))");
935 boost::smatch matched_map_data;
936
937 if(boost::regex_search(map_string, matched_map_data, rexpression_map_data,
938 boost::regex_constants::match_not_dot_null)) {
939 std::stringstream ss;
940 ss << matched_map_data[1];
941 ss << map_data;
942 ss << matched_map_data[3];
943
945 } else {
946 throw editor_map_save_exception(_("Could not save into scenario"));
947 }
948 }
949
951
953 } catch(const filesystem::io_exception& e) {
955 symbols["msg"] = e.what();
956 const std::string msg = VGETTEXT("Could not save the map: $msg", symbols);
957
958 throw editor_map_save_exception(msg);
959 }
960}
961
963{
964 if(map_.h() != map.h() || map_.w() != map.w()) {
966 } else {
968 }
969
970 map_ = map;
971}
972
974{
975 LOG_ED << "Performing action " << action.get_id() << ": " << action.get_name() << ", actions count is "
976 << action.get_instance_count();
977 auto undo = action.perform(*this);
978 if(actions_since_save_ < 0) {
979 // set to a value that will make it impossible to get to zero, as at this point
980 // it is no longer possible to get back the original map state using undo/redo
981 actions_since_save_ = 1 + undo_stack_.size();
982 }
983
985
986 undo_stack_.emplace_back(std::move(undo));
987
989
990 redo_stack_.clear();
991}
992
994{
995 LOG_ED << "Performing (partial) action " << action.get_id() << ": " << action.get_name() << ", actions count is "
996 << action.get_instance_count();
997 if(!can_undo()) {
998 throw editor_logic_exception("Empty undo stack in perform_partial_action()");
999 }
1000
1002 if(undo_chain == nullptr) {
1003 throw editor_logic_exception("Last undo action not a chain in perform_partial_action()");
1004 }
1005
1006 auto undo = action.perform(*this);
1007
1008 // actions_since_save_ += action.action_count();
1009 undo_chain->prepend_action(std::move(undo));
1010
1011 redo_stack_.clear();
1012}
1013
1015{
1016 return actions_since_save_ != 0;
1017}
1018
1023
1028
1030{
1031 return !undo_stack_.empty();
1032}
1033
1035{
1036 return !redo_stack_.empty();
1037}
1038
1040{
1041 return undo_stack_.empty() ? nullptr : undo_stack_.back().get();
1042}
1043
1045{
1046 return redo_stack_.empty() ? nullptr : redo_stack_.back().get();
1047}
1048
1050{
1051 return undo_stack_.empty() ? nullptr : undo_stack_.back().get();
1052}
1053
1055{
1056 return redo_stack_.empty() ? nullptr : redo_stack_.back().get();
1057}
1058
1060{
1061 LOG_ED << "undo() beg, undo stack is " << undo_stack_.size() << ", redo stack " << redo_stack_.size();
1062
1063 if(can_undo()) {
1066 } else {
1067 WRN_ED << "undo() called with an empty undo stack";
1068 }
1069
1070 LOG_ED << "undo() end, undo stack is " << undo_stack_.size() << ", redo stack " << redo_stack_.size();
1071}
1072
1074{
1075 LOG_ED << "redo() beg, undo stack is " << undo_stack_.size() << ", redo stack " << redo_stack_.size();
1076
1077 if(can_redo()) {
1080 } else {
1081 WRN_ED << "redo() called with an empty redo stack";
1082 }
1083
1084 LOG_ED << "redo() end, undo stack is " << undo_stack_.size() << ", redo stack " << redo_stack_.size();
1085}
1086
1088{
1089 // callers should check for these conditions
1090 if(!can_undo()) {
1091 throw editor_logic_exception("Empty undo stack in partial_undo()");
1092 }
1093
1095 if(undo_chain == nullptr) {
1096 throw editor_logic_exception("Last undo action not a chain in partial undo");
1097 }
1098
1099 // a partial undo performs the first action form the current action's action_chain that would be normally performed
1100 // i.e. the *first* one.
1101 const auto first_action_in_chain = undo_chain->pop_first_action();
1102 if(undo_chain->empty()) {
1104 undo_stack_.pop_back();
1105 }
1106
1107 redo_stack_.emplace_back(first_action_in_chain->perform(*this));
1108 // actions_since_save_ -= last_redo_action()->action_count();
1109}
1110
1112{
1113 undo_stack_.clear();
1114 redo_stack_.clear();
1115}
1116
1118{
1119 if(stack.size() > max_action_stack_size_) {
1120 stack.pop_front();
1121 }
1122}
1123
1125{
1126 assert(!from.empty());
1127
1128 std::unique_ptr<editor_action> action;
1129 action.swap(from.back());
1130
1131 from.pop_back();
1132
1133 auto reverse_action = action->perform(*this);
1134 to.emplace_back(std::move(reverse_action));
1135
1136 trim_stack(to);
1137}
1138
1140{
1141 return is_pure_map() ? _("New Map") : _("New Scenario");
1142}
1143
1144} // end namespace editor
std::string filename_
map_location loc
Definition move.cpp:172
double t
Variant for storing WML attributes.
Class for writing a config out to a file in pieces.
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
void remove_attribute(std::string_view key)
Definition config.cpp:162
config & add_child(std::string_view key)
Definition config.cpp:436
void remove_attributes(T... keys)
Definition config.hpp:536
void append_children_by_move(config &cfg, std::string_view key)
Moves children with the given name from the given config to this one.
Definition config.cpp:224
void merge_attributes(const config &)
Definition config.cpp:722
child_itors child_range(std::string_view key)
Definition config.cpp:268
const attribute_value * get(std::string_view key) const
Returns a pointer to the attribute with the given key or nullptr if it does not exist.
Definition config.cpp:665
optional_config_impl< config > find_child(std::string_view 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:764
void append_attributes(const config &cfg)
Adds attributes from cfg.
Definition config.cpp:174
std::size_t child_count(std::string_view key) const
Definition config.cpp:292
void remove_children(std::string_view key, const std::function< bool(const config &)> &p={})
Removes all children with tag key for which p returns true.
Definition config.cpp:634
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:2392
Container action wrapping several actions into one.
Definition action.hpp:88
Base class for all editor actions.
virtual const std::string & get_name() const
int get_id() const
Debugging aid.
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.
std::set< map_location > set_starting_position_labels(display &disp)
Set labels for staring positions in the given display object.
static editor_map from_string(std::string_view data)
Wrapper around editor_map(cfg, data) that catches possible exceptions and wraps them in a editor_map_...
void set_selection(const std::set< map_location > &area)
Select the given area.
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 playlist_contains(const std::shared_ptr< sound::music_track > &track) const
Checks whether the given track is part of current playlist.
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 toggle_track(const std::shared_ptr< sound::music_track > &track)
Remove the given track from the current playlist if present, else appends it.
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.
const std::string & get_filename() const
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.
virtual const editor_map & map() const override
Const map accessor.
void set_map(const editor_map &map)
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.
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 select_area(int index)
Select the nth tod area.
void clear_undo_redo()
Clear the undo and redo stacks.
bool is_embedded() const
virtual ~map_context()
Map context destructor.
std::list< std::shared_ptr< sound::music_track > > music_tracks_
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.
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.
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
void set_starting_position_labels(display &disp)
void clear_modified()
Clear the modified state.
bool is_pure_map() const
bool victory_defeated() const
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.
terrain_code get_terrain(const map_location &loc) const
Looks up terrain at a particular location.
Definition map.cpp:265
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:352
bool empty() const
Tell if the map is of 0 size.
Definition map.hpp:68
std::string write() const
Definition map.cpp:172
gamemap_base::set_terrain_result 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:359
const terrain_type & get_terrain_info(const t_translation::terrain_code &terrain) const
Definition map.cpp:78
@ yes_no_buttons
Shows a yes and no button.
Definition message.hpp:81
@ auto_close
Enables auto close.
Definition message.hpp:71
void write(config &res) const
Definition label.cpp:81
void clear_all()
Definition label.cpp:241
void read(const config &cfg)
Definition label.cpp:93
static prefs & get()
void add_recent_files_entry(const std::string &path)
Adds an entry to the recent files list.
static std::shared_ptr< music_track > create(const config &cfg)
This class stores all the data for a single 'side' (in game nomenclature).
Definition team.hpp:74
bool uses_shroud() const
Definition team.hpp:344
const std::set< std::string > & recruits() const
Definition team.hpp:250
bool no_leader() const
Definition team.hpp:369
team_shared_vision::type share_vision() const
Definition team.hpp:419
void change_team(const std::string &name, const t_string &user_name)
Definition team.cpp:602
int gold() const
Definition team.hpp:183
const std::set< map_location > & villages() const
Definition team.hpp:178
int raw_income() const
Definition team.hpp:189
side_controller::type controller() const
Definition team.hpp:282
const std::string & team_name() const
Definition team.hpp:323
bool uses_fog() const
Definition team.hpp:345
bool hidden() const
Definition team.hpp:375
const t_string & user_team_name() const
Definition team.hpp:324
t_translation::terrain_code terrain_with_default_base() const
Return the overlay part of this terrain, on the default_base().
Definition terrain.cpp:269
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:39
static unit_ptr create(const config &cfg, bool use_traits=false, const vconfig *vcfg=nullptr)
Initializes a unit from a config.
Definition unit.hpp:107
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
const config * cfg
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:100
bool unrenamable() const
Whether this unit can be renamed.
Definition unit.hpp:342
const std::string & type_id() const
The id of this unit's type.
Definition unit.cpp:1964
bool can_recruit() const
Whether this unit can recruit other units - ie, are they a leader unit.
Definition unit.hpp:534
const std::string & id() const
Gets this unit's id.
Definition unit.hpp:286
int side() const
The side this unit belongs to.
Definition unit.hpp:249
const t_string & name() const
Gets this unit's translatable display name.
Definition unit.hpp:309
const map_location & get_location() const
The current map location this unit is at.
Definition unit.hpp:1327
map_location::direction facing() const
The current direction this unit is facing within its hex.
Definition unit.hpp:1343
bool loyal() const
Gets whether this unit is loyal - ie, it costs no upkeep.
Definition unit.cpp:1761
std::string id
Text to match against addon_info.tags()
Definition manager.cpp:199
#define log_scope2(domain, description)
Definition log.hpp:276
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)
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 file_exists(const std::string &name)
Returns true if a file or directory with such name already exists.
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=false)
Returns the base filename of a file, with directory name stripped.
std::string get_map_file(const std::string &name)
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=utils::nullopt)
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
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=std::ios_base::binary)
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...
std::string get_current_editor_dir(const std::string &addon_id)
std::string path
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
@ OK
Dialog was closed with the OK button.
Definition retval.hpp:35
config read(std::istream &in, abstract_validator *validator)
Definition parser.cpp:610
constexpr ter_layer NO_LAYER
std::set< std::string > split_set(std::string_view s, char sep, const int flags)
bool contains(const Container &container, const Value &value)
Returns true iff value is found in container.
Definition general.hpp:88
std::string join(const Range &v, const std::string &s=",")
Generates a new string joining container items in a list.
std::map< std::string, t_string > string_map
filesystem::scoped_istream preprocess_string(const std::string &contents, const std::string &textdomain)
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
std::string filename
Filename.
editor_team_info(const team &t)
An exception object used when an IO error occurs.
Encapsulates the map of the game.
Definition location.hpp:46
static std::string write_direction(direction dir)
Definition location.cpp:154
void write(config &cfg) const
Definition location.cpp:223
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...
#define e
#define c