The Battle for Wesnoth  1.19.18+dev
edit_unit.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2023 - 2025
3  by Subhraman Sarkar (babaissarkar) <suvrax@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 /* unit type editor dialog */
17 
18 #define GETTEXT_DOMAIN "wesnoth-lib"
19 
21 
22 #include "filesystem.hpp"
23 #include "gettext.hpp"
25 #include "gui/dialogs/message.hpp"
28 #include "gui/widgets/button.hpp"
29 #include "gui/widgets/combobox.hpp"
30 #include "gui/widgets/image.hpp"
31 #include "gui/widgets/label.hpp"
36 #include "gui/widgets/slider.hpp"
37 #include "gui/widgets/spinner.hpp"
39 #include "gui/widgets/text_box.hpp"
41 #include "picture.hpp"
43 #include "serialization/parser.hpp"
45 #include "units/types.hpp"
46 
47 #include <boost/algorithm/string/replace.hpp>
48 #include <boost/filesystem.hpp>
49 #include <sstream>
50 
51 namespace gui2::dialogs
52 {
53 
55 
56 // TODO properly support attributes from installed addons
58  : modal_dialog(window_id())
59  , game_config_(game_config)
60  , addon_id_(addon_id)
61 {
62  //TODO some weapon specials can have args (PLAGUE_TYPE)
63  io::read(*preprocess_file(game_config::path+"/data/core/macros/weapon_specials.cfg", specials_map_));
64  for (const auto& x : specials_map_) {
65  specials_list_.emplace_back("label", x.first, "checkbox", false);
66  }
67 
68  io::read(*preprocess_file(game_config::path+"/data/core/macros/abilities.cfg", abilities_map_));
69  for (const auto& x : abilities_map_) {
70  // Don't add any macros that have INTERNAL
71  if (x.first.find("INTERNAL") == std::string::npos) {
72  abilities_list_.emplace_back("label", x.first, "checkbox", false);
73  }
74  }
75 
76  connect_signal<event::SDL_KEY_DOWN>(std::bind(
77  &editor_edit_unit::signal_handler_sdl_key_down, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5, std::placeholders::_6));
78 }
79 
81  tab_container& tabs = find_widget<tab_container>("tabs");
83 
84  button& quit = find_widget<button>("exit");
86 
87  //
88  // Main Stats tab
89  //
90 
91  menu_button& alignments = find_widget<menu_button>("alignment_list");
92  for (auto& align : unit_alignments::values) {
93  // Show the user the translated strings,
94  // but use the untranslated align strings for generated WML
95  const std::string& icon_path = "icons/alignments/alignment_" + std::string(align) + "_30.png";
96  align_list_.emplace_back("label", t_string(static_cast<std::string>(align), "wesnoth"), "icon", icon_path);
97  }
98  alignments.set_values(align_list_);
99 
100  menu_button& races = find_widget<menu_button>("race_list");
101  for(const race_map::value_type& i : unit_types.races()) {
102  const std::string& race_name = i.second.id();
103  race_list_.emplace_back("label", race_name, "icon", i.second.get_icon_path_stem() + "_30.png");
104  }
105 
106  if (!race_list_.empty()) {
107  races.set_values(race_list_);
108  }
109 
110  button& load = find_widget<button>("load_unit_type");
111  std::stringstream tooltip;
112  tooltip << t_string("Hotkey(s): ", "wesnoth");
113  #ifdef __APPLE__
114  tooltip << "cmd+o";
115  #else
116  tooltip << "ctrl+o";
117  #endif
118  load.set_tooltip(tooltip.str());
120 
122  find_widget<button>("browse_unit_image"),
123  std::bind(&editor_edit_unit::select_file, this, "data/core/images/units", "unit_image"));
125  find_widget<button>("preview_unit_image"),
126  std::bind(&editor_edit_unit::update_image, this, "unit_image"));
128  find_widget<button>("browse_portrait_image"),
129  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "portrait_image"));
131  find_widget<button>("preview_portrait_image"),
132  std::bind(&editor_edit_unit::update_image, this, "portrait_image"));
133 
135  find_widget<text_box>("name_box"),
136  std::bind(&editor_edit_unit::button_state_change, this));
138  find_widget<text_box>("id_box"),
139  std::bind(&editor_edit_unit::button_state_change, this));
140 
141  //
142  // Advanced Tab
143  //
144 
145  menu_button& movetypes = find_widget<menu_button>("movetype_list");
146  for(const auto& mt : unit_types.movement_types()) {
147  movetype_list_.emplace_back("label", mt.first);
148  }
149 
150  if (!movetype_list_.empty()) {
151  movetypes.set_values(movetype_list_);
152  }
153 
154  menu_button& defenses = find_widget<menu_button>("defense_list");
155  const config& defense_attr = game_config_
156  .mandatory_child("units")
157  .mandatory_child("movetype")
158  .mandatory_child("defense");
159  for (const auto& [key, _] : defense_attr.attribute_range()) {
160  defense_list_.emplace_back("label", key);
161  }
162 
163  menu_button& movement_costs = find_widget<menu_button>("movement_costs_list");
164  if (!defense_list_.empty()) {
165  defenses.set_values(defense_list_);
166  def_toggles_.resize(defense_list_.size());
167  movement_costs.set_values(defense_list_);
168  move_toggles_.resize(defense_list_.size());
169  }
170 
171  menu_button& resistances = find_widget<menu_button>("resistances_list");
172 
173  const config& resistances_attr = game_config_
174  .mandatory_child("units")
175  .mandatory_child("movetype")
176  .mandatory_child("resistance");
177  for (const auto& [key, _] : resistances_attr.attribute_range()) {
178  resistances_list_.emplace_back("label", key, "icon", "icons/profiles/" + key + ".png");
179  }
180 
181  if (!resistances_list_.empty()) {
182  resistances.set_values(resistances_list_);
183  res_toggles_.resize(resistances_list_.size());
184  }
185 
186  menu_button& usage_types = find_widget<menu_button>("usage_list");
187  usage_type_list_.emplace_back("label", _("scout"));
188  usage_type_list_.emplace_back("label", _("fighter"));
189  usage_type_list_.emplace_back("label", _("archer"));
190  usage_type_list_.emplace_back("label", _("mixed fighter"));
191  usage_type_list_.emplace_back("label", _("healer"));
192  usage_types.set_values(usage_type_list_);
193 
194  multimenu_button& abilities = find_widget<multimenu_button>("abilities_list");
195  abilities.set_values(abilities_list_);
196 
198  find_widget<button>("browse_small_profile_image"),
199  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "small_profile_image"));
201  find_widget<button>("preview_small_profile_image"),
202  std::bind(&editor_edit_unit::update_image, this, "small_profile_image"));
204  find_widget<button>("load_movetype"),
205  std::bind(&editor_edit_unit::load_movetype, this));
207  find_widget<slider>("resistances_slider"),
208  std::bind(&editor_edit_unit::store_resistances, this));
210  find_widget<menu_button>("resistances_list"),
211  std::bind(&editor_edit_unit::update_resistances, this));
213  find_widget<toggle_button>("resistances_checkbox"),
215 
217  find_widget<slider>("defense_slider"),
218  std::bind(&editor_edit_unit::store_defenses, this));
220  find_widget<menu_button>("defense_list"),
221  std::bind(&editor_edit_unit::update_defenses, this));
223  find_widget<toggle_button>("defense_checkbox"),
224  std::bind(&editor_edit_unit::enable_defense_slider, this));
225 
227  find_widget<slider>("movement_costs_slider"),
228  std::bind(&editor_edit_unit::store_movement_costs, this));
230  find_widget<menu_button>("movement_costs_list"),
231  std::bind(&editor_edit_unit::update_movement_costs, this));
233  find_widget<toggle_button>("movement_costs_checkbox"),
234  std::bind(&editor_edit_unit::enable_movement_slider, this));
235 
236  if (!res_toggles_.empty()) {
238  }
239 
240  if (!def_toggles_.empty()) {
242  }
243 
244  if (!move_toggles_.empty()) {
246  }
247 
248  //
249  // Attack Tab
250  //
251 
252  multimenu_button& specials = find_widget<multimenu_button>("weapon_specials_list");
253  specials.set_values(specials_list_);
254 
255  combobox& attack_types = find_widget<combobox>("attack_type_list");
256  if (!resistances_list_.empty()) {
257  attack_types.set_values(resistances_list_);
258  }
259 
260  // Connect signals
262  find_widget<button>("browse_attack_image"),
263  std::bind(&editor_edit_unit::select_file, this, "data/core/images/attacks", "attack_image"));
265  find_widget<button>("preview_attack_image"),
266  std::bind(&editor_edit_unit::update_image, this, "attack_image"));
268  find_widget<menu_button>("atk_list"),
269  std::bind(&editor_edit_unit::select_attack, this));
271  find_widget<button>("atk_new"),
272  std::bind(&editor_edit_unit::add_attack, this));
274  find_widget<button>("atk_delete"),
275  std::bind(&editor_edit_unit::delete_attack, this));
277  find_widget<button>("atk_next"),
278  std::bind(&editor_edit_unit::next_attack, this));
280  find_widget<button>("atk_prev"),
281  std::bind(&editor_edit_unit::prev_attack, this));
282 
283  update_index();
284 
285  // Disable OK button at start, since ID and Name boxes are empty
287 }
288 
290 {
291  save_unit_type();
292 
293  tab_container& tabs = find_widget<tab_container>("tabs");
294  if (tabs.get_active_tab_index() == 3) {
295  update_wml_view();
296  }
297 }
298 
299 void editor_edit_unit::select_file(const std::string& default_dir, const std::string& id_stem)
300 {
302  dlg.set_title(_("Choose File"))
303  .set_ok_label(_("Select"))
304  .set_path(default_dir)
305  .set_read_only(true);
306 
307  // If the file is found inside Wesnoth's data, give its relative path
308  // if not, ask user if they want to copy it into their addon.
309  // If yes, copy and return correct relative path inside addon.
310  // return empty otherwise.
311  auto find_or_copy = [](const std::string& path, const std::string& addon_id, const std::string& type) {
312  const std::string message
313  = _("This file is outside Wesnoth’s data dirs. Do you wish to copy it into your add-on?");
314  const auto optional_path = filesystem::to_asset_path(path, addon_id, type);
315 
316  if(optional_path.has_value()) {
317  return optional_path.value();
320  output_path /= type;
321  output_path /= boost::filesystem::path(path).filename();
322  filesystem::copy_file(path, output_path.string());
323  return output_path.filename().string();
324  } else {
325  return std::string();
326  }
327  };
328 
329  if (dlg.show()) {
330  if((id_stem == "unit_image")
331  || (id_stem == "portrait_image")
332  || (id_stem == "small_profile_image")
333  || (id_stem == "attack_image"))
334  {
335  find_widget<text_box>("path_"+id_stem).set_value(find_or_copy(dlg.path(), addon_id_, "images"));
336  update_image(id_stem);
337  }
338  }
339 }
340 
342  const auto& all_type_list = unit_types.types_list();
343  const auto& type_select = gui2::dialogs::units_dialog::build_create_dialog(all_type_list);
344 
345  if (!type_select->show() && !type_select->is_selected()) {
346  return;
347  }
348 
349  const auto& type = all_type_list[type_select->get_selected_index()];
350 
351  find_widget<text_box>("id_box").set_value(type->id());
352  find_widget<text_box>("name_box").set_value(type->type_name().base_str());
353  find_widget<spinner>("level_box").set_value(type->level());
354  find_widget<slider>("cost_slider").set_value(type->cost());
355  find_widget<text_box>("adv_box").set_value(utils::join(type->advances_to()));
356  find_widget<slider>("hp_slider").set_value(type->hitpoints());
357  find_widget<slider>("xp_slider").set_value(type->experience_needed());
358  find_widget<slider>("move_slider").set_value(type->movement());
359  find_widget<scroll_text>("desc_box").set_value(type->unit_description().base_str());
360  find_widget<text_box>("path_unit_image").set_value(type->image());
361  find_widget<text_box>("path_portrait_image").set_value(type->big_profile());
362 
363  for (const auto& gender : type->genders())
364  {
365  if (gender == unit_race::GENDER::MALE) {
366  find_widget<toggle_button>("gender_male").set_value(true);
367  }
368 
369  if (gender == unit_race::GENDER::FEMALE) {
370  find_widget<toggle_button>("gender_female").set_value(true);
371  }
372  }
373 
375  find_widget<menu_button>("race_list"),
376  race_list_,
377  type->race_id());
378 
380  find_widget<menu_button>("alignment_list"),
381  align_list_,
382  unit_alignments::get_string(type->alignment()));
383 
384  update_image("unit_image");
385 
386  find_widget<text_box>("path_small_profile_image").set_value(type->small_profile());
387 
389  find_widget<menu_button>("movetype_list"),
391  type->movement_type_id());
392 
393  config cfg;
394  type->movement_type().write(cfg, false);
395  movement_ = cfg.mandatory_child("movement_costs");
396  defenses_ = cfg.mandatory_child("defense");
397  resistances_ = cfg.mandatory_child("resistance");
398 
399  // Overrides for resistance/defense/movement costs
400  for (unsigned i = 0; i < resistances_list_.size(); i++) {
401  if (!type->get_cfg().has_child("resistance")) {
402  break;
403  }
404 
405  for (const auto& [key, _] : type->get_cfg().mandatory_child("resistance").attribute_range()) {
406  if (resistances_list_.at(i)["label"] == key) {
407  res_toggles_[i] = 1;
408  }
409  }
410  }
411 
412  for (unsigned i = 0; i < defense_list_.size(); i++) {
413  if (type->get_cfg().has_child("defense")) {
414  for (const auto& [key, _] : type->get_cfg().mandatory_child("defense").attribute_range()) {
415  if (defense_list_.at(i)["label"] == key) {
416  def_toggles_[i] = 1;
417  }
418  }
419  }
420 
421  if (type->get_cfg().has_child("movement_costs")) {
422  for (const auto& [key, _] : type->get_cfg().mandatory_child("movement_costs").attribute_range()) {
423  if (defense_list_.at(i)["label"] == key) {
424  move_toggles_[i] = 1;
425  }
426  }
427  }
428  }
429 
431  update_defenses();
433 
435  find_widget<menu_button>("usage_list"),
437  type->usage());
438 
439  update_image("small_profile_image");
440 
441  attacks_.clear();
442  for(const auto& atk : type->attacks())
443  {
444  config attack;
445  boost::dynamic_bitset<> enabled(specials_list_.size());
446  attack["name"] = atk.id();
447  attack["description"] = atk.name().base_str();
448  attack["icon"] = atk.icon();
449  attack["range"] = atk.range();
450  attack["damage"] = atk.damage();
451  attack["number"] = atk.num_attacks();
452  attack["type"] = atk.type();
453  attacks_.push_back(std::make_pair(enabled, attack));
454  }
455 
456  if (!type->attacks().empty()) {
457  selected_attack_ = 1;
458  update_attacks();
459  }
460 
461  update_index();
462 
465 }
466 
468 
469  // Clear the config
470  type_cfg_.clear();
471 
472  // Textdomain
473  std::string current_textdomain = "wesnoth-" + addon_id_;
474 
475  config& utype = type_cfg_.add_child("unit_type");
476  utype["id"] = find_widget<text_box>("id_box").get_value();
477  utype["name"] = t_string(find_widget<text_box>("name_box").get_value(), current_textdomain);
478  utype["image"] = find_widget<text_box>("path_unit_image").get_value();
479  utype["profile"] = find_widget<text_box>("path_portrait_image").get_value();
480  utype["level"] = find_widget<spinner>("level_box").get_value();
481  utype["advances_to"] = find_widget<text_box>("adv_box").get_value();
482  utype["hitpoints"] = find_widget<slider>("hp_slider").get_value();
483  utype["experience"] = find_widget<slider>("xp_slider").get_value();
484  utype["cost"] = find_widget<slider>("cost_slider").get_value();
485  utype["movement"] = find_widget<slider>("move_slider").get_value();
486  utype["description"] = t_string(find_widget<scroll_text>("desc_box").get_value(), current_textdomain);
487  utype["race"] = find_widget<menu_button>("race_list").get_value_string();
488  utype["alignment"] = unit_alignments::values[find_widget<menu_button>("alignment_list").get_value()];
489 
490  // Gender
491  if (find_widget<toggle_button>("gender_male").get_value()) {
492  if (find_widget<toggle_button>("gender_female").get_value()) {
493  utype["gender"] = "male,female";
494  } else {
495  utype["gender"] = "male";
496  }
497  } else {
498  if (find_widget<toggle_button>("gender_female").get_value()) {
499  utype["gender"] = "female";
500  }
501  }
502 
503  // Page 2
504 
505  utype["small_profile"] = find_widget<text_box>("path_small_profile_image").get_value();
506  utype["movement_type"] = find_widget<menu_button>("movetype_list").get_value_string();
507  utype["usage"] = find_widget<menu_button>("usage_list").get_value_string();
508 
509  if (res_toggles_.any()) {
510  config& resistances = utype.add_child("resistance");
511  int i = 0;
512  for (const auto& [key, _] : resistances_.attribute_range()) {
513  if (res_toggles_[i]) {
514  resistances[key] = resistances_[key];
515  }
516  i++;
517  }
518  }
519 
520  if (def_toggles_.any()) {
521  config& defenses = utype.add_child("defense");
522  int i = 0;
523  for (const auto& [key, _] : defenses_.attribute_range()) {
524  if (def_toggles_[i]) {
525  defenses[key] = defenses_[key];
526  }
527  i++;
528  }
529  }
530 
531  if (move_toggles_.any()) {
532  config& movement_costs = utype.add_child("movement_costs");
533  int i = 0;
534  for (const auto& [key, _] : movement_.attribute_range()) {
535  if (move_toggles_[i]) {
536  movement_costs[key] = movement_[key];
537  }
538  i++;
539  }
540  }
541 
542  const auto& abilities_states = find_widget<multimenu_button>("abilities_list").get_toggle_states();
543  if (abilities_states.any()) {
544  unsigned int i = 0;
545  sel_abilities_.clear();
546  for (const auto& x : abilities_map_) {
547  if (i >= abilities_states.size()) {
548  break;
549  }
550 
551  if (abilities_states[i] == true) {
552  sel_abilities_.push_back(x.first);
553  }
554 
555  i++;
556  }
557  }
558 
559  // Note : attacks and abilities are not written to the config, since they have macros.
560 }
561 
563  find_widget<slider>("resistances_slider")
564  .set_value(
565  100 - resistances_[find_widget<menu_button>("resistances_list").get_value_string()].to_int());
566 
567  find_widget<slider>("resistances_slider")
568  .set_active(res_toggles_[find_widget<menu_button>("resistances_list").get_value()]);
569 
570  find_widget<toggle_button>("resistances_checkbox")
571  .set_value(res_toggles_[find_widget<menu_button>("resistances_list").get_value()]);
572 }
573 
575  resistances_[find_widget<menu_button>("resistances_list").get_value_string()]
576  = 100 - find_widget<slider>("resistances_slider").get_value();
577 }
578 
580  bool toggle = find_widget<toggle_button>("resistances_checkbox").get_value();
581  res_toggles_[find_widget<menu_button>("resistances_list").get_value()] = toggle;
582  find_widget<slider>("resistances_slider").set_active(toggle);
583 }
584 
586  find_widget<slider>("defense_slider")
587  .set_value(
588  100 - defenses_[find_widget<menu_button>("defense_list").get_value_string()].to_int());
589 
590  find_widget<slider>("defense_slider")
591  .set_active(def_toggles_[find_widget<menu_button>("defense_list").get_value()]);
592 
593  find_widget<toggle_button>("defense_checkbox")
594  .set_value(def_toggles_[find_widget<menu_button>("defense_list").get_value()]);
595 }
596 
598  defenses_[find_widget<menu_button>("defense_list").get_value_string()]
599  = 100 - find_widget<slider>("defense_slider").get_value();
600 }
601 
603  bool toggle = find_widget<toggle_button>("defense_checkbox").get_value();
604  def_toggles_[find_widget<menu_button>("defense_list").get_value()] = toggle;
605  find_widget<slider>("defense_slider").set_active(toggle);
606 }
607 
609  find_widget<slider>("movement_costs_slider")
610  .set_value(
611  movement_[find_widget<menu_button>("movement_costs_list").get_value_string()].to_int());
612 
613  find_widget<slider>("movement_costs_slider")
614  .set_active(move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()]);
615 
616  find_widget<toggle_button>("movement_costs_checkbox")
617  .set_value(move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()]);
618 }
619 
621  movement_[find_widget<menu_button>("movement_costs_list").get_value_string()]
622  = find_widget<slider>("movement_costs_slider").get_value();
623 }
624 
626  bool toggle = find_widget<toggle_button>("movement_costs_checkbox").get_value();
627  move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()] = toggle;
628  find_widget<slider>("movement_costs_slider").set_active(toggle);
629 }
630 
632  // Textdomain
633  std::string current_textdomain = "wesnoth-"+addon_id_;
634 
635  // Save current attack data
636  if (selected_attack_ < 1) {
637  return;
638  }
639 
640  config& attack = attacks_.at(selected_attack_-1).second;
641 
642  attack["name"] = find_widget<text_box>("atk_id_box").get_value();
643  attack["description"] = t_string(find_widget<text_box>("atk_name_box").get_value(), current_textdomain);
644  attack["icon"] = find_widget<text_box>("path_attack_image").get_value();
645  attack["type"] = find_widget<combobox>("attack_type_list").get_value();
646  attack["damage"] = find_widget<slider>("dmg_box").get_value();
647  attack["number"] = find_widget<slider>("dmg_num_box").get_value();
648  attack["range"] = find_widget<combobox>("range_list").get_value();
649 
650  attacks_.at(selected_attack_-1).first = find_widget<multimenu_button>("weapon_specials_list").get_toggle_states();
651 }
652 
654  //Load data
655  if (selected_attack_ < 1) {
656  return;
657  }
658 
659  config& attack = attacks_.at(selected_attack_-1).second;
660 
661  find_widget<text_box>("atk_id_box").set_value(attack["name"]);
662  find_widget<text_box>("atk_name_box").set_value(attack["description"]);
663  find_widget<text_box>("path_attack_image").set_value(attack["icon"]);
664  update_image("attack_image");
665  find_widget<slider>("dmg_box").set_value(attack["damage"].to_int());
666  find_widget<slider>("dmg_num_box").set_value(attack["number"].to_int());
667  find_widget<combobox>("range_list").set_value(attack["range"]);
668 
670  find_widget<combobox>("attack_type_list"), resistances_list_, attack["type"]);
671 
672  find_widget<multimenu_button>("weapon_specials_list")
673  .select_options(attacks_.at(selected_attack_-1).first);
674 }
675 
677  find_widget<button>("atk_prev").set_active(selected_attack_ > 1);
678  find_widget<button>("atk_delete").set_active(selected_attack_ > 0);
679  find_widget<button>("atk_next").set_active(selected_attack_ != attacks_.size());
680 
681  if (!attacks_.empty()) {
682  std::vector<config> atk_name_list;
683  for(const auto& atk_data : attacks_) {
684  atk_name_list.emplace_back("label", atk_data.second["name"]);
685  }
686  menu_button& atk_list = find_widget<menu_button>("atk_list");
687  atk_list.set_values(atk_name_list);
688  atk_list.set_selected(selected_attack_-1, false);
689  }
690 
691  //Set index
692  const std::string new_index_str = formatter() << selected_attack_ << "/" << attacks_.size();
693  find_widget<label>("atk_number").set_label(new_index_str);
694 }
695 
697  // Textdomain
698  std::string current_textdomain = "wesnoth-"+addon_id_;
699 
700  config attack;
701 
702  attack["name"] = find_widget<text_box>("atk_id_box").get_value();
703  attack["description"] = t_string(find_widget<text_box>("atk_name_box").get_value(), current_textdomain);
704  attack["icon"] = find_widget<text_box>("path_attack_image").get_value();
705  attack["type"] = find_widget<combobox>("attack_type_list").get_value();
706  attack["damage"] = find_widget<slider>("dmg_box").get_value();
707  attack["number"] = find_widget<slider>("dmg_num_box").get_value();
708  attack["range"] = find_widget<combobox>("range_list").get_value();
709 
711 
712  attacks_.insert(
713  attacks_.begin() + selected_attack_ - 1
714  , std::make_pair(find_widget<multimenu_button>("weapon_specials_list").get_toggle_states(), attack));
715 
716  update_index();
717 }
718 
720  if (!attacks_.empty()) {
721  attacks_.erase(attacks_.begin() + selected_attack_ - 1);
722  }
723 
724  if (attacks_.empty()) {
725  // clear fields instead since there are no attacks to show
726  selected_attack_ = 0;
727  find_widget<button>("atk_delete").set_active(false);
728  } else if (selected_attack_ == 1) {
729  // 1st attack removed, show the next one
730  next_attack();
731  } else {
732  // show previous attack otherwise
733  prev_attack();
734  }
735 
736  update_index();
737 }
738 
740  store_attack();
741 
742  if (attacks_.size() > 1) {
744  update_attacks();
745  }
746 
747  update_index();
748 }
749 
751  store_attack();
752 
753  if (selected_attack_ > 0) {
755  }
756 
757  if (attacks_.size() > 1) {
758  update_attacks();
759  }
760 
761  update_index();
762 }
763 
765  selected_attack_ = find_widget<menu_button>("atk_list").get_value()+1;
766  update_attacks();
767  update_index();
768 }
769 
770 //TODO Check if works with non-mainline movetypes
772  for(const auto& movetype : game_config_
773  .mandatory_child("units")
774  .child_range("movetype")) {
775  if (movetype["name"] == find_widget<menu_button>("movetype_list").get_value_string()) {
776  // Set resistances
777  resistances_ = movetype.mandatory_child("resistance");
779  // Set defense
780  defenses_ = movetype.mandatory_child("defense");
781  update_defenses();
782  // Set movement
783  movement_ = movetype.mandatory_child("movement_costs");
785  }
786  }
787 }
788 
789 void editor_edit_unit::write_macro(std::ostream& out, unsigned level, const std::string& macro_name)
790 {
791  for(unsigned i = 0; i < level; i++)
792  {
793  out << "\t";
794  }
795  out << "{" << macro_name << "}\n";
796 }
797 
799  store_attack();
800  save_unit_type();
801 
802  std::stringstream wml_stream;
803 
804  // Textdomain
805  std::string current_textdomain = "wesnoth-"+addon_id_;
806 
807  wml_stream
808  << "#textdomain " << current_textdomain << "\n"
809  << "#\n"
810  << "# This file was generated using the scenario editor.\n"
811  << "#\n";
812 
813  {
814  config_writer out(wml_stream, false);
815  int level = 0;
816 
817  out.open_child("unit_type");
818 
819  level++;
820  for (const auto& [key, value] : type_cfg_.mandatory_child("unit_type").attribute_range()) {
821  io::write_key_val(wml_stream, key, value, level, current_textdomain);
822  }
823 
824  // Abilities
825  if (!sel_abilities_.empty()) {
826  out.open_child("abilities");
827  level++;
828  for (const std::string& ability : sel_abilities_) {
829  write_macro(wml_stream, level, ability);
830  }
831  level--;
832  out.close_child("abilities");
833  }
834 
835  // Attacks
836  if (!attacks_.empty()) {
837  for (const auto& atk : attacks_) {
838  out.open_child("attack");
839  level++;
840  for (const auto& [key, value] : atk.second.attribute_range()) {
841  if (!value.empty()) {
842  io::write_key_val(wml_stream, key, value, level, current_textdomain);
843  }
844  }
845 
846  if(atk.first.any()) {
847  out.open_child("specials");
848  level++;
849  int i = 0;
850  for (const auto& attr : specials_map_) {
851  if (atk.first[i]) {
852  write_macro(wml_stream, level, attr.first);
853  }
854  i++;
855  }
856  level--;
857  out.close_child("specials");
858 
859  }
860  level--;
861  out.close_child("attack");
862  }
863  }
864 
865  if (!movement_.empty() && (move_toggles_.size() <= movement_.attribute_count()) && move_toggles_.any())
866  {
867  out.open_child("movement_costs");
868  level++;
869  int i = 0;
870  for (const auto& [key, value] : movement_.attribute_range()) {
871  if (move_toggles_[i] == 1) {
872  io::write_key_val(wml_stream, key, value, level, current_textdomain);
873  }
874  i++;
875  }
876  level--;
877  out.close_child("movement_costs");
878  }
879 
880  if (!defenses_.empty() && def_toggles_.any() && (def_toggles_.size() <= defenses_.attribute_count()))
881  {
882  out.open_child("defense");
883  level++;
884  int i = 0;
885  for (const auto& [key, value] : defenses_.attribute_range()) {
886  if (def_toggles_[i] == 1) {
887  io::write_key_val(wml_stream, key, value, level, current_textdomain);
888  }
889  i++;
890  }
891  level--;
892  out.close_child("defense");
893  }
894 
895  if (!resistances_.empty() && res_toggles_.any() && (res_toggles_.size() <= resistances_.attribute_count()))
896  {
897  out.open_child("resistance");
898  level++;
899  int i = 0;
900  for (const auto& [key, value] : resistances_.attribute_range()) {
901  if (res_toggles_[i] == 1) {
902  io::write_key_val(wml_stream, key, value, level, current_textdomain);
903  }
904  i++;
905  }
906  level--;
907  out.close_child("resistance");
908  }
909 
910  out.close_child("unit_type");
911  }
912 
913  generated_wml = wml_stream.str();
914 
915  find_widget<scroll_text>("wml_view").set_label(generated_wml);
916 }
917 
918 void editor_edit_unit::update_image(const std::string& id_stem) {
919  std::string rel_path = find_widget<text_box>("path_"+id_stem).get_value();
920 
921  // remove IPF
922  if (rel_path.find("~") != std::string::npos) {
923  rel_path = rel_path.substr(0, rel_path.find("~"));
924  }
925 
926  int scale_size = 200; // TODO: Arbitrary, can be changed later.
927  if (rel_path.size() > 0) {
928  point img_size = ::image::get_size(::image::locator{rel_path});
929  float aspect_ratio = static_cast<float>(img_size.x)/img_size.y;
930  if(img_size.x > scale_size) {
931  rel_path.append("~SCALE(" + std::to_string(scale_size) + "," + std::to_string(scale_size*aspect_ratio) + ")");
932  } else if (img_size.y > scale_size) {
933  rel_path.append("~SCALE(" + std::to_string(scale_size/aspect_ratio) + "," + std::to_string(scale_size) + ")");
934  }
935  }
936 
937  if (id_stem == "portrait_image") {
938  // portrait image uses same [image] as unit_image
939  find_widget<image>("unit_image").set_label(rel_path);
940  } else {
941  find_widget<image>(id_stem).set_label(rel_path);
942  }
943 
945  queue_redraw();
946 }
947 
948 bool editor_edit_unit::check_id(const std::string& id) {
949  for(char c : id) {
950  if (!(std::isalnum(c) || c == '_' || c == ' ')) {
951  // One bad char means entire id string is invalid
952  return false;
953  }
954  }
955  return true;
956 }
957 
959  std::string id = find_widget<text_box>("id_box").get_value();
960  std::string name = find_widget<text_box>("name_box").get_value();
961 
962  find_widget<button>("ok").set_active(!id.empty() && !name.empty() && check_id(id));
963 
964  queue_redraw();
965 }
966 
968  const std::string& message
969  = _("Unsaved changes will be lost. Do you want to leave?");
972  }
973 }
974 
976  // Write the file
977  update_wml_view();
978 
979  std::string unit_name = type_cfg_.mandatory_child("unit_type")["name"];
980  boost::algorithm::replace_all(unit_name, " ", "_");
981 
982  // Path to <unit_type_name>.cfg
983  std::string unit_path = filesystem::get_current_editor_dir(addon_id_) + "/units/" + unit_name + filesystem::wml_extension;
984 
985  // Write to file
986  try {
988  gui2::show_transient_message("", _("Unit type saved."));
989  } catch(const filesystem::io_exception& e) {
990  gui2::show_transient_message("", e.what());
991  }
992 }
993 
995  bool& handled,
996  const SDL_Keycode key,
997  SDL_Keymod modifier)
998 {
999  #ifdef __APPLE__
1000  // Idiomatic modifier key in macOS computers.
1001  const SDL_Keycode modifier_key = KMOD_GUI;
1002  #else
1003  // Idiomatic modifier key in Microsoft desktop environments. Common in
1004  // GNU/Linux as well, to some extent.
1005  const SDL_Keycode modifier_key = KMOD_CTRL;
1006  #endif
1007 
1008  // Ctrl+O shortcut for Load Unit Type
1009  switch(key) {
1010  case SDLK_o:
1011  if (modifier & modifier_key) {
1012  handled = true;
1013  load_unit_type();
1014  }
1015  break;
1016  }
1017 
1018 }
1019 
1020 }
Class for writing a config out to a file in pieces.
void close_child(const std::string &key)
void open_child(const std::string &key)
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:157
config & add_child(std::string_view key)
Definition: config.cpp:436
std::size_t attribute_count() const
Count the number of non-blank attributes.
Definition: config.cpp:307
const_attr_itors attribute_range() const
Definition: config.cpp:740
bool empty() const
Definition: config.cpp:823
config & mandatory_child(std::string_view key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition: config.cpp:362
void clear()
Definition: config.cpp:802
std::ostringstream wrapper.
Definition: formatter.hpp:40
A class grating read only view to a vector of config objects, viewed as one config with all children ...
const config & mandatory_child(std::string_view key) const
Simple push button.
Definition: button.hpp:36
Class for a combobox.
Definition: combobox.hpp:37
void set_values(const std::vector<::config > &values, unsigned selected=0)
Definition: combobox.cpp:274
Dialog that allows user to create custom unit types.
Definition: edit_unit.hpp:33
unsigned int selected_attack_
0 means there are no attacks.
Definition: edit_unit.hpp:69
void write()
Write the cfg file.
Definition: edit_unit.cpp:975
void quit_confirmation()
Quit confirmation.
Definition: edit_unit.cpp:967
std::vector< config > defense_list_
Definition: edit_unit.hpp:57
boost::dynamic_bitset res_toggles_
Used to control checkboxes for various resistances, defences, etc.
Definition: edit_unit.hpp:55
const game_config_view & game_config_
Definition: edit_unit.hpp:44
std::vector< std::string > sel_abilities_
Need this because can't store macros in config.
Definition: edit_unit.hpp:63
std::vector< std::pair< boost::dynamic_bitset<>, config > > attacks_
Definition: edit_unit.hpp:60
std::vector< config > abilities_list_
Definition: edit_unit.hpp:58
void store_attack()
Callbacks for attack page.
Definition: edit_unit.cpp:631
void set_selected_from_string(menu_button &list, std::vector< config > values, std::string item)
Definition: edit_unit.hpp:133
std::vector< config > movetype_list_
Definition: edit_unit.hpp:57
std::vector< config > race_list_
Definition: edit_unit.hpp:57
void update_defenses()
Callbacks for defense list.
Definition: edit_unit.cpp:585
std::string generated_wml
Generated WML.
Definition: edit_unit.hpp:66
void save_unit_type()
Save Unit Type data to cfg.
Definition: edit_unit.cpp:467
std::vector< config > align_list_
Definition: edit_unit.hpp:57
void button_state_change()
Callback to enable/disable OK button if ID/Name is invalid.
Definition: edit_unit.cpp:958
void signal_handler_sdl_key_down(const event::ui_event, bool &handled, const SDL_Keycode key, SDL_Keymod modifier)
Definition: edit_unit.cpp:994
editor_edit_unit(const game_config_view &game_config, const std::string &addon_id)
Definition: edit_unit.cpp:57
void select_file(const std::string &default_dir, const std::string &id_stem)
Callback for file select button.
Definition: edit_unit.cpp:299
boost::dynamic_bitset move_toggles_
Definition: edit_unit.hpp:55
void update_movement_costs()
Callbacks for movement list.
Definition: edit_unit.cpp:608
std::vector< config > resistances_list_
Definition: edit_unit.hpp:57
void on_page_select()
Callback when an tab item in the "page" listbox is selected.
Definition: edit_unit.cpp:289
void update_resistances()
Callback for resistance list.
Definition: edit_unit.cpp:562
bool check_id(const std::string &id)
Utility method to check if ID contains any invalid characters.
Definition: edit_unit.cpp:948
const std::string & addon_id_
Definition: edit_unit.hpp:45
std::vector< config > specials_list_
Definition: edit_unit.hpp:58
boost::dynamic_bitset def_toggles_
Definition: edit_unit.hpp:55
void load_unit_type()
Load Unit Type data from cfg.
Definition: edit_unit.cpp:341
virtual void pre_show() override
Actions to be taken before showing the window.
Definition: edit_unit.cpp:80
void update_wml_view()
Update wml preview.
Definition: edit_unit.cpp:798
void write_macro(std::ostream &out, unsigned level, const std::string &macro_name)
Write macro to a stream at specified tab level.
Definition: edit_unit.cpp:789
std::vector< config > usage_type_list_
Definition: edit_unit.hpp:57
void load_movetype()
Callback for loading movetype data in UI.
Definition: edit_unit.cpp:771
void update_image(const std::string &id_stem)
Callback for image update.
Definition: edit_unit.cpp:918
file_dialog & set_ok_label(const std::string &value)
Sets the OK button label.
file_dialog & set_path(const std::string &value)
Sets the initial file selection.
file_dialog & set_title(const std::string &value)
Sets the current dialog title text.
Definition: file_dialog.hpp:59
file_dialog & set_read_only(bool value)
Whether to provide user interface elements for manipulating existing objects.
std::string path() const
Gets the current file selection.
Main class to show messages to the user.
Definition: message.hpp:36
@ yes_no_buttons
Shows a yes and no button.
Definition: message.hpp:81
Abstract base class for all modal dialogs.
bool show(const unsigned auto_close_time=0)
Shows the window.
At the moment two kinds of tips are known:
Definition: tooltip.cpp:41
static std::unique_ptr< units_dialog > build_create_dialog(const std::vector< const unit_type * > &types_list)
void set_selected(unsigned selected, bool fire_event=true)
void set_values(const std::vector<::config > &values, unsigned selected=0)
void set_values(const std::vector<::config > &values)
Set the available menu options.
const t_string & tooltip() const
A container widget that shows one of its pages of widgets depending on which tab the user clicked.
unsigned get_active_tab_index()
void queue_redraw()
Indicates that this widget should be redrawn.
Definition: widget.cpp:464
void set_retval(const int retval, const bool close_window=true)
Sets there return value of the window.
Definition: window.hpp:387
void invalidate_layout()
Updates the size of the window.
Definition: window.cpp:759
static const std::string & type()
Static type getter that does not rely on the widget being constructed.
Generic locator abstracting the location of an image.
Definition: picture.hpp:59
The basic "size" of the unit - flying, small land, large land, etc.
Definition: movetype.hpp:44
const movement_type_map & movement_types() const
Definition: types.hpp:410
const race_map & races() const
Definition: types.hpp:409
const std::vector< const unit_type * > types_list() const
Definition: types.hpp:400
const config * cfg
Declarations for File-IO.
std::size_t i
Definition: function.cpp:1032
static std::string _(const char *str)
Definition: gettext.hpp:97
const std::string wml_extension
Definition: filesystem.hpp:81
void copy_file(const std::string &src, const std::string &dest)
Read a file and then writes it back out.
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_current_editor_dir(const std::string &addon_id)
utils::optional< std::string > to_asset_path(const std::string &path, const std::string &addon_id, const std::string &asset_type)
Helper function to convert absolute path to wesnoth relative path.
Game configuration data as global variables.
Definition: build_info.cpp:61
std::string path
Definition: filesystem.cpp:106
REGISTER_DIALOG(editor_edit_unit)
ui_event
The event sent to the dispatcher.
Definition: handler.hpp:115
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification &signal)
Connects a signal handler for getting a notification upon modification.
Definition: dispatcher.cpp:189
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:163
std::vector< game_tip > load(const config &cfg)
Loads the tips from a config.
Definition: tips.cpp:37
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
@ CANCEL
Dialog was closed with the CANCEL button.
Definition: retval.hpp:38
point get_size(const locator &i_locator, bool skip_cache)
Returns the width and height of an image.
Definition: picture.cpp:803
config read(std::istream &in, abstract_validator *validator)
Definition: parser.cpp:600
void write_key_val(std::ostream &out, const std::string &key, const config::attribute_value &value, unsigned level, std::string &textdomain)
Definition: parser.cpp:667
constexpr auto values
Definition: ranges.hpp:42
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
filesystem::scoped_istream preprocess_file(const std::string &fname, preproc_map &defines)
Function to use the WML preprocessor on a file.
static config unit_name(const unit *u)
Definition: reports.cpp:163
An exception object used when an IO error occurs.
Definition: filesystem.hpp:67
Holds a 2D point.
Definition: point.hpp:25
static std::string get_string(enum_type key)
Converts a enum to its string equivalent.
Definition: enum_base.hpp:46
mock_char c
unit_type_data unit_types
Definition: types.cpp:1514
#define e