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  bool exists_in_filtered_result = false;
227  for(unsigned i = 0; i < levels.size(); ++i) {
228  if(show_items[i]) {
229  add_campaign_to_tree(levels[i]->data());
230 
231  if (!exists_in_filtered_result) {
232  exists_in_filtered_result = levels[i]->id() == was_selected;
233  }
234  }
235  }
236 
237  if(!was_selected.empty() && exists_in_filtered_result) {
238  find_widget<tree_view_node>(this, was_selected, false).select_node();
239  } else {
240  campaign_selected();
241  }
242 }
243 
245 {
246  static bool force = false;
247  if(force) {
248  return;
249  }
250 
251  if(current_sorting_ == order) {
253  currently_sorted_asc_ = false;
254  } else {
255  currently_sorted_asc_ = true;
257  }
258  } else if(current_sorting_ == RANK) {
259  currently_sorted_asc_ = true;
260  current_sorting_ = order;
261  } else {
262  currently_sorted_asc_ = true;
263  current_sorting_ = order;
264 
265  force = true;
266 
267  if(order == NAME) {
268  find_widget<toggle_button>(this, "sort_time", false).set_value(0);
269  } else if(order == DATE) {
270  find_widget<toggle_button>(this, "sort_name", false).set_value(0);
271  }
272 
273  force = false;
274  }
275 
277 }
278 
279 void campaign_selection::filter_text_changed(const std::string& text)
280 {
281  const std::vector<std::string> words = utils::split(text, ' ');
282 
283  if(words == last_search_words_) {
284  return;
285  }
286 
287  last_search_words_ = words;
289 }
290 
292 {
293  text_box* filter = find_widget<text_box>(&window, "filter_box", false, true);
295  std::bind(&campaign_selection::filter_text_changed, this, std::placeholders::_2));
296 
297  /***** Setup campaign tree. *****/
298  tree_view& tree = find_widget<tree_view>(&window, "campaign_tree", false);
299 
301  std::bind(&campaign_selection::campaign_selected, this));
302 
303  toggle_button& sort_name = find_widget<toggle_button>(&window, "sort_name", false);
304  toggle_button& sort_time = find_widget<toggle_button>(&window, "sort_time", false);
305 
308 
311 
312  window.keyboard_capture(filter);
314 
315  /***** Setup campaign details. *****/
316  multi_page& pages = find_widget<multi_page>(&window, "campaign_details", false);
317 
318  for(const auto& level : engine_.get_levels_by_type_unfiltered(level_type::type::sp_campaign)) {
319  const config& campaign = level->data();
320 
321  /*** Add tree item ***/
322  add_campaign_to_tree(campaign);
323 
324  /*** Add detail item ***/
327 
328  item["label"] = campaign["description"];
329  item["use_markup"] = "true";
330 
331  if(!campaign["description_alignment"].empty()) {
332  item["text_alignment"] = campaign["description_alignment"];
333  }
334 
335  data.emplace("description", item);
336 
337  item["label"] = campaign["image"];
338  data.emplace("image", item);
339 
340  pages.add_page(data);
341  page_ids_.push_back(campaign["id"]);
342  }
343 
344  std::vector<std::string> dirs;
345  filesystem::get_files_in_dir(game_config::path + "/data/campaigns", nullptr, &dirs);
346  if(dirs.size() <= 15) {
347  config missing;
348  missing["icon"] = "units/unknown-unit.png";
349  missing["name"] = _("Missing Campaigns");
350  missing["completed"] = false;
351  missing["id"] = missing_campaign_;
352 
354 
357 
358  // TRANSLATORS: "more than 15" gives a little leeway to add or remove one without changing the translatable text.
359  // It's already ambiguous, 1.18 has 19 campaigns, if you include the tutorial and multiplayer-only World Conquest.
360  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.");
361  data.emplace("description", item);
362 
363  pages.add_page(data);
364  page_ids_.push_back(missing_campaign_);
365  }
366 
367  //
368  // Set up Mods selection dropdown
369  //
370  multimenu_button& mods_menu = find_widget<multimenu_button>(&window, "mods_menu", false);
371 
373  std::vector<config> mod_menu_values;
374  std::vector<std::string> enabled = engine_.active_mods();
375 
377  const bool active = std::find(enabled.begin(), enabled.end(), mod->id) != enabled.end();
378 
379  mod_menu_values.emplace_back("label", mod->name, "checkbox", active);
380 
381  mod_states_.push_back(active);
382  mod_ids_.emplace_back(mod->id);
383  }
384 
385  mods_menu.set_values(mod_menu_values);
386  mods_menu.select_options(mod_states_);
387 
389  } else {
390  mods_menu.set_active(false);
391  mods_menu.set_label(_("active_modifications^None"));
392  }
393 
394  //
395  // Set up Difficulty dropdown
396  //
397  menu_button& diff_menu = find_widget<menu_button>(this, "difficulty_menu", false);
398 
399  diff_menu.set_use_markup(true);
401 
403 }
404 
406 {
407  tree_view& tree = find_widget<tree_view>(this, "campaign_tree", false);
410 
411  item["label"] = campaign["icon"];
412  data.emplace("icon", item);
413 
414  item["label"] = campaign["name"];
415  data.emplace("name", item);
416 
417  // We completed the campaign! Calculate the appropriate victory laurel.
418  if(campaign["completed"].to_bool()) {
419  config::const_child_itors difficulties = campaign.child_range("difficulty");
420 
421  auto did_complete_at = [](const config& c) { return c["completed_at"].to_bool(); };
422 
423  // Check for non-completion on every difficulty save the first.
424  const bool only_first_completed = difficulties.size() > 1 &&
425  std::none_of(difficulties.begin() + 1, difficulties.end(), did_complete_at);
426 
427  /*
428  * Criteria:
429  *
430  * - Use the gold laurel (hardest) for campaigns with only one difficulty OR
431  * if out of two or more difficulties, the last one has been completed.
432  *
433  * - Use the bronze laurel (easiest) only if the first difficulty out of two
434  * or more has been completed.
435  *
436  * - Use the silver laurel otherwise.
437  */
438  if(!difficulties.empty() && did_complete_at(difficulties.back())) {
440  } else if(only_first_completed && did_complete_at(difficulties.front())) {
442  } else {
444  }
445 
446  data.emplace("victory", item);
447  }
448 
449  tree.add_node("campaign", data).set_id(campaign["id"]);
450 }
451 
453 {
454  tree_view& tree = find_widget<tree_view>(&window, "campaign_tree", false);
455 
456  if(tree.empty()) {
457  return;
458  }
459 
460  assert(tree.selected_item());
461  if(!tree.selected_item()->id().empty()) {
462  auto iter = std::find(page_ids_.begin(), page_ids_.end(), tree.selected_item()->id());
463  if(iter != page_ids_.end()) {
464  choice_ = std::distance(page_ids_.begin(), iter);
465  }
466  }
467 
468 
469  rng_mode_ = RNG_MODE(std::clamp<unsigned>(find_widget<menu_button>(&window, "rng_menu", false).get_value(), RNG_DEFAULT, RNG_BIASED));
470 
472 }
473 
475 {
476  boost::dynamic_bitset<> new_mod_states =
477  find_widget<multimenu_button>(this, "mods_menu", false).get_toggle_states();
478 
479  // Get a mask of any mods that were toggled, regardless of new state
480  mod_states_ = mod_states_ ^ new_mod_states;
481 
482  for(unsigned i = 0; i < mod_states_.size(); i++) {
483  if(mod_states_[i]) {
485  }
486  }
487 
488  // Save the full toggle states for next time
489  mod_states_ = new_mod_states;
490 }
491 
492 } // 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.
virtual void set_active(const bool active) override
See styled_widget::set_active.
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:1215
void add_to_keyboard_chain(widget *widget)
Adds the widget to the keyboard chain.
Definition: window.cpp:1221
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:404
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:83
config generate_difficulty_config(const config &source)
Helper function to convert old difficulty markup.
REGISTER_DIALOG(tod_new_schedule)
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