The Battle for Wesnoth  1.19.7+dev
edit_unit.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2023 - 2024
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 "formula/string_utils.hpp"
24 #include "gettext.hpp"
26 #include "gui/dialogs/message.hpp"
29 #include "gui/widgets/button.hpp"
30 #include "gui/widgets/combobox.hpp"
31 #include "gui/widgets/image.hpp"
32 #include "gui/widgets/label.hpp"
33 #include "gui/widgets/listbox.hpp"
38 #include "gui/widgets/slider.hpp"
39 #include "gui/widgets/spinner.hpp"
41 #include "gui/widgets/text_box.hpp"
43 #include "picture.hpp"
45 #include "serialization/parser.hpp"
47 #include "units/types.hpp"
48 
49 #include <boost/algorithm/string/replace.hpp>
50 #include <boost/filesystem.hpp>
51 #include <sstream>
52 
53 namespace gui2::dialogs
54 {
55 
57 
58 // TODO properly support attributes from installed addons
60  : modal_dialog(window_id())
61  , game_config_(game_config)
62  , addon_id_(addon_id)
63 {
64  //TODO some weapon specials can have args (PLAGUE_TYPE)
65  config specials;
66 
67  read(specials, *(preprocess_file(game_config::path+"/data/core/macros/weapon_specials.cfg", &specials_map_)));
68  for (const auto& x : specials_map_) {
69  specials_list_.emplace_back("label", x.first, "checkbox", false);
70  }
71 
72  read(specials, *(preprocess_file(game_config::path+"/data/core/macros/abilities.cfg", &abilities_map_)));
73  for (const auto& x : abilities_map_) {
74  // Don't add any macros that have INTERNAL
75  if (x.first.find("INTERNAL") == std::string::npos) {
76  abilities_list_.emplace_back("label", x.first, "checkbox", false);
77  }
78  }
79 
80  connect_signal<event::SDL_KEY_DOWN>(std::bind(
81  &editor_edit_unit::signal_handler_sdl_key_down, this, std::placeholders::_2, std::placeholders::_3, std::placeholders::_5, std::placeholders::_6));
82 }
83 
85  tab_container& tabs = find_widget<tab_container>("tabs");
87 
88  button& quit = find_widget<button>("exit");
90 
91  //
92  // Main Stats tab
93  //
94 
95  tabs.select_tab(0);
96 
97  menu_button& alignments = find_widget<menu_button>("alignment_list");
98  for (auto& align : unit_alignments::values) {
99  // Show the user the translated strings,
100  // but use the untranslated align strings for generated WML
101  const std::string& icon_path = "icons/alignments/alignment_" + std::string(align) + "_30.png";
102  align_list_.emplace_back("label", t_string(static_cast<std::string>(align), "wesnoth"), "icon", icon_path);
103  }
104  alignments.set_values(align_list_);
105 
106  menu_button& races = find_widget<menu_button>("race_list");
107  for(const race_map::value_type& i : unit_types.races()) {
108  const std::string& race_name = i.second.id();
109  race_list_.emplace_back("label", race_name, "icon", i.second.get_icon_path_stem() + "_30.png");
110  }
111 
112  if (!race_list_.empty()) {
113  races.set_values(race_list_);
114  }
115 
116  button& load = find_widget<button>("load_unit_type");
117  std::stringstream tooltip;
118  tooltip << t_string("Hotkey(s): ", "wesnoth");
119  #ifdef __APPLE__
120  tooltip << "cmd+o";
121  #else
122  tooltip << "ctrl+o";
123  #endif
124  load.set_tooltip(tooltip.str());
126 
128  find_widget<button>("browse_unit_image"),
129  std::bind(&editor_edit_unit::select_file, this, "data/core/images/units", "unit_image"));
131  find_widget<button>("preview_unit_image"),
132  std::bind(&editor_edit_unit::update_image, this, "unit_image"));
134  find_widget<button>("browse_portrait_image"),
135  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "portrait_image"));
137  find_widget<button>("preview_portrait_image"),
138  std::bind(&editor_edit_unit::update_image, this, "portrait_image"));
139 
141  find_widget<text_box>("name_box"),
142  std::bind(&editor_edit_unit::button_state_change, this));
144  find_widget<text_box>("id_box"),
145  std::bind(&editor_edit_unit::button_state_change, this));
146 
147  //
148  // Advanced Tab
149  //
150  tabs.select_tab(1);
151 
152  menu_button& movetypes = find_widget<menu_button>("movetype_list");
153  for(const auto& mt : unit_types.movement_types()) {
154  movetype_list_.emplace_back("label", mt.first);
155  }
156 
157  if (!movetype_list_.empty()) {
158  movetypes.set_values(movetype_list_);
159  }
160 
161  menu_button& defenses = find_widget<menu_button>("defense_list");
162  const config& defense_attr = game_config_
163  .mandatory_child("units")
164  .mandatory_child("movetype")
165  .mandatory_child("defense");
166  for (const auto& [key, _] : defense_attr.attribute_range()) {
167  defense_list_.emplace_back("label", key);
168  }
169 
170  menu_button& movement_costs = find_widget<menu_button>("movement_costs_list");
171  if (!defense_list_.empty()) {
172  defenses.set_values(defense_list_);
173  def_toggles_.resize(defense_list_.size());
174  movement_costs.set_values(defense_list_);
175  move_toggles_.resize(defense_list_.size());
176  }
177 
178  menu_button& resistances = find_widget<menu_button>("resistances_list");
179 
180  const config& resistances_attr = game_config_
181  .mandatory_child("units")
182  .mandatory_child("movetype")
183  .mandatory_child("resistance");
184  for (const auto& [key, _] : resistances_attr.attribute_range()) {
185  resistances_list_.emplace_back("label", key, "icon", "icons/profiles/" + key + ".png");
186  }
187 
188  if (!resistances_list_.empty()) {
189  resistances.set_values(resistances_list_);
190  res_toggles_.resize(resistances_list_.size());
191  }
192 
193  menu_button& usage_types = find_widget<menu_button>("usage_list");
194  usage_type_list_.emplace_back("label", _("scout"));
195  usage_type_list_.emplace_back("label", _("fighter"));
196  usage_type_list_.emplace_back("label", _("archer"));
197  usage_type_list_.emplace_back("label", _("mixed fighter"));
198  usage_type_list_.emplace_back("label", _("healer"));
199  usage_types.set_values(usage_type_list_);
200 
201  multimenu_button& abilities = find_widget<multimenu_button>("abilities_list");
202  abilities.set_values(abilities_list_);
203 
205  find_widget<button>("browse_small_profile_image"),
206  std::bind(&editor_edit_unit::select_file, this, "data/core/images/portraits", "small_profile_image"));
208  find_widget<button>("preview_small_profile_image"),
209  std::bind(&editor_edit_unit::update_image, this, "small_profile_image"));
211  find_widget<button>("load_movetype"),
212  std::bind(&editor_edit_unit::load_movetype, this));
214  find_widget<slider>("resistances_slider"),
215  std::bind(&editor_edit_unit::store_resistances, this));
217  find_widget<menu_button>("resistances_list"),
218  std::bind(&editor_edit_unit::update_resistances, this));
220  find_widget<toggle_button>("resistances_checkbox"),
222 
224  find_widget<slider>("defense_slider"),
225  std::bind(&editor_edit_unit::store_defenses, this));
227  find_widget<menu_button>("defense_list"),
228  std::bind(&editor_edit_unit::update_defenses, this));
230  find_widget<toggle_button>("defense_checkbox"),
231  std::bind(&editor_edit_unit::enable_defense_slider, this));
232 
234  find_widget<slider>("movement_costs_slider"),
235  std::bind(&editor_edit_unit::store_movement_costs, this));
237  find_widget<menu_button>("movement_costs_list"),
238  std::bind(&editor_edit_unit::update_movement_costs, this));
240  find_widget<toggle_button>("movement_costs_checkbox"),
241  std::bind(&editor_edit_unit::enable_movement_slider, this));
242 
243  if (!res_toggles_.empty()) {
245  }
246 
247  if (!def_toggles_.empty()) {
249  }
250 
251  if (!move_toggles_.empty()) {
253  }
254 
255  //
256  // Attack Tab
257  //
258  tabs.select_tab(2);
259  multimenu_button& specials = find_widget<multimenu_button>("weapon_specials_list");
260  specials.set_values(specials_list_);
261 
262  combobox& attack_types = find_widget<combobox>("attack_type_list");
263  if (!resistances_list_.empty()) {
264  attack_types.set_values(resistances_list_);
265  }
266 
267  // Connect signals
269  find_widget<button>("browse_attack_image"),
270  std::bind(&editor_edit_unit::select_file, this, "data/core/images/attacks", "attack_image"));
272  find_widget<button>("preview_attack_image"),
273  std::bind(&editor_edit_unit::update_image, this, "attack_image"));
275  find_widget<menu_button>("atk_list"),
276  std::bind(&editor_edit_unit::select_attack, this));
278  find_widget<button>("atk_new"),
279  std::bind(&editor_edit_unit::add_attack, this));
281  find_widget<button>("atk_delete"),
282  std::bind(&editor_edit_unit::delete_attack, this));
284  find_widget<button>("atk_next"),
285  std::bind(&editor_edit_unit::next_attack, this));
287  find_widget<button>("atk_prev"),
288  std::bind(&editor_edit_unit::prev_attack, this));
289 
290  update_index();
291 
292  tabs.select_tab(0);
293 
294  // Disable OK button at start, since ID and Name boxes are empty
296 }
297 
299 {
300  save_unit_type();
301 
302  tab_container& tabs = find_widget<tab_container>("tabs");
303  if (tabs.get_active_tab_index() == 3) {
304  update_wml_view();
305  }
306 }
307 
308 void editor_edit_unit::select_file(const std::string& default_dir, const std::string& id_stem)
309 {
311  dlg.set_title(_("Choose File"))
312  .set_ok_label(_("Select"))
313  .set_path(default_dir)
314  .set_read_only(true);
315 
316  if (dlg.show()) {
317 
318  std::string dn = dlg.path();
319  const std::string& message
320  = _("This file is outside Wesnoth’s data dirs. Do you wish to copy it into your add-on?");
321 
322  if(id_stem == "unit_image") {
323 
324  if (!filesystem::to_asset_path(dn, addon_id_, "images")) {
326  filesystem::copy_file(dlg.path(), dn);
327  }
328  }
329 
330  } else if((id_stem == "portrait_image")||(id_stem == "small_profile_image")) {
331 
332  if (!filesystem::to_asset_path(dn, addon_id_, "images")) {
334  filesystem::copy_file(dlg.path(), dn);
335  }
336  }
337 
338  } else if(id_stem == "attack_image") {
339 
340  if (!filesystem::to_asset_path(dn, addon_id_, "images")) {
342  filesystem::copy_file(dlg.path(), dn);
343  }
344  }
345 
346  }
347 
348  find_widget<text_box>("path_"+id_stem).set_value(dn);
349  update_image(id_stem);
350  }
351 }
352 
355  if (dlg_uc.show()) {
356  const unit_type *type = unit_types.find(dlg_uc.choice());
357 
358  tab_container& tabs = find_widget<tab_container>("tabs");
359  tabs.select_tab(0);
360 
361  find_widget<text_box>("id_box").set_value(type->id());
362  find_widget<text_box>("name_box").set_value(type->type_name().base_str());
363  find_widget<spinner>("level_box").set_value(type->level());
364  find_widget<slider>("cost_slider").set_value(type->cost());
365  find_widget<text_box>("adv_box").set_value(utils::join(type->advances_to()));
366  find_widget<slider>("hp_slider").set_value(type->hitpoints());
367  find_widget<slider>("xp_slider").set_value(type->experience_needed());
368  find_widget<slider>("move_slider").set_value(type->movement());
369  find_widget<scroll_text>("desc_box").set_value(type->unit_description().base_str());
370  find_widget<text_box>("path_unit_image").set_value(type->image());
371  find_widget<text_box>("path_portrait_image").set_value(type->big_profile());
372 
373  for (const auto& gender : type->genders())
374  {
375  if (gender == unit_race::GENDER::MALE) {
376  find_widget<toggle_button>("gender_male").set_value(true);
377  }
378 
379  if (gender == unit_race::GENDER::FEMALE) {
380  find_widget<toggle_button>("gender_female").set_value(true);
381  }
382  }
383 
385  find_widget<menu_button>("race_list"),
386  race_list_,
387  type->race_id());
388 
390  find_widget<menu_button>("alignment_list"),
391  align_list_,
392  unit_alignments::get_string(type->alignment()));
393 
394  update_image("unit_image");
395 
396  tabs.select_tab(1);
397  find_widget<text_box>("path_small_profile_image").set_value(type->small_profile());
398 
400  find_widget<menu_button>("movetype_list"),
402  type->movement_type_id());
403 
404  config cfg;
405  type->movement_type().write(cfg, false);
406  movement_ = cfg.mandatory_child("movement_costs");
407  defenses_ = cfg.mandatory_child("defense");
408  resistances_ = cfg.mandatory_child("resistance");
409 
410  // Overrides for resistance/defense/movement costs
411  for (unsigned i = 0; i < resistances_list_.size(); i++) {
412  if (!type->get_cfg().has_child("resistance")) {
413  break;
414  }
415 
416  for (const auto& [key, _] : type->get_cfg().mandatory_child("resistance").attribute_range()) {
417  if (resistances_list_.at(i)["label"] == key) {
418  res_toggles_[i] = 1;
419  }
420  }
421  }
422 
423  for (unsigned i = 0; i < defense_list_.size(); i++) {
424  if (type->get_cfg().has_child("defense")) {
425  for (const auto& [key, _] : type->get_cfg().mandatory_child("defense").attribute_range()) {
426  if (defense_list_.at(i)["label"] == key) {
427  def_toggles_[i] = 1;
428  }
429  }
430  }
431 
432  if (type->get_cfg().has_child("movement_costs")) {
433  for (const auto& [key, _] : type->get_cfg().mandatory_child("movement_costs").attribute_range()) {
434  if (defense_list_.at(i)["label"] == key) {
435  move_toggles_[i] = 1;
436  }
437  }
438  }
439  }
440 
442  update_defenses();
444 
446  find_widget<menu_button>("usage_list"),
448  type->usage());
449 
450  update_image("small_profile_image");
451 
452  tabs.select_tab(2);
453  attacks_.clear();
454  for(const auto& atk : type->attacks())
455  {
456  config attack;
457  boost::dynamic_bitset<> enabled(specials_list_.size());
458  attack["name"] = atk.id();
459  attack["description"] = atk.name().base_str();
460  attack["icon"] = atk.icon();
461  attack["range"] = atk.range();
462  attack["damage"] = atk.damage();
463  attack["number"] = atk.num_attacks();
464  attack["type"] = atk.type();
465  attacks_.push_back(std::make_pair(enabled, attack));
466  }
467 
468  if (!type->attacks().empty()) {
469  selected_attack_ = 1;
470  update_attacks();
471  }
472 
473  update_index();
474 
475  tabs.select_tab(0);
476 
479  }
480 }
481 
483 
484  // Clear the config
485  type_cfg_.clear();
486 
487  // Textdomain
488  std::string current_textdomain = "wesnoth-"+addon_id_;
489 
490  tab_container& tabs = find_widget<tab_container>("tabs");
491 
492  // Page 1
493  grid* grid = tabs.get_tab_grid(0);
494 
495  config& utype = type_cfg_.add_child("unit_type");
496  utype["id"] = grid->find_widget<text_box>("id_box").get_value();
497  utype["name"] = t_string(grid->find_widget<text_box>("name_box").get_value(), current_textdomain);
498  utype["image"] = grid->find_widget<text_box>("path_unit_image").get_value();
499  utype["profile"] = grid->find_widget<text_box>("path_portrait_image").get_value();
500  utype["level"] = grid->find_widget<spinner>("level_box").get_value();
501  utype["advances_to"] = grid->find_widget<text_box>("adv_box").get_value();
502  utype["hitpoints"] = grid->find_widget<slider>("hp_slider").get_value();
503  utype["experience"] = grid->find_widget<slider>("xp_slider").get_value();
504  utype["cost"] = grid->find_widget<slider>("cost_slider").get_value();
505  utype["movement"] = grid->find_widget<slider>("move_slider").get_value();
506  utype["description"] = t_string(grid->find_widget<scroll_text>("desc_box").get_value(), current_textdomain);
507  utype["race"] = grid->find_widget<menu_button>("race_list").get_value_string();
508  utype["alignment"] = unit_alignments::values[grid->find_widget<menu_button>("alignment_list").get_value()];
509 
510  // Gender
511  if (grid->find_widget<toggle_button>("gender_male").get_value()) {
512  if (grid->find_widget<toggle_button>("gender_female").get_value()) {
513  utype["gender"] = "male,female";
514  } else {
515  utype["gender"] = "male";
516  }
517  } else {
518  if (grid->find_widget<toggle_button>("gender_female").get_value()) {
519  utype["gender"] = "female";
520  }
521  }
522 
523  // Page 2
524  grid = tabs.get_tab_grid(1);
525 
526  utype["small_profile"] = grid->find_widget<text_box>("path_small_profile_image").get_value();
527  utype["movement_type"] = grid->find_widget<menu_button>("movetype_list").get_value_string();
528  utype["usage"] = grid->find_widget<menu_button>("usage_list").get_value_string();
529 
530  if (res_toggles_.any()) {
531  config& resistances = utype.add_child("resistance");
532  int i = 0;
533  for (const auto& [key, _] : resistances_.attribute_range()) {
534  if (res_toggles_[i]) {
535  resistances[key] = resistances_[key];
536  }
537  i++;
538  }
539  }
540 
541  if (def_toggles_.any()) {
542  config& defenses = utype.add_child("defense");
543  int i = 0;
544  for (const auto& [key, _] : defenses_.attribute_range()) {
545  if (def_toggles_[i]) {
546  defenses[key] = defenses_[key];
547  }
548  i++;
549  }
550  }
551 
552  if (move_toggles_.any()) {
553  config& movement_costs = utype.add_child("movement_costs");
554  int i = 0;
555  for (const auto& [key, _] : movement_.attribute_range()) {
556  if (move_toggles_[i]) {
557  movement_costs[key] = movement_[key];
558  }
559  i++;
560  }
561  }
562 
563  const auto& abilities_states = grid->find_widget<multimenu_button>("abilities_list").get_toggle_states();
564  if (abilities_states.any()) {
565  unsigned int i = 0;
566  sel_abilities_.clear();
567  for (const auto& x : abilities_map_) {
568  if (i >= abilities_states.size()) {
569  break;
570  }
571 
572  if (abilities_states[i] == true) {
573  sel_abilities_.push_back(x.first);
574  }
575 
576  i++;
577  }
578  }
579 
580  // Note : attacks and abilities are not written to the config, since they have macros.
581 }
582 
584  find_widget<slider>("resistances_slider")
585  .set_value(
586  100 - resistances_[find_widget<menu_button>("resistances_list").get_value_string()].to_int());
587 
588  find_widget<slider>("resistances_slider")
589  .set_active(res_toggles_[find_widget<menu_button>("resistances_list").get_value()]);
590 
591  find_widget<toggle_button>("resistances_checkbox")
592  .set_value(res_toggles_[find_widget<menu_button>("resistances_list").get_value()]);
593 }
594 
596  resistances_[find_widget<menu_button>("resistances_list").get_value_string()]
597  = 100 - find_widget<slider>("resistances_slider").get_value();
598 }
599 
601  bool toggle = find_widget<toggle_button>("resistances_checkbox").get_value();
602  res_toggles_[find_widget<menu_button>("resistances_list").get_value()] = toggle;
603  find_widget<slider>("resistances_slider").set_active(toggle);
604 }
605 
607  find_widget<slider>("defense_slider")
608  .set_value(
609  100 - defenses_[find_widget<menu_button>("defense_list").get_value_string()].to_int());
610 
611  find_widget<slider>("defense_slider")
612  .set_active(def_toggles_[find_widget<menu_button>("defense_list").get_value()]);
613 
614  find_widget<toggle_button>("defense_checkbox")
615  .set_value(def_toggles_[find_widget<menu_button>("defense_list").get_value()]);
616 }
617 
619  defenses_[find_widget<menu_button>("defense_list").get_value_string()]
620  = 100 - find_widget<slider>("defense_slider").get_value();
621 }
622 
624  bool toggle = find_widget<toggle_button>("defense_checkbox").get_value();
625  def_toggles_[find_widget<menu_button>("defense_list").get_value()] = toggle;
626  find_widget<slider>("defense_slider").set_active(toggle);
627 }
628 
630  find_widget<slider>("movement_costs_slider")
631  .set_value(
632  movement_[find_widget<menu_button>("movement_costs_list").get_value_string()].to_int());
633 
634  find_widget<slider>("movement_costs_slider")
635  .set_active(move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()]);
636 
637  find_widget<toggle_button>("movement_costs_checkbox")
638  .set_value(move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()]);
639 }
640 
642  movement_[find_widget<menu_button>("movement_costs_list").get_value_string()]
643  = find_widget<slider>("movement_costs_slider").get_value();
644 }
645 
647  bool toggle = find_widget<toggle_button>("movement_costs_checkbox").get_value();
648  move_toggles_[find_widget<menu_button>("movement_costs_list").get_value()] = toggle;
649  find_widget<slider>("movement_costs_slider").set_active(toggle);
650 }
651 
653  // Textdomain
654  std::string current_textdomain = "wesnoth-"+addon_id_;
655 
656  // Save current attack data
657  if (selected_attack_ < 1) {
658  return;
659  }
660 
661  config& attack = attacks_.at(selected_attack_-1).second;
662 
663  tab_container& tabs = find_widget<tab_container>("tabs");
664  int prev_tab = tabs.get_active_tab_index();
665  tabs.select_tab(2);
666 
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).first = find_widget<multimenu_button>("weapon_specials_list").get_toggle_states();
676 
677  tabs.select_tab(prev_tab);
678 }
679 
681  //Load data
682  if (selected_attack_ < 1) {
683  return;
684  }
685 
686  config& attack = attacks_.at(selected_attack_-1).second;
687 
688  tab_container& tabs = find_widget<tab_container>("tabs");
689  int prev_tab = tabs.get_active_tab_index();
690  tabs.select_tab(2);
691 
692  find_widget<text_box>("atk_id_box").set_value(attack["name"]);
693  find_widget<text_box>("atk_name_box").set_value(attack["description"]);
694  find_widget<text_box>("path_attack_image").set_value(attack["icon"]);
695  update_image("attack_image");
696  find_widget<slider>("dmg_box").set_value(attack["damage"].to_int());
697  find_widget<slider>("dmg_num_box").set_value(attack["number"].to_int());
698  find_widget<combobox>("range_list").set_value(attack["range"]);
699 
701  find_widget<combobox>("attack_type_list"), resistances_list_, attack["type"]);
702 
703  find_widget<multimenu_button>("weapon_specials_list")
704  .select_options(attacks_.at(selected_attack_-1).first);
705 
706  tabs.select_tab(prev_tab);
707 }
708 
710  tab_container& tabs = find_widget<tab_container>("tabs");
711  int prev_tab = tabs.get_active_tab_index();
712  tabs.select_tab(2);
713 
714  find_widget<button>("atk_prev").set_active(selected_attack_ > 1);
715  find_widget<button>("atk_delete").set_active(selected_attack_ > 0);
716  find_widget<button>("atk_next").set_active(selected_attack_ != attacks_.size());
717 
718  if (!attacks_.empty()) {
719  std::vector<config> atk_name_list;
720  for(const auto& atk_data : attacks_) {
721  atk_name_list.emplace_back("label", atk_data.second["name"]);
722  }
723  menu_button& atk_list = find_widget<menu_button>("atk_list");
724  atk_list.set_values(atk_name_list);
725  atk_list.set_selected(selected_attack_-1, false);
726  }
727 
728  //Set index
729  const std::string new_index_str = formatter() << selected_attack_ << "/" << attacks_.size();
730  find_widget<label>("atk_number").set_label(new_index_str);
731 
732  tabs.select_tab(prev_tab);
733 }
734 
736  // Textdomain
737  std::string current_textdomain = "wesnoth-"+addon_id_;
738 
739  tab_container& tabs = find_widget<tab_container>("tabs");
740  int prev_tab = tabs.get_active_tab_index();
741  tabs.select_tab(2);
742 
743  config attack;
744 
745  attack["name"] = find_widget<text_box>("atk_id_box").get_value();
746  attack["description"] = t_string(find_widget<text_box>("atk_name_box").get_value(), current_textdomain);
747  attack["icon"] = find_widget<text_box>("path_attack_image").get_value();
748  attack["type"] = find_widget<combobox>("attack_type_list").get_value();
749  attack["damage"] = find_widget<slider>("dmg_box").get_value();
750  attack["number"] = find_widget<slider>("dmg_num_box").get_value();
751  attack["range"] = find_widget<combobox>("range_list").get_value();
752 
754 
755  attacks_.insert(
756  attacks_.begin() + selected_attack_ - 1
757  , std::make_pair(find_widget<multimenu_button>("weapon_specials_list").get_toggle_states(), attack));
758 
759  update_index();
760 
761  tabs.select_tab(prev_tab);
762 }
763 
765  tab_container& tabs = find_widget<tab_container>("tabs");
766  int prev_tab = tabs.get_active_tab_index();
767  tabs.select_tab(2);
768 
769  //remove attack
770  if (!attacks_.empty()) {
771  attacks_.erase(attacks_.begin() + selected_attack_ - 1);
772  }
773 
774  if (attacks_.empty()) {
775  // clear fields instead since there are no attacks to show
776  selected_attack_ = 0;
777  find_widget<button>("atk_delete").set_active(false);
778  } else if (selected_attack_ == 1) {
779  // 1st attack removed, show the next one
780  next_attack();
781  } else {
782  // show previous attack otherwise
783  prev_attack();
784  }
785 
786  update_index();
787 
788  tabs.select_tab(prev_tab);
789 }
790 
792  store_attack();
793 
794  if (attacks_.size() > 1) {
796  update_attacks();
797  }
798 
799  update_index();
800 }
801 
803  store_attack();
804 
805  if (selected_attack_ > 0) {
807  }
808 
809  if (attacks_.size() > 1) {
810  update_attacks();
811  }
812 
813  update_index();
814 }
815 
817  selected_attack_ = find_widget<menu_button>("atk_list").get_value()+1;
818  update_attacks();
819  update_index();
820 }
821 
822 //TODO Check if works with non-mainline movetypes
824  tab_container& tabs = find_widget<tab_container>("tabs");
825  int prev_tab = tabs.get_active_tab_index();
826  tabs.select_tab(1);
827 
828  for(const auto& movetype : game_config_
829  .mandatory_child("units")
830  .child_range("movetype")) {
831  if (movetype["name"] == find_widget<menu_button>("movetype_list").get_value_string()) {
832  // Set resistances
833  resistances_ = movetype.mandatory_child("resistance");
835  // Set defense
836  defenses_ = movetype.mandatory_child("defense");
837  update_defenses();
838  // Set movement
839  movement_ = movetype.mandatory_child("movement_costs");
841  }
842  }
843 
844  tabs.select_tab(prev_tab);
845 }
846 
847 void editor_edit_unit::write_macro(std::ostream& out, unsigned level, const std::string& macro_name)
848 {
849  for(unsigned i = 0; i < level; i++)
850  {
851  out << "\t";
852  }
853  out << "{" << macro_name << "}\n";
854 }
855 
857  store_attack();
858  save_unit_type();
859 
860  tab_container& tabs = find_widget<tab_container>("tabs");
861  tabs.select_tab(3);
862 
863  std::stringstream wml_stream;
864 
865  // Textdomain
866  std::string current_textdomain = "wesnoth-"+addon_id_;
867 
868  wml_stream
869  << "#textdomain " << current_textdomain << "\n"
870  << "#\n"
871  << "# This file was generated using the scenario editor.\n"
872  << "#\n";
873 
874  {
875  config_writer out(wml_stream, false);
876  int level = 0;
877 
878  out.open_child("unit_type");
879 
880  level++;
881  for (const auto& [key, value] : type_cfg_.mandatory_child("unit_type").attribute_range()) {
882  ::write_key_val(wml_stream, key, value, level, current_textdomain);
883  }
884 
885  // Abilities
886  if (!sel_abilities_.empty()) {
887  out.open_child("abilities");
888  level++;
889  for (const std::string& ability : sel_abilities_) {
890  write_macro(wml_stream, level, ability);
891  }
892  level--;
893  out.close_child("abilities");
894  }
895 
896  // Attacks
897  if (!attacks_.empty()) {
898  for (const auto& atk : attacks_) {
899  out.open_child("attack");
900  level++;
901  for (const auto& [key, value] : atk.second.attribute_range()) {
902  if (!value.empty()) {
903  ::write_key_val(wml_stream, key, value, level, current_textdomain);
904  }
905  }
906 
907  if(atk.first.any()) {
908  out.open_child("specials");
909  level++;
910  int i = 0;
911  for (const auto& attr : specials_map_) {
912  if (atk.first[i]) {
913  write_macro(wml_stream, level, attr.first);
914  }
915  i++;
916  }
917  level--;
918  out.close_child("specials");
919 
920  }
921  level--;
922  out.close_child("attack");
923  }
924  }
925 
926  if (!movement_.empty() && (move_toggles_.size() <= movement_.attribute_count()) && move_toggles_.any())
927  {
928  out.open_child("movement_costs");
929  level++;
930  int i = 0;
931  for (const auto& [key, value] : movement_.attribute_range()) {
932  if (move_toggles_[i] == 1) {
933  ::write_key_val(wml_stream, key, value, level, current_textdomain);
934  }
935  i++;
936  }
937  level--;
938  out.close_child("movement_costs");
939  }
940 
941  if (!defenses_.empty() && def_toggles_.any() && (def_toggles_.size() <= defenses_.attribute_count()))
942  {
943  out.open_child("defense");
944  level++;
945  int i = 0;
946  for (const auto& [key, value] : defenses_.attribute_range()) {
947  if (def_toggles_[i] == 1) {
948  ::write_key_val(wml_stream, key, value, level, current_textdomain);
949  }
950  i++;
951  }
952  level--;
953  out.close_child("defense");
954  }
955 
956  if (!resistances_.empty() && res_toggles_.any() && (res_toggles_.size() <= resistances_.attribute_count()))
957  {
958  out.open_child("resistance");
959  level++;
960  int i = 0;
961  for (const auto& [key, value] : resistances_.attribute_range()) {
962  if (res_toggles_[i] == 1) {
963  ::write_key_val(wml_stream, key, value, level, current_textdomain);
964  }
965  i++;
966  }
967  level--;
968  out.close_child("resistance");
969  }
970 
971  out.close_child("unit_type");
972  }
973 
974  generated_wml = wml_stream.str();
975 
976  find_widget<scroll_text>("wml_view").set_label(generated_wml);
977 }
978 
979 void editor_edit_unit::update_image(const std::string& id_stem) {
980  std::string rel_path = find_widget<text_box>("path_"+id_stem).get_value();
981 
982  // remove IPF
983  if (rel_path.find("~") != std::string::npos) {
984  rel_path = rel_path.substr(0, rel_path.find("~"));
985  }
986 
987  int scale_size = 200; // TODO: Arbitrary, can be changed later.
988  if (rel_path.size() > 0) {
989  point img_size = ::image::get_size(::image::locator{rel_path});
990  float aspect_ratio = static_cast<float>(img_size.x)/img_size.y;
991  if(img_size.x > scale_size) {
992  rel_path.append("~SCALE(" + std::to_string(scale_size) + "," + std::to_string(scale_size*aspect_ratio) + ")");
993  } else if (img_size.y > scale_size) {
994  rel_path.append("~SCALE(" + std::to_string(scale_size/aspect_ratio) + "," + std::to_string(scale_size) + ")");
995  }
996  }
997 
998  if (id_stem == "portrait_image") {
999  // portrait image uses same [image] as unit_image
1000  find_widget<image>("unit_image").set_label(rel_path);
1001  } else {
1002  find_widget<image>(id_stem).set_label(rel_path);
1003  }
1004 
1006  queue_redraw();
1007 }
1008 
1009 bool editor_edit_unit::check_id(const std::string& id) {
1010  for(char c : id) {
1011  if (!(std::isalnum(c) || c == '_' || c == ' ')) {
1012  // One bad char means entire id string is invalid
1013  return false;
1014  }
1015  }
1016  return true;
1017 }
1018 
1020  grid* grid = find_widget<tab_container>("tabs").get_tab_grid(0);
1021 
1022  std::string id = grid->find_widget<text_box>("id_box").get_value();
1023  std::string name = grid->find_widget<text_box>("name_box").get_value();
1024 
1025  find_widget<button>("ok").set_active(!id.empty() && !name.empty() && check_id(id));
1026 
1027  queue_redraw();
1028 }
1029 
1031  const std::string& message
1032  = _("Unsaved changes will be lost. Do you want to leave?");
1035  }
1036 }
1037 
1039  // Write the file
1040  update_wml_view();
1041 
1042  std::string unit_name = type_cfg_.mandatory_child("unit_type")["name"];
1043  boost::algorithm::replace_all(unit_name, " ", "_");
1044 
1045  // Path to <unit_type_name>.cfg
1046  std::string unit_path = filesystem::get_current_editor_dir(addon_id_) + "/units/" + unit_name + filesystem::wml_extension;
1047 
1048  // Write to file
1049  try {
1051  gui2::show_transient_message("", _("Unit type saved."));
1052  } catch(const filesystem::io_exception& e) {
1053  gui2::show_transient_message("", e.what());
1054  }
1055 }
1056 
1058  bool& handled,
1059  const SDL_Keycode key,
1060  SDL_Keymod modifier)
1061 {
1062  #ifdef __APPLE__
1063  // Idiomatic modifier key in macOS computers.
1064  const SDL_Keycode modifier_key = KMOD_GUI;
1065  #else
1066  // Idiomatic modifier key in Microsoft desktop environments. Common in
1067  // GNU/Linux as well, to some extent.
1068  const SDL_Keycode modifier_key = KMOD_CTRL;
1069  #endif
1070 
1071  // Ctrl+O shortcut for Load Unit Type
1072  switch(key) {
1073  case SDLK_o:
1074  if (modifier & modifier_key) {
1075  handled = true;
1076  load_unit_type();
1077  }
1078  break;
1079  }
1080 
1081 }
1082 
1083 }
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:172
std::size_t attribute_count() const
Count the number of non-blank attributes.
Definition: config.cpp:311
config & mandatory_child(config_key_type key, int n=0)
Returns the nth child with the given key, or throws an error if there is none.
Definition: config.cpp:366
const_attr_itors attribute_range() const
Definition: config.cpp:760
bool empty() const
Definition: config.cpp:849
void clear()
Definition: config.cpp:828
config & add_child(config_key_type key)
Definition: config.cpp:440
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(config_key_type 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:1038
void quit_confirmation()
Quit confirmation.
Definition: edit_unit.cpp:1030
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:652
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:606
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:482
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:1019
void signal_handler_sdl_key_down(const event::ui_event, bool &handled, const SDL_Keycode key, SDL_Keymod modifier)
Definition: edit_unit.cpp:1057
editor_edit_unit(const game_config_view &game_config, const std::string &addon_id)
Definition: edit_unit.cpp:59
void select_file(const std::string &default_dir, const std::string &id_stem)
Callback for file select button.
Definition: edit_unit.cpp:308
boost::dynamic_bitset move_toggles_
Definition: edit_unit.hpp:55
void update_movement_costs()
Callbacks for movement list.
Definition: edit_unit.cpp:629
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:298
void update_resistances()
Callback for resistance list.
Definition: edit_unit.cpp:583
bool check_id(const std::string &id)
Utility method to check if ID contains any invalid characters.
Definition: edit_unit.cpp:1009
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:353
virtual void pre_show() override
Actions to be taken before showing the window.
Definition: edit_unit.cpp:84
void update_wml_view()
Update wml preview.
Definition: edit_unit.cpp:856
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:847
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:823
void update_image(const std::string &id_stem)
Callback for image update.
Definition: edit_unit.cpp:979
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
const std::string & choice() const
Unit type choice from the user.
Definition: unit_create.hpp:40
Base container class.
Definition: grid.hpp:32
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.
std::string get_value()
Definition: scroll_text.cpp:76
const t_string & tooltip() const
A container widget that shows one of its pages of widgets depending on which tab the user clicked.
grid * get_tab_grid(unsigned i)
void select_tab(unsigned index)
unsigned get_active_tab_index()
std::string get_value() const
A widget that allows the user to input text in single line.
Definition: text_box.hpp:125
virtual unsigned get_value() const override
Inherited from selectable_item.
void queue_redraw()
Indicates that this widget should be redrawn.
Definition: widget.cpp:464
T * find_widget(const std::string_view id, const bool must_be_active, const bool must_exist)
Gets a widget with the wanted id.
Definition: widget.hpp:753
void set_retval(const int retval, const bool close_window=true)
Sets there return value of the window.
Definition: window.hpp:394
void invalidate_layout()
Updates the size of the window.
Definition: window.cpp:761
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:398
const unit_type * find(const std::string &key, unit_type::BUILD_STATUS status=unit_type::FULL) const
Finds a unit_type by its id() and makes sure it is built to the specified level.
Definition: types.cpp:1265
const race_map & races() const
Definition: types.hpp:397
A single unit type that the player may recruit.
Definition: types.hpp:43
Declarations for File-IO.
std::size_t i
Definition: function.cpp:1029
static std::string _(const char *str)
Definition: gettext.hpp:93
bool to_asset_path(std::string &path, const std::string &addon_id, const std::string &asset_type)
Helper function to convert absolute path to wesnoth relative path.
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)
Game configuration data as global variables.
Definition: build_info.cpp:61
std::string path
Definition: filesystem.cpp:91
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:203
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:177
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:777
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:161
void read(config &cfg, std::istream &in, abstract_validator *validator)
Definition: parser.cpp:629
void write_key_val(std::ostream &out, const std::string &key, const config::attribute_value &value, unsigned level, std::string &textdomain)
Definition: parser.cpp:696
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:1500
#define e