The Battle for Wesnoth  1.19.0-dev
campaign_selection.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2009 - 2024
3  by Mark de Wever <koraq@xs4all.nl>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 #define GETTEXT_DOMAIN "wesnoth-lib"
17 
19 
20 #include "filesystem.hpp"
21 #include "font/text_formatting.hpp"
24 #include "gui/widgets/button.hpp"
28 #include "gui/widgets/text_box.hpp"
32 #include "gui/widgets/window.hpp"
33 #include "preferences/game.hpp"
34 
35 #include <functional>
36 #include "utils/irdya_datetime.hpp"
37 
38 namespace gui2::dialogs
39 {
40 
41 REGISTER_DIALOG(campaign_selection)
42 
43 void campaign_selection::campaign_selected()
44 {
45  tree_view& tree = find_widget<tree_view>(this, "campaign_tree", false);
46  if(tree.empty()) {
47  return;
48  }
49 
50  assert(tree.selected_item());
51 
52  if(!tree.selected_item()->id().empty()) {
53  auto iter = std::find(page_ids_.begin(), page_ids_.end(), tree.selected_item()->id());
54 
55  if(tree.selected_item()->id() == missing_campaign_) {
56  find_widget<button>(this, "ok", false).set_active(false);
57  } else {
58  find_widget<button>(this, "ok", false).set_active(true);
59  }
60 
61  const int choice = std::distance(page_ids_.begin(), iter);
62  if(iter == page_ids_.end()) {
63  return;
64  }
65 
66  multi_page& pages = find_widget<multi_page>(this, "campaign_details", false);
67  pages.select_page(choice);
68 
69  engine_.set_current_level(choice);
70 
71  styled_widget& background = find_widget<styled_widget>(this, "campaign_background", false);
72  background.set_label(engine_.current_level().data()["background"].str());
73 
74  // Rebuild difficulty menu
75  difficulties_.clear();
76 
77  auto& diff_menu = find_widget<menu_button>(this, "difficulty_menu", false);
78 
79  const auto& diff_config = generate_difficulty_config(engine_.current_level().data());
80  diff_menu.set_active(diff_config.child_count("difficulty") > 1);
81 
82  if(!diff_config.empty()) {
83  std::vector<config> entry_list;
84  unsigned n = 0, selection = 0, max_n = diff_config.child_count("difficulty");
85 
86  for(const auto& cfg : diff_config.child_range("difficulty")) {
87  config entry;
88 
89  // FIXME: description may have markup that will display weird on the menu_button proper
90  entry["label"] = cfg["label"].str() + " (" + cfg["description"].str() + ")";
91  entry["image"] = cfg["image"].str("misc/blank-hex.png");
92 
93  if(preferences::is_campaign_completed(tree.selected_item()->id(), cfg["define"])) {
94  std::string laurel;
95 
96  if(n + 1 >= max_n) {
98  } else if(n == 0) {
100  } else {
102  }
103 
104  entry["image"] = laurel + "~BLIT(" + entry["image"] + ")";
105  }
106 
107  if(!cfg["description"].empty()) {
108  std::string desc;
109  if(cfg["auto_markup"].to_bool(true) == false) {
110  desc = cfg["description"].str();
111  } else {
112  //desc = "<small>";
113  if(!cfg["old_markup"].to_bool()) {
114  desc += font::span_color(font::GRAY_COLOR) + "(" + cfg["description"].str() + ")</span>";
115  } else {
116  desc += font::span_color(font::GRAY_COLOR) + cfg["description"].str() + "</span>";
117  }
118  //desc += "</small>";
119  }
120 
121  // Icons get displayed instead of the labels on the dropdown menu itself,
122  // so we want to prepend each label to its description here
123  desc = cfg["label"].str() + "\n" + desc;
124 
125  entry["details"] = std::move(desc);
126  }
127 
128  entry_list.emplace_back(std::move(entry));
129  difficulties_.emplace_back(cfg["define"].str());
130 
131  if(cfg["default"].to_bool(false)) {
132  selection = n;
133  }
134 
135  ++n;
136  }
137 
138  diff_menu.set_values(entry_list);
139  diff_menu.set_selected(selection);
140  }
141  }
142 }
143 
145 {
146  const std::size_t selection = find_widget<menu_button>(this, "difficulty_menu", false).get_value();
147  current_difficulty_ = difficulties_.at(std::min(difficulties_.size() - 1, selection));
148 }
149 
151 {
152  using level_ptr = ng::create_engine::level_ptr;
153 
154  auto levels = engine_.get_levels_by_type_unfiltered(level_type::type::sp_campaign);
155 
156  switch(order) {
157  case RANK: // Already sorted by rank
158  // This'll actually never happen, but who knows if that'll ever change...
159  if(!ascending) {
160  std::reverse(levels.begin(), levels.end());
161  }
162 
163  break;
164 
165  case DATE:
166  std::sort(levels.begin(), levels.end(), [ascending](const level_ptr& a, const level_ptr& b) {
167  auto cpn_a = std::dynamic_pointer_cast<ng::campaign>(a);
168  auto cpn_b = std::dynamic_pointer_cast<ng::campaign>(b);
169 
170  if(cpn_b == nullptr) {
171  return cpn_a != nullptr;
172  }
173 
174  if(cpn_a == nullptr) {
175  return false;
176  }
177 
178  return ascending
179  ? cpn_a->dates().first < cpn_b->dates().first
180  : cpn_a->dates().first > cpn_b->dates().first;
181  });
182 
183  break;
184 
185  case NAME:
186  std::sort(levels.begin(), levels.end(), [ascending](const level_ptr& a, const level_ptr& b) {
187  const int cmp = translation::icompare(a->name(), b->name());
188  return ascending ? cmp < 0 : cmp > 0;
189  });
190 
191  break;
192  }
193 
194  tree_view& tree = find_widget<tree_view>(this, "campaign_tree", false);
195 
196  // Remember which campaign was selected...
197  std::string was_selected;
198  if(!tree.empty()) {
199  was_selected = tree.selected_item()->id();
200  tree.clear();
201  }
202 
203  boost::dynamic_bitset<> show_items;
204  show_items.resize(levels.size(), true);
205 
206  if(!last_search_words_.empty()) {
207  for(unsigned i = 0; i < levels.size(); ++i) {
208  bool found = false;
209  for(const auto& word : last_search_words_) {
210  found = translation::ci_search(levels[i]->name(), word) ||
211  translation::ci_search(levels[i]->data()["name"].t_str().base_str(), word) ||
212  translation::ci_search(levels[i]->description(), word) ||
213  translation::ci_search(levels[i]->data()["description"].t_str().base_str(), word) ||
214  translation::ci_search(levels[i]->data()["abbrev"], word) ||
215  translation::ci_search(levels[i]->data()["abbrev"].t_str().base_str(), word);
216 
217  if(!found) {
218  break;
219  }
220  }
221 
222  show_items[i] = found;
223  }
224  }
225 
226  // List of which options has been selected in the completion filter multimenu_button
227  boost::dynamic_bitset<> filter_comp_options = find_widget<multimenu_button>(this, "filter_completion", false).get_toggle_states();
228 
229  bool exists_in_filtered_result = false;
230  for(unsigned i = 0; i < levels.size(); ++i) {
231  bool completed = preferences::is_campaign_completed(levels[i]->data()["id"]);
232  config::const_child_itors difficulties = levels[i]->data().child_range("difficulty");
233  auto did_complete_at = [](const config& c) { return c["completed_at"].to_bool(); };
234 
235  // Check for non-completion on every difficulty save the first.
236  const bool only_first_completed = difficulties.size() > 1 &&
237  std::none_of(difficulties.begin() + 1, difficulties.end(), did_complete_at);
238  const bool completed_easy = only_first_completed && did_complete_at(difficulties.front());
239  const bool completed_hardest = !difficulties.empty() && did_complete_at(difficulties.back());
240  const bool completed_mid = completed && !completed_hardest && !completed_easy;
241 
242  if( show_items[i] && (
243  ( (!completed) && filter_comp_options[0] ) // Selects all campaigns not finished by player
244  || ( completed && filter_comp_options[4] ) // Selects all campaigns finished by player
245  || ( completed_hardest && filter_comp_options[3] ) // Selects campaigns completed in hardest difficulty
246  || ( completed_easy && filter_comp_options[1] ) // Selects campaigns completed in easiest difficulty
247  || ( completed_mid && filter_comp_options[2]) // Selects campaigns completed in any other difficulty
248  )) {
249  add_campaign_to_tree(levels[i]->data());
250  if (!exists_in_filtered_result) {
251  exists_in_filtered_result = levels[i]->id() == was_selected;
252  }
253  }
254  }
255 
256  if(!was_selected.empty() && exists_in_filtered_result) {
257  find_widget<tree_view_node>(this, was_selected, false).select_node();
258  } else {
259  campaign_selected();
260  }
261 }
262 
264 {
265  static bool force = false;
266  if(force) {
267  return;
268  }
269 
270  if(current_sorting_ == order) {
272  currently_sorted_asc_ = false;
273  } else {
274  currently_sorted_asc_ = true;
276  }
277  } else if(current_sorting_ == RANK) {
278  currently_sorted_asc_ = true;
279  current_sorting_ = order;
280  } else {
281  currently_sorted_asc_ = true;
282  current_sorting_ = order;
283 
284  force = true;
285 
286  if(order == NAME) {
287  find_widget<toggle_button>(this, "sort_time", false).set_value(0);
288  } else if(order == DATE) {
289  find_widget<toggle_button>(this, "sort_name", false).set_value(0);
290  }
291 
292  force = false;
293  }
294 
296 }
297 
298 void campaign_selection::filter_text_changed(const std::string& text)
299 {
300  const std::vector<std::string> words = utils::split(text, ' ');
301 
302  if(words == last_search_words_) {
303  return;
304  }
305 
306  last_search_words_ = words;
308 }
309 
311 {
312  text_box* filter = find_widget<text_box>(&window, "filter_box", false, true);
314  std::bind(&campaign_selection::filter_text_changed, this, std::placeholders::_2));
315 
316  /***** Setup campaign tree. *****/
317  tree_view& tree = find_widget<tree_view>(&window, "campaign_tree", false);
318 
320  std::bind(&campaign_selection::campaign_selected, this));
321 
322  toggle_button& sort_name = find_widget<toggle_button>(&window, "sort_name", false);
323  toggle_button& sort_time = find_widget<toggle_button>(&window, "sort_time", false);
324 
327 
330 
331  window.keyboard_capture(filter);
333 
334  /***** Setup campaign details. *****/
335  multi_page& pages = find_widget<multi_page>(&window, "campaign_details", false);
336 
337  multimenu_button& filter_comp = find_widget<multimenu_button>(&window, "filter_completion", false);
338  connect_signal_notify_modified(filter_comp,
339  std::bind(&campaign_selection::sort_campaigns, this, RANK, 1));
340  for (unsigned j = 0; j < filter_comp.num_options(); j++) {
341  filter_comp.select_option(j);
342  }
343 
344  for(const auto& level : engine_.get_levels_by_type_unfiltered(level_type::type::sp_campaign)) {
345  const config& campaign = level->data();
346 
347  /*** Add tree item ***/
348  add_campaign_to_tree(campaign);
349 
350  /*** Add detail item ***/
353 
354  item["label"] = campaign["description"];
355  item["use_markup"] = "true";
356 
357  if(!campaign["description_alignment"].empty()) {
358  item["text_alignment"] = campaign["description_alignment"];
359  }
360 
361  data.emplace("description", item);
362 
363  item["label"] = campaign["image"];
364  data.emplace("image", item);
365 
366  pages.add_page(data);
367  page_ids_.push_back(campaign["id"]);
368  }
369 
370  std::vector<std::string> dirs;
371  filesystem::get_files_in_dir(game_config::path + "/data/campaigns", nullptr, &dirs);
372  if(dirs.size() <= 15) {
373  config missing;
374  missing["icon"] = "units/unknown-unit.png";
375  missing["name"] = _("Missing Campaigns");
376  missing["completed"] = false;
377  missing["id"] = missing_campaign_;
378 
380 
383 
384  // TRANSLATORS: "more than 15" gives a little leeway to add or remove one without changing the translatable text.
385  // It's already ambiguous, 1.18 has 19 campaigns, if you include the tutorial and multiplayer-only World Conquest.
386  item["label"] = _("Wesnoth normally includes more than 15 mainline campaigns, even before installing any from the add-ons server. If you’ve installed the game via a package manager, there’s probably a separate package to install the complete game data.");
387  data.emplace("description", item);
388 
389  pages.add_page(data);
390  page_ids_.push_back(missing_campaign_);
391  }
392 
393  //
394  // Set up Mods selection dropdown
395  //
396  multimenu_button& mods_menu = find_widget<multimenu_button>(&window, "mods_menu", false);
397 
399  std::vector<config> mod_menu_values;
400  std::vector<std::string> enabled = engine_.active_mods();
401 
403  const bool active = std::find(enabled.begin(), enabled.end(), mod->id) != enabled.end();
404 
405  mod_menu_values.emplace_back("label", mod->name, "checkbox", active);
406 
407  mod_states_.push_back(active);
408  mod_ids_.emplace_back(mod->id);
409  }
410 
411  mods_menu.set_values(mod_menu_values);
412  mods_menu.select_options(mod_states_);
413 
415  } else {
416  mods_menu.set_active(false);
417  mods_menu.set_label(_("active_modifications^None"));
418  }
419 
420  //
421  // Set up Difficulty dropdown
422  //
423  menu_button& diff_menu = find_widget<menu_button>(this, "difficulty_menu", false);
424 
425  diff_menu.set_use_markup(true);
427 
429 }
430 
432 {
433  tree_view& tree = find_widget<tree_view>(this, "campaign_tree", false);
436 
437  item["label"] = campaign["icon"];
438  data.emplace("icon", item);
439 
440  item["label"] = campaign["name"];
441  data.emplace("name", item);
442 
443  // We completed the campaign! Calculate the appropriate victory laurel.
444  if(campaign["completed"].to_bool()) {
445  config::const_child_itors difficulties = campaign.child_range("difficulty");
446 
447  auto did_complete_at = [](const config& c) { return c["completed_at"].to_bool(); };
448 
449  // Check for non-completion on every difficulty save the first.
450  const bool only_first_completed = difficulties.size() > 1 &&
451  std::none_of(difficulties.begin() + 1, difficulties.end(), did_complete_at);
452 
453  /*
454  * Criteria:
455  *
456  * - Use the gold laurel (hardest) for campaigns with only one difficulty OR
457  * if out of two or more difficulties, the last one has been completed.
458  *
459  * - Use the bronze laurel (easiest) only if the first difficulty out of two
460  * or more has been completed.
461  *
462  * - Use the silver laurel otherwise.
463  */
464  if(!difficulties.empty() && did_complete_at(difficulties.back())) {
466  } else if(only_first_completed && did_complete_at(difficulties.front())) {
468  } else {
470  }
471 
472  data.emplace("victory", item);
473  }
474 
475  tree.add_node("campaign", data).set_id(campaign["id"]);
476 }
477 
479 {
480  tree_view& tree = find_widget<tree_view>(&window, "campaign_tree", false);
481 
482  if(tree.empty()) {
483  return;
484  }
485 
486  assert(tree.selected_item());
487  if(!tree.selected_item()->id().empty()) {
488  auto iter = std::find(page_ids_.begin(), page_ids_.end(), tree.selected_item()->id());
489  if(iter != page_ids_.end()) {
490  choice_ = std::distance(page_ids_.begin(), iter);
491  }
492  }
493 
494 
495  rng_mode_ = RNG_MODE(std::clamp<unsigned>(find_widget<menu_button>(&window, "rng_menu", false).get_value(), RNG_DEFAULT, RNG_BIASED));
496 
498 }
499 
501 {
502  boost::dynamic_bitset<> new_mod_states =
503  find_widget<multimenu_button>(this, "mods_menu", false).get_toggle_states();
504 
505  // Get a mask of any mods that were toggled, regardless of new state
506  mod_states_ = mod_states_ ^ new_mod_states;
507 
508  for(unsigned i = 0; i < mod_states_.size(); i++) {
509  if(mod_states_[i]) {
511  }
512  }
513 
514  // Save the full toggle states for next time
515  mod_states_ = new_mod_states;
516 }
517 
518 } // namespace dialogs
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:159
child_itors child_range(config_key_type key)
Definition: config.cpp:273
boost::iterator_range< const_child_iterator > const_child_itors
Definition: config.hpp:283
This shows the dialog which allows the user to choose which campaign to play.
void toggle_sorting_selection(CAMPAIGN_ORDER order)
RNG_MODE rng_mode_
whether the player checked the "Deterministic" checkbox.
virtual void pre_show(window &window) override
Actions to be taken before showing the window.
void difficulty_selected()
Called when the difficulty selection changes.
std::vector< std::string > mod_ids_
std::vector< std::string > difficulties_
RNG_MODE
RNG mode selection values.
static const std::string missing_campaign_
void sort_campaigns(CAMPAIGN_ORDER order, bool ascending)
void add_campaign_to_tree(const config &campaign)
void filter_text_changed(const std::string &text)
void campaign_selected()
Called when another campaign is selected.
virtual void post_show(window &window) override
Actions to be taken after the window has been shown.
std::vector< std::string > last_search_words_
std::vector< std::string > page_ids_
A menu_button is a styled_widget to choose an element from a list of elements.
Definition: menu_button.hpp:59
A multi page is a control that contains several 'pages' of which only one is visible.
Definition: multi_page.hpp:50
grid & add_page(const widget_item &item)
Adds single page to the grid.
Definition: multi_page.cpp:43
void select_page(const unsigned page, const bool select=true)
Selects a page.
Definition: multi_page.cpp:104
A multimenu_button is a styled_widget to choose an element from a list of elements.
void select_options(boost::dynamic_bitset<> states)
Set the options selected in the menu.
void set_values(const std::vector<::config > &values)
Set the available menu options.
unsigned num_options()
Get the number of options available in the menu.
virtual void set_active(const bool active) override
See styled_widget::set_active.
void select_option(const unsigned option, const bool selected=true)
Select an option in the menu.
Base class for all visible items.
virtual void set_label(const t_string &text)
virtual void set_use_markup(bool use_markup)
void set_text_changed_callback(std::function< void(text_box_base *textbox, const std::string text)> cb)
Set the text_changed callback.
Class for a single line text area.
Definition: text_box.hpp:142
Class for a toggle button.
A tree view is a control that holds several items of the same or different types.
Definition: tree_view.hpp:60
bool empty() const
Definition: tree_view.cpp:99
tree_view_node & add_node(const std::string &id, const widget_data &data, const int index=-1)
Definition: tree_view.cpp:56
tree_view_node * selected_item()
Definition: tree_view.hpp:109
void set_id(const std::string &id)
Definition: widget.cpp:98
const std::string & id() const
Definition: widget.cpp:110
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition: window.hpp:63
void keyboard_capture(widget *widget)
Definition: window.cpp:1221
void add_to_keyboard_chain(widget *widget)
Adds the widget to the keyboard chain.
Definition: window.cpp:1227
bool toggle_mod(const std::string &id, bool force=false)
std::vector< std::string > & active_mods()
const std::vector< extras_metadata_ptr > & get_const_extras_by_type(const MP_EXTRA extra_type) const
std::shared_ptr< level > level_ptr
std::vector< level_ptr > get_levels_by_type_unfiltered(level_type::type type) const
Declarations for File-IO.
std::size_t i
Definition: function.cpp:968
static std::string _(const char *str)
Definition: gettext.hpp:93
This file contains the window object, this object is a top level container which has the event manage...
void get_files_in_dir(const std::string &dir, std::vector< std::string > *files, std::vector< std::string > *dirs, name_mode mode, filter_mode filter, reorder_mode reorder, file_tree_checksum *checksum)
Get a list of all files and/or directories in a given directory.
Definition: filesystem.cpp:405
const color_t GRAY_COLOR
std::string span_color(const color_t &color)
Returns a Pango formatting string using the provided color_t object.
std::string victory_laurel_hardest
std::string victory_laurel
std::string victory_laurel_easy
std::string path
Definition: filesystem.cpp:84
config generate_difficulty_config(const config &source)
Helper function to convert old difficulty markup.
REGISTER_DIALOG(editor_edit_unit)
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
std::map< std::string, widget_item > widget_data
Definition: widget.hpp:34
std::map< std::string, t_string > widget_item
Definition: widget.hpp:31
std::pair< std::string, unsigned > item
Definition: help_impl.hpp:412
void set_modifications(const std::vector< std::string > &value, bool mp)
Definition: game.cpp:717
bool is_campaign_completed(const std::string &campaign_id)
Definition: game.cpp:290
bool ci_search(const std::string &s1, const std::string &s2)
Definition: gettext.cpp:565
std::vector< std::string > split(const config_attribute_value &val)
std::string_view data
Definition: picture.cpp:194
mock_char c
static map_location::DIRECTION n
#define a
#define b