The Battle for Wesnoth  1.19.20+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  for (const auto& [id, cfg] : unit_types.specials()) {
63  specials_list_.emplace_back("label", id, "checkbox", false);
64  }
65 
66  for (const auto& [id, cfg] : unit_types.abilities()) {
67  abilities_list_.emplace_back("label", id, "checkbox", false);
68  }
69 
70  connect_signal<event::SDL_KEY_DOWN>(std::bind(
71  &editor_edit_unit::signal_handler_sdl_key_down, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5, std::placeholders::_6));
72 }
73 
75  tab_container& tabs = find_widget<tab_container>("tabs");
77 
78  button& quit = find_widget<button>("exit");
80 
81  //
82  // Main Stats tab
83  //
84 
85  menu_button& alignments = find_widget<menu_button>("alignment_list");
86  for (auto& align : unit_alignments::values) {
87  // Show the user the translated strings,
88  // but use the untranslated align strings for generated WML
89  const std::string& icon_path = "icons/alignments/alignment_" + std::string(align) + "_30.png";
90  align_list_.emplace_back("label", t_string(static_cast<std::string>(align), "wesnoth"), "icon", icon_path);
91  }
92  alignments.set_values(align_list_);
93 
94  menu_button& races = find_widget<menu_button>("race_list");
95  for(const race_map::value_type& i : unit_types.races()) {
96  const std::string& race_name = i.second.id();
97  race_list_.emplace_back("label", race_name, "icon", i.second.get_icon_path_stem() + "_30.png");
98  }
99 
100  if (!race_list_.empty()) {
101  races.set_values(race_list_);
102  }
103 
104  button& load = find_widget<button>("load_unit_type");
105  std::stringstream tooltip;
106  tooltip << t_string("Hotkey(s): ", "wesnoth");
107  #ifdef __APPLE__
108  tooltip << "cmd+o";
109  #else
110  tooltip << "ctrl+o";
111  #endif
112  load.set_tooltip(tooltip.str());
114 
116  find_widget<button>("browse_unit_image"),
117  std::bind(&editor_edit_unit::select_file, this, "data/core/images/units", "unit_image"));
119  find_widget<button>("preview_unit_image"),
120  std::bind(&editor_edit_unit::update_image, this, "unit_image"));
122  find_widget<button>("browse_portrait_image"),
123  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "portrait_image"));
125  find_widget<button>("preview_portrait_image"),
126  std::bind(&editor_edit_unit::update_image, this, "portrait_image"));
127 
129  find_widget<text_box>("name_box"),
130  std::bind(&editor_edit_unit::button_state_change, this));
132  find_widget<text_box>("id_box"),
133  std::bind(&editor_edit_unit::button_state_change, this));
134 
135  //
136  // Advanced Tab
137  //
138 
139  menu_button& movetypes = find_widget<menu_button>("movetype_list");
140  for(const auto& mt : unit_types.movement_types()) {
141  movetype_list_.emplace_back("label", mt.first);
142  }
143 
144  if (!movetype_list_.empty()) {
145  movetypes.set_values(movetype_list_);
146  }
147 
148  menu_button& defenses = find_widget<menu_button>("defense_list");
149  const config& defense_attr = game_config_
150  .mandatory_child("units")
151  .mandatory_child("movetype")
152  .mandatory_child("defense");
153  for (const auto& [key, _] : defense_attr.attribute_range()) {
154  defense_list_.emplace_back("label", key);
155  }
156 
157  menu_button& movement_costs = find_widget<menu_button>("movement_costs_list");
158  if (!defense_list_.empty()) {
159  defenses.set_values(defense_list_);
160  def_toggles_.resize(defense_list_.size());
161  movement_costs.set_values(defense_list_);
162  move_toggles_.resize(defense_list_.size());
163  }
164 
165  menu_button& resistances = find_widget<menu_button>("resistances_list");
166 
167  const config& resistances_attr = game_config_
168  .mandatory_child("units")
169  .mandatory_child("movetype")
170  .mandatory_child("resistance");
171  for (const auto& [key, _] : resistances_attr.attribute_range()) {
172  resistances_list_.emplace_back("label", key, "icon", "icons/profiles/" + key + ".png");
173  }
174 
175  if (!resistances_list_.empty()) {
176  resistances.set_values(resistances_list_);
177  res_toggles_.resize(resistances_list_.size());
178  }
179 
180  menu_button& usage_types = find_widget<menu_button>("usage_list");
181  usage_type_list_.emplace_back("label", _("scout"));
182  usage_type_list_.emplace_back("label", _("fighter"));
183  usage_type_list_.emplace_back("label", _("archer"));
184  usage_type_list_.emplace_back("label", _("mixed fighter"));
185  usage_type_list_.emplace_back("label", _("healer"));
186  usage_types.set_values(usage_type_list_);
187 
188  multimenu_button& abilities = find_widget<multimenu_button>("abilities_list");
189  abilities.set_values(abilities_list_);
190 
192  find_widget<button>("browse_small_profile_image"),
193  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "small_profile_image"));
195  find_widget<button>("preview_small_profile_image"),
196  std::bind(&editor_edit_unit::update_image, this, "small_profile_image"));
198  find_widget<button>("load_movetype"),
199  std::bind(&editor_edit_unit::load_movetype, this));
201  find_widget<slider>("resistances_slider"),
202  std::bind(&editor_edit_unit::store_resistances, this));
204  find_widget<menu_button>("resistances_list"),
205  std::bind(&editor_edit_unit::update_resistances, this));
207  find_widget<toggle_button>("resistances_checkbox"),
209 
211  find_widget<slider>("defense_slider"),
212  std::bind(&editor_edit_unit::store_defenses, this));
214  find_widget<menu_button>("defense_list"),
215  std::bind(&editor_edit_unit::update_defenses, this));
217  find_widget<toggle_button>("defense_checkbox"),
218  std::bind(&editor_edit_unit::enable_defense_slider, this));
219 
221  find_widget<slider>("movement_costs_slider"),
222  std::bind(&editor_edit_unit::store_movement_costs, this));
224  find_widget<menu_button>("movement_costs_list"),
225  std::bind(&editor_edit_unit::update_movement_costs, this));
227  find_widget<toggle_button>("movement_costs_checkbox"),
228  std::bind(&editor_edit_unit::enable_movement_slider, this));
229 
230  if (!res_toggles_.empty()) {
232  }
233 
234  if (!def_toggles_.empty()) {
236  }
237 
238  if (!move_toggles_.empty()) {
240  }
241 
242  //
243  // Attack Tab
244  //
245 
246  multimenu_button& specials = find_widget<multimenu_button>("weapon_specials_list");
247  specials.set_values(specials_list_);
248 
249  combobox& attack_types = find_widget<combobox>("attack_type_list");
250  if (!resistances_list_.empty()) {
251  attack_types.set_values(resistances_list_);
252  }
253 
254  // Connect signals
256  find_widget<button>("browse_attack_image"),
257  std::bind(&editor_edit_unit::select_file, this, "data/core/images/attacks", "attack_image"));
259  find_widget<button>("preview_attack_image"),
260  std::bind(&editor_edit_unit::update_image, this, "attack_image"));
262  find_widget<menu_button>("atk_list"),
263  std::bind(&editor_edit_unit::select_attack, this));
265  find_widget<button>("atk_new"),
266  std::bind(&editor_edit_unit::add_attack, this));
268  find_widget<button>("atk_delete"),
269  std::bind(&editor_edit_unit::delete_attack, this));
271  find_widget<button>("atk_next"),
272  std::bind(&editor_edit_unit::next_attack, this));
274  find_widget<button>("atk_prev"),
275  std::bind(&editor_edit_unit::prev_attack, this));
276 
277  update_index();
278 
279  // Disable OK button at start, since ID and Name boxes are empty
281 }
282 
284 {
285  save_unit_type();
286 
287  tab_container& tabs = find_widget<tab_container>("tabs");
288  if (tabs.get_active_tab_index() == 3) {
289  update_wml_view();
290  }
291 }
292 
293 void editor_edit_unit::select_file(const std::string& default_dir, const std::string& id_stem)
294 {
296  dlg.set_title(_("Choose File"))
297  .set_ok_label(_("Select"))
298  .set_path(default_dir)
299  .set_read_only(true);
300 
301  // If the file is found inside Wesnoth's data, give its relative path
302  // if not, ask user if they want to copy it into their addon.
303  // If yes, copy and return correct relative path inside addon.
304  // return empty otherwise.
305  auto find_or_copy = [](const std::string& path, const std::string& addon_id, const std::string& type) {
306  const std::string message
307  = _("This file is outside Wesnoth’s data dirs. Do you wish to copy it into your add-on?");
308  const auto optional_path = filesystem::to_asset_path(path, addon_id, type);
309 
310  if(optional_path.has_value()) {
311  return optional_path.value();
314  output_path /= type;
315  output_path /= boost::filesystem::path(path).filename();
316  filesystem::copy_file(path, output_path.string());
317  return output_path.filename().string();
318  } else {
319  return std::string();
320  }
321  };
322 
323  if (dlg.show()) {
324  if((id_stem == "unit_image")
325  || (id_stem == "portrait_image")
326  || (id_stem == "small_profile_image")
327  || (id_stem == "attack_image"))
328  {
329  find_widget<text_box>("path_"+id_stem).set_value(find_or_copy(dlg.path(), addon_id_, "images"));
330  update_image(id_stem);
331  }
332  }
333 }
334 
336  const auto& all_type_list = unit_types.types_list();
337  const auto& type_select = gui2::dialogs::units_dialog::build_create_dialog(all_type_list);
338  type_select->set_ok_label("Load");
339 
340  if (!type_select->show() && !type_select->is_selected()) {
341  return;
342  }
343 
344  const auto& type = all_type_list[type_select->get_selected_index()];
345 
346  find_widget<text_box>("id_box").set_value(type->id());
347  find_widget<text_box>("name_box").set_value(type->type_name().base_str());
348  find_widget<spinner>("level_box").set_value(type->level());
349  find_widget<slider>("cost_slider").set_value(type->cost());
350  find_widget<text_box>("adv_box").set_value(utils::join(type->advances_to()));
351  find_widget<slider>("hp_slider").set_value(type->hitpoints());
352  find_widget<slider>("xp_slider").set_value(type->experience_needed());
353  find_widget<slider>("move_slider").set_value(type->movement());
354  find_widget<scroll_text>("desc_box").set_value(type->unit_description().base_str());
355  find_widget<text_box>("path_unit_image").set_value(type->image());
356  find_widget<text_box>("path_portrait_image").set_value(type->big_profile());
357 
358  for (const auto& gender : type->genders())
359  {
360  if (gender == unit_race::GENDER::MALE) {
361  find_widget<toggle_button>("gender_male").set_value(true);
362  }
363 
364  if (gender == unit_race::GENDER::FEMALE) {
365  find_widget<toggle_button>("gender_female").set_value(true);
366  }
367  }
368 
370  find_widget<menu_button>("race_list"),
371  race_list_,
372  type->race_id());
373 
375  find_widget<menu_button>("alignment_list"),
376  align_list_,
377  unit_alignments::get_string(type->alignment()));
378 
379  update_image("unit_image");
380 
381  boost::dynamic_bitset<> enabled_abilities(abilities_list_.size());
382  for (size_t i = 0; i < abilities_list_.size(); i++) {
383  for (auto special : type->abilities()) {
384  const std::string id = special->cfg()["unique_id"].str(special->id());
385  if (abilities_list_[i]["label"] == id) {
386  enabled_abilities[i] = true;
387  }
388  }
389  }
390  find_widget<multimenu_button>("abilities_list").select_options(enabled_abilities);
391 
392  find_widget<text_box>("path_small_profile_image").set_value(type->small_profile());
393 
395  find_widget<menu_button>("movetype_list"),
397  type->movement_type_id());
398 
399  config cfg;
400  type->movement_type().write(cfg, false);
401  movement_ = cfg.mandatory_child("movement_costs");
402  defenses_ = cfg.mandatory_child("defense");
403  resistances_ = cfg.mandatory_child("resistance");
404 
405  // Overrides for resistance/defense/movement costs
406  for (unsigned i = 0; i < resistances_list_.size(); i++) {
407  if (!type->get_cfg().has_child("resistance")) {
408  break;
409  }
410 
411  for (const auto& [key, _] : type->get_cfg().mandatory_child("resistance").attribute_range()) {
412  if (resistances_list_.at(i)["label"] == key) {
413  res_toggles_[i] = 1;
414  }
415  }
416  }
417 
418  for (unsigned i = 0; i < defense_list_.size(); i++) {
419  if (type->get_cfg().has_child("defense")) {
420  for (const auto& [key, _] : type->get_cfg().mandatory_child("defense").attribute_range()) {
421  if (defense_list_.at(i)["label"] == key) {
422  def_toggles_[i] = 1;
423  }
424  }
425  }
426 
427  if (type->get_cfg().has_child("movement_costs")) {
428  for (const auto& [key, _] : type->get_cfg().mandatory_child("movement_costs").attribute_range()) {
429  if (defense_list_.at(i)["label"] == key) {
430  move_toggles_[i] = 1;
431  }
432  }
433  }
434  }
435 
437  update_defenses();
439 
441  find_widget<menu_button>("usage_list"),
443  type->usage());
444 
445  update_image("small_profile_image");
446 
447  attacks_.clear();
448  for(const auto& atk : type->attacks())
449  {
450  config attack;
451  attack["name"] = atk.id();
452  attack["description"] = atk.name().base_str();
453  attack["icon"] = atk.icon();
454  attack["range"] = atk.range();
455  attack["damage"] = atk.damage();
456  attack["number"] = atk.num_attacks();
457  attack["type"] = atk.type();
458 
459  boost::dynamic_bitset<> enabled_specials(specials_list_.size());
460 
461  for (size_t i = 0; i < specials_list_.size(); i++) {
462  for (auto special : atk.specials()) {
463  const std::string id = special->cfg()["unique_id"].str(special->id());
464  if (specials_list_[i]["label"] == id) {
465  enabled_specials[i] = true;
466  }
467  }
468  }
469 
470  attacks_.push_back(std::make_pair(enabled_specials, attack));
471  }
472 
473  if (!type->attacks().empty()) {
474  selected_attack_ = 1;
475  update_attacks();
476  }
477 
478  update_index();
479 
482 }
483 
485 
486  // Clear the config
487  type_cfg_.clear();
488 
489  // Textdomain
490  std::string current_textdomain = "wesnoth-" + addon_id_;
491 
492  config& utype = type_cfg_.add_child("unit_type");
493  utype["id"] = find_widget<text_box>("id_box").get_value();
494  utype["name"] = t_string(find_widget<text_box>("name_box").get_value(), current_textdomain);
495  utype["image"] = find_widget<text_box>("path_unit_image").get_value();
496  utype["profile"] = find_widget<text_box>("path_portrait_image").get_value();
497  utype["level"] = find_widget<spinner>("level_box").get_value();
498  utype["advances_to"] = find_widget<text_box>("adv_box").get_value();
499  utype["hitpoints"] = find_widget<slider>("hp_slider").get_value();
500  utype["experience"] = find_widget<slider>("xp_slider").get_value();
501  utype["cost"] = find_widget<slider>("cost_slider").get_value();
502  utype["movement"] = find_widget<slider>("move_slider").get_value();
503  utype["description"] = t_string(find_widget<scroll_text>("desc_box").get_value(), current_textdomain);
504  utype["race"] = find_widget<menu_button>("race_list").get_value_string();
505  utype["alignment"] = unit_alignments::values[find_widget<menu_button>("alignment_list").get_value()];
506 
507  // Gender
508  if (find_widget<toggle_button>("gender_male").get_value()) {
509  if (find_widget<toggle_button>("gender_female").get_value()) {
510  utype["gender"] = "male,female";
511  } else {
512  utype["gender"] = "male";
513  }
514  } else {
515  if (find_widget<toggle_button>("gender_female").get_value()) {
516  utype["gender"] = "female";
517  }
518  }
519 
520  // Page 2
521 
522  utype["small_profile"] = find_widget<text_box>("path_small_profile_image").get_value();
523  utype["movement_type"] = find_widget<menu_button>("movetype_list").get_value_string();
524  utype["usage"] = find_widget<menu_button>("usage_list").get_value_string();
525 
526  if (res_toggles_.any()) {
527  config& resistances = utype.add_child("resistance");
528  int i = 0;
529  for (const auto& [key, _] : resistances_.attribute_range()) {
530  if (res_toggles_[i]) {
531  resistances[key] = resistances_[key];
532  }
533  i++;
534  }
535  }
536 
537  if (def_toggles_.any()) {
538  config& defenses = utype.add_child("defense");
539  int i = 0;
540  for (const auto& [key, _] : defenses_.attribute_range()) {
541  if (def_toggles_[i]) {
542  defenses[key] = defenses_[key];
543  }
544  i++;
545  }
546  }
547 
548  if (move_toggles_.any()) {
549  config& movement_costs = utype.add_child("movement_costs");
550  int i = 0;
551  for (const auto& [key, _] : movement_.attribute_range()) {
552  if (move_toggles_[i]) {
553  movement_costs[key] = movement_[key];
554  }
555  i++;
556  }
557  }
558 
559  std::vector<std::string> selected_abilities;
560  const auto& abilities_states = find_widget<multimenu_button>("abilities_list").get_toggle_states();
561  if (abilities_states.any()) {
562  for (size_t i = 0; i < abilities_states.size(); i++) {
563  if (abilities_states[i]) {
564  selected_abilities.push_back(abilities_list_[i]["label"]);
565  }
566  }
567  }
568  if (!selected_abilities.empty()) {
569  utype["abilities_list"] = utils::join(selected_abilities);
570  }
571 
572  // Page 3 (Attacks)
573 
574  for (const auto& [special_bits, cfg] : attacks_) {
575  config& atk = utype.add_child("attack", cfg);
576  std::vector<std::string> selected_specials;
577  for (size_t i = 0; i < special_bits.size(); i++) {
578  if (special_bits[i]) {
579  selected_specials.push_back(specials_list_[i]["label"]);
580  }
581  }
582  if (!selected_specials.empty()) {
583  atk["specials_list"] = utils::join(selected_specials);
584  }
585  }
586 }
587 
589  find_widget<slider>("resistances_slider")
590  .set_value(
591  100 - resistances_[find_widget<menu_button>("resistances_list").get_value_string()].to_int());
592 
593  find_widget<slider>("resistances_slider")
594  .set_active(res_toggles_[find_widget<menu_button>("resistances_list").get_value()]);
595 
596  find_widget<toggle_button>("resistances_checkbox")
597  .set_value(res_toggles_[find_widget<menu_button>("resistances_list").get_value()]);
598 }
599 
601  resistances_[find_widget<menu_button>("resistances_list").get_value_string()]
602  = 100 - find_widget<slider>("resistances_slider").get_value();
603 }
604 
606  bool toggle = find_widget<toggle_button>("resistances_checkbox").get_value();
607  res_toggles_[find_widget<menu_button>("resistances_list").get_value()] = toggle;
608  find_widget<slider>("resistances_slider").set_active(toggle);
609 }
610 
612  find_widget<slider>("defense_slider")
613  .set_value(
614  100 - defenses_[find_widget<menu_button>("defense_list").get_value_string()].to_int());
615 
616  find_widget<slider>("defense_slider")
617  .set_active(def_toggles_[find_widget<menu_button>("defense_list").get_value()]);
618 
619  find_widget<toggle_button>("defense_checkbox")
620  .set_value(def_toggles_[find_widget<menu_button>("defense_list").get_value()]);
621 }
622 
624  defenses_[find_widget<menu_button>("defense_list").get_value_string()]
625  = 100 - find_widget<slider>("defense_slider").get_value();
626 }
627 
629  bool toggle = find_widget<toggle_button>("defense_checkbox").get_value();
630  def_toggles_[find_widget<menu_button>("defense_list").get_value()] = toggle;
631  find_widget<slider>("defense_slider").set_active(toggle);
632 }
633 
635  find_widget<slider>("movement_costs_slider")
636  .set_value(
637  movement_[find_widget<menu_button>("movement_costs_list").get_value_string()].to_int());
638 
639  find_widget<slider>("movement_costs_slider")
640  .set_active(move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()]);
641 
642  find_widget<toggle_button>("movement_costs_checkbox")
643  .set_value(move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()]);
644 }
645 
647  movement_[find_widget<menu_button>("movement_costs_list").get_value_string()]
648  = find_widget<slider>("movement_costs_slider").get_value();
649 }
650 
652  bool toggle = find_widget<toggle_button>("movement_costs_checkbox").get_value();
653  move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()] = toggle;
654  find_widget<slider>("movement_costs_slider").set_active(toggle);
655 }
656 
658  // Textdomain
659  std::string current_textdomain = "wesnoth-"+addon_id_;
660 
661  // Save current attack data
662  if (selected_attack_ < 1) {
663  return;
664  }
665 
666  config attack;
667  attack["name"] = find_widget<text_box>("atk_id_box").get_value();
668  attack["description"] = t_string(find_widget<text_box>("atk_name_box").get_value(), current_textdomain);
669  attack["icon"] = find_widget<text_box>("path_attack_image").get_value();
670  attack["type"] = find_widget<combobox>("attack_type_list").get_value();
671  attack["damage"] = find_widget<slider>("dmg_box").get_value();
672  attack["number"] = find_widget<slider>("dmg_num_box").get_value();
673  attack["range"] = find_widget<combobox>("range_list").get_value();
674 
675  attacks_.at(selected_attack_-1) = {
676  find_widget<multimenu_button>("weapon_specials_list").get_toggle_states(),
677  attack
678  };
679 }
680 
682  //Load data
683  if (selected_attack_ < 1) {
684  return;
685  }
686 
687  config& attack = attacks_.at(selected_attack_-1).second;
688 
689  find_widget<text_box>("atk_id_box").set_value(attack["name"]);
690  find_widget<text_box>("atk_name_box").set_value(attack["description"]);
691  find_widget<text_box>("path_attack_image").set_value(attack["icon"]);
692  update_image("attack_image");
693  find_widget<slider>("dmg_box").set_value(attack["damage"].to_int());
694  find_widget<slider>("dmg_num_box").set_value(attack["number"].to_int());
695  find_widget<combobox>("range_list").set_value(attack["range"]);
696 
698  find_widget<combobox>("attack_type_list"), resistances_list_, attack["type"]);
699 
700  find_widget<multimenu_button>("weapon_specials_list")
701  .select_options(attacks_.at(selected_attack_-1).first);
702 }
703 
705  find_widget<button>("atk_prev").set_active(selected_attack_ > 1);
706  find_widget<button>("atk_delete").set_active(selected_attack_ > 0);
707  find_widget<button>("atk_next").set_active(selected_attack_ != attacks_.size());
708 
709  if (!attacks_.empty()) {
710  std::vector<config> atk_name_list;
711  for(const auto& atk_data : attacks_) {
712  atk_name_list.emplace_back("label", atk_data.second["name"]);
713  }
714  menu_button& atk_list = find_widget<menu_button>("atk_list");
715  atk_list.set_values(atk_name_list);
716  atk_list.set_selected(selected_attack_-1, false);
717  }
718 
719  // Set index
720  const std::string new_index_str = formatter() << selected_attack_ << "/" << attacks_.size();
721  find_widget<label>("atk_number").set_label(new_index_str);
722 }
723 
725  // Textdomain
726  std::string current_textdomain = "wesnoth-"+addon_id_;
727 
728  config attack;
729  attack["name"] = find_widget<text_box>("atk_id_box").get_value();
730  attack["description"] = t_string(find_widget<text_box>("atk_name_box").get_value(), current_textdomain);
731  attack["icon"] = find_widget<text_box>("path_attack_image").get_value();
732  attack["type"] = find_widget<combobox>("attack_type_list").get_value();
733  attack["damage"] = find_widget<slider>("dmg_box").get_value();
734  attack["number"] = find_widget<slider>("dmg_num_box").get_value();
735  attack["range"] = find_widget<combobox>("range_list").get_value();
736 
738 
739  attacks_.insert(
740  attacks_.begin() + selected_attack_ - 1
741  , std::make_pair(find_widget<multimenu_button>("weapon_specials_list").get_toggle_states(), attack));
742 
743  update_index();
744 }
745 
747  if (!attacks_.empty()) {
748  attacks_.erase(attacks_.begin() + selected_attack_ - 1);
749  }
750 
751  if (attacks_.empty()) {
752  // clear fields instead since there are no attacks to show
753  selected_attack_ = 0;
754  find_widget<button>("atk_delete").set_active(false);
755  } else if (selected_attack_ == 1) {
756  // 1st attack removed, show the next one
757  next_attack();
758  } else {
759  // show previous attack otherwise
760  prev_attack();
761  }
762 
763  update_index();
764 }
765 
767  store_attack();
768 
769  if (attacks_.size() > 1) {
771  update_attacks();
772  }
773 
774  update_index();
775 }
776 
778  store_attack();
779 
780  if (selected_attack_ > 0) {
782  }
783 
784  if (attacks_.size() > 1) {
785  update_attacks();
786  }
787 
788  update_index();
789 }
790 
792  selected_attack_ = find_widget<menu_button>("atk_list").get_value()+1;
793  update_attacks();
794  update_index();
795 }
796 
797 //TODO Check if works with non-mainline movetypes
799  for(const auto& movetype : game_config_
800  .mandatory_child("units")
801  .child_range("movetype")) {
802  if (movetype["name"] == find_widget<menu_button>("movetype_list").get_value_string()) {
803  // Set resistances
804  resistances_ = movetype.mandatory_child("resistance");
806  // Set defense
807  defenses_ = movetype.mandatory_child("defense");
808  update_defenses();
809  // Set movement
810  movement_ = movetype.mandatory_child("movement_costs");
812  }
813  }
814 }
815 
817  store_attack();
818  save_unit_type();
819 
820  std::stringstream wml_stream;
821 
822  // Textdomain
823  std::string current_textdomain = "wesnoth-" + addon_id_;
824 
825  wml_stream
826  << "#textdomain " << current_textdomain << "\n"
827  << "#\n"
828  << "# This file was generated using the scenario editor.\n"
829  << "#\n";
830 
831  config_writer out(wml_stream, false);
832 
833  config& utype_cfg = type_cfg_.mandatory_child("unit_type");
834 
835  // Update movement, defense and resistance in cfg
836  if (!movement_.empty() && (move_toggles_.size() <= movement_.attribute_count()) && move_toggles_.any())
837  {
838  config& mvt_cfg = utype_cfg.add_child("movement_costs");
839  int i = 0;
840  for (const auto& [key, value] : movement_.attribute_range()) {
841  if (move_toggles_[i] == 1) {
842  mvt_cfg[key] = value;
843  }
844  i++;
845  }
846  }
847 
848  if (!defenses_.empty() && def_toggles_.any() && (def_toggles_.size() <= defenses_.attribute_count()))
849  {
850  config& def_cfg = utype_cfg.add_child("defense");
851  int i = 0;
852  for (const auto& [key, value] : defenses_.attribute_range()) {
853  if (def_toggles_[i] == 1) {
854  def_cfg[key] = value;
855  }
856  i++;
857  }
858  }
859 
860  if (!resistances_.empty() && res_toggles_.any() && (res_toggles_.size() <= resistances_.attribute_count()))
861  {
862  config& res_cfg = utype_cfg.add_child("resistance");
863  int i = 0;
864  for (const auto& [key, value] : resistances_.attribute_range()) {
865  if (res_toggles_[i] == 1) {
866  res_cfg[key] = value;
867  }
868  i++;
869  }
870  }
871 
872  out.write(type_cfg_);
873  generated_wml = wml_stream.str();
874  find_widget<scroll_text>("wml_view").set_label(generated_wml);
875 }
876 
877 void editor_edit_unit::update_image(const std::string& id_stem) {
878  std::string rel_path = find_widget<text_box>("path_"+id_stem).get_value();
879 
880  // remove IPF
881  if (rel_path.find("~") != std::string::npos) {
882  rel_path = rel_path.substr(0, rel_path.find("~"));
883  }
884 
885  int scale_size = 200; // TODO: Arbitrary, can be changed later.
886  if (rel_path.size() > 0) {
887  point img_size = ::image::get_size(::image::locator{rel_path});
888  float aspect_ratio = static_cast<float>(img_size.x)/img_size.y;
889  if(img_size.x > scale_size) {
890  rel_path.append("~SCALE(" + std::to_string(scale_size) + "," + std::to_string(scale_size*aspect_ratio) + ")");
891  } else if (img_size.y > scale_size) {
892  rel_path.append("~SCALE(" + std::to_string(scale_size/aspect_ratio) + "," + std::to_string(scale_size) + ")");
893  }
894  }
895 
896  if (id_stem == "portrait_image") {
897  // portrait image uses same [image] as unit_image
898  find_widget<image>("unit_image").set_label(rel_path);
899  } else {
900  find_widget<image>(id_stem).set_label(rel_path);
901  }
902 
904  queue_redraw();
905 }
906 
907 bool editor_edit_unit::check_id(const std::string& id) {
908  for(char c : id) {
909  if (!(std::isalnum(c) || c == '_' || c == ' ')) {
910  // One bad char means entire id string is invalid
911  return false;
912  }
913  }
914  return true;
915 }
916 
918  std::string id = find_widget<text_box>("id_box").get_value();
919  std::string name = find_widget<text_box>("name_box").get_value();
920 
921  find_widget<button>("ok").set_active(!id.empty() && !name.empty() && check_id(id));
922 
923  queue_redraw();
924 }
925 
927  const std::string& message
928  = _("Unsaved changes will be lost. Do you want to leave?");
931  }
932 }
933 
935  // Write the file
936  update_wml_view();
937 
938  std::string unit_name = type_cfg_.mandatory_child("unit_type")["name"];
939  boost::algorithm::replace_all(unit_name, " ", "_");
940 
941  // Path to <unit_type_name>.cfg
942  std::string unit_path = filesystem::get_current_editor_dir(addon_id_) + "/units/" + unit_name + filesystem::wml_extension;
943 
944  // Write to file
945  try {
947  gui2::show_transient_message("", _("Unit type saved."));
948  } catch(const filesystem::io_exception& e) {
949  gui2::show_transient_message("", e.what());
950  }
951 }
952 
954  bool& handled,
955  const SDL_Keycode key,
956  SDL_Keymod modifier)
957 {
958  #ifdef __APPLE__
959  // Idiomatic modifier key in macOS computers.
960  const SDL_Keycode modifier_key = KMOD_GUI;
961  #else
962  // Idiomatic modifier key in Microsoft desktop environments. Common in
963  // GNU/Linux as well, to some extent.
964  const SDL_Keycode modifier_key = KMOD_CTRL;
965  #endif
966 
967  // Ctrl+O shortcut for Load Unit Type
968  switch(key) {
969  case SDLK_o:
970  if (modifier & modifier_key) {
971  handled = true;
972  load_unit_type();
973  }
974  break;
975  }
976 
977 }
978 
979 }
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: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:65
void write()
Write the cfg file.
Definition: edit_unit.cpp:934
void quit_confirmation()
Quit confirmation.
Definition: edit_unit.cpp:926
void set_selected_from_string(menu_button &list, std::vector< config > values, const std::string &item)
Definition: edit_unit.hpp:126
std::vector< config > defense_list_
Definition: edit_unit.hpp:56
boost::dynamic_bitset res_toggles_
Used to control checkboxes for various resistances, defences, etc.
Definition: edit_unit.hpp:54
const game_config_view & game_config_
Definition: edit_unit.hpp:44
std::vector< std::pair< boost::dynamic_bitset<>, config > > attacks_
Definition: edit_unit.hpp:59
std::vector< config > abilities_list_
Definition: edit_unit.hpp:57
void store_attack()
Callbacks for attack page.
Definition: edit_unit.cpp:657
std::vector< config > movetype_list_
Definition: edit_unit.hpp:56
std::vector< config > race_list_
Definition: edit_unit.hpp:56
void update_defenses()
Callbacks for defense list.
Definition: edit_unit.cpp:611
std::string generated_wml
Generated WML.
Definition: edit_unit.hpp:62
void save_unit_type()
Save Unit Type data to cfg.
Definition: edit_unit.cpp:484
std::vector< config > align_list_
Definition: edit_unit.hpp:56
void button_state_change()
Callback to enable/disable OK button if ID/Name is invalid.
Definition: edit_unit.cpp:917
void signal_handler_sdl_key_down(const event::ui_event, bool &handled, const SDL_Keycode key, SDL_Keymod modifier)
Definition: edit_unit.cpp:953
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:293
boost::dynamic_bitset move_toggles_
Definition: edit_unit.hpp:54
void update_movement_costs()
Callbacks for movement list.
Definition: edit_unit.cpp:634
std::vector< config > resistances_list_
Definition: edit_unit.hpp:56
void on_page_select()
Callback when an tab item in the "page" listbox is selected.
Definition: edit_unit.cpp:283
void update_resistances()
Callback for resistance list.
Definition: edit_unit.cpp:588
bool check_id(const std::string &id)
Utility method to check if ID contains any invalid characters.
Definition: edit_unit.cpp:907
const std::string & addon_id_
Definition: edit_unit.hpp:45
std::vector< config > specials_list_
Definition: edit_unit.hpp:57
boost::dynamic_bitset def_toggles_
Definition: edit_unit.hpp:54
void load_unit_type()
Load Unit Type data from cfg.
Definition: edit_unit.cpp:335
virtual void pre_show() override
Actions to be taken before showing the window.
Definition: edit_unit.cpp:74
void update_wml_view()
Update wml preview.
Definition: edit_unit.cpp:816
std::vector< config > usage_type_list_
Definition: edit_unit.hpp:56
void load_movetype()
Callback for loading movetype data in UI.
Definition: edit_unit.cpp:798
void update_image(const std::string &id_stem)
Callback for image update.
Definition: edit_unit.cpp:877
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 std::map< std::string, config > & abilities() const
Definition: types.hpp:409
const movement_type_map & movement_types() const
Definition: types.hpp:408
const race_map & races() const
Definition: types.hpp:407
const std::vector< const unit_type * > types_list() const
Definition: types.hpp:398
const std::map< std::string, config > & specials() const
Definition: types.hpp:410
const config * cfg
Declarations for File-IO.
std::size_t i
Definition: function.cpp:1031
static std::string _(const char *str)
Definition: gettext.hpp:97
const std::string wml_extension
Definition: filesystem.cpp:281
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:811
constexpr auto values
Definition: ranges.hpp:46
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
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:1494
#define e