The Battle for Wesnoth  1.17.23+dev
campaign_selection.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2009 - 2023
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"
25 #include "gui/widgets/image.hpp"
26 #include "gui/widgets/listbox.hpp"
31 #include "gui/widgets/settings.hpp"
32 #include "gui/widgets/text_box.hpp"
36 #include "gui/widgets/window.hpp"
37 #include "lexical_cast.hpp"
38 #include "preferences/game.hpp"
39 
40 #include <functional>
41 #include "utils/irdya_datetime.hpp"
42 
43 namespace gui2::dialogs
44 {
45 
46 REGISTER_DIALOG(campaign_selection)
47 
48 void campaign_selection::campaign_selected()
49 {
50  tree_view& tree = find_widget<tree_view>(this, "campaign_tree", false);
51  if(tree.empty()) {
52  return;
53  }
54 
55  assert(tree.selected_item());
56 
57  if(!tree.selected_item()->id().empty()) {
58  auto iter = std::find(page_ids_.begin(), page_ids_.end(), tree.selected_item()->id());
59 
60  if(tree.selected_item()->id() == missing_campaign_) {
61  find_widget<button>(this, "ok", false).set_active(false);
62  } else {
63  find_widget<button>(this, "ok", false).set_active(true);
64  }
65 
66  const int choice = std::distance(page_ids_.begin(), iter);
67  if(iter == page_ids_.end()) {
68  return;
69  }
70 
71  multi_page& pages = find_widget<multi_page>(this, "campaign_details", false);
72  pages.select_page(choice);
73 
74  engine_.set_current_level(choice);
75 
76  styled_widget& background = find_widget<styled_widget>(this, "campaign_background", false);
77  background.set_label(engine_.current_level().data()["background"].str());
78 
79  // Rebuild difficulty menu
80  difficulties_.clear();
81 
82  auto& diff_menu = find_widget<menu_button>(this, "difficulty_menu", false);
83 
84  const auto& diff_config = generate_difficulty_config(engine_.current_level().data());
85  diff_menu.set_active(diff_config.child_count("difficulty") > 1);
86 
87  if(!diff_config.empty()) {
88  std::vector<config> entry_list;
89  unsigned n = 0, selection = 0, max_n = diff_config.child_count("difficulty");
90 
91  for(const auto& cfg : diff_config.child_range("difficulty")) {
92  config entry;
93 
94  // FIXME: description may have markup that will display weird on the menu_button proper
95  entry["label"] = cfg["label"].str() + " (" + cfg["description"].str() + ")";
96  entry["image"] = cfg["image"].str("misc/blank-hex.png");
97 
98  if(preferences::is_campaign_completed(tree.selected_item()->id(), cfg["define"])) {
99  std::string laurel;
100 
101  if(n + 1 >= max_n) {
103  } else if(n == 0) {
105  } else {
107  }
108 
109  entry["image"] = laurel + "~BLIT(" + entry["image"] + ")";
110  }
111 
112  if(!cfg["description"].empty()) {
113  std::string desc;
114  if(cfg["auto_markup"].to_bool(true) == false) {
115  desc = cfg["description"].str();
116  } else {
117  //desc = "<small>";
118  if(!cfg["old_markup"].to_bool()) {
119  desc += font::span_color(font::GRAY_COLOR) + "(" + cfg["description"].str() + ")</span>";
120  } else {
121  desc += font::span_color(font::GRAY_COLOR) + cfg["description"].str() + "</span>";
122  }
123  //desc += "</small>";
124  }
125 
126  // Icons get displayed instead of the labels on the dropdown menu itself,
127  // so we want to prepend each label to its description here
128  desc = cfg["label"].str() + "\n" + desc;
129 
130  entry["details"] = std::move(desc);
131  }
132 
133  entry_list.emplace_back(std::move(entry));
134  difficulties_.emplace_back(cfg["define"].str());
135 
136  if(cfg["default"].to_bool(false)) {
137  selection = n;
138  }
139 
140  ++n;
141  }
142 
143  diff_menu.set_values(entry_list);
144  diff_menu.set_selected(selection);
145  }
146  }
147 }
148 
150 {
151  const std::size_t selection = find_widget<menu_button>(this, "difficulty_menu", false).get_value();
152  current_difficulty_ = difficulties_.at(std::min(difficulties_.size() - 1, selection));
153 }
154 
156 {
157  using level_ptr = ng::create_engine::level_ptr;
158 
159  auto levels = engine_.get_levels_by_type_unfiltered(level_type::type::sp_campaign);
160 
161  switch(order) {
162  case RANK: // Already sorted by rank
163  // This'll actually never happen, but who knows if that'll ever change...
164  if(!ascending) {
165  std::reverse(levels.begin(), levels.end());
166  }
167 
168  break;
169 
170  case DATE:
171  std::sort(levels.begin(), levels.end(), [ascending](const level_ptr& a, const level_ptr& b) {
172  auto cpn_a = std::dynamic_pointer_cast<ng::campaign>(a);
173  auto cpn_b = std::dynamic_pointer_cast<ng::campaign>(b);
174 
175  if(cpn_b == nullptr) {
176  return cpn_a != nullptr;
177  }
178 
179  if(cpn_a == nullptr) {
180  return false;
181  }
182 
183  return ascending
184  ? cpn_a->dates().first < cpn_b->dates().first
185  : cpn_a->dates().first > cpn_b->dates().first;
186  });
187 
188  break;
189 
190  case NAME:
191  std::sort(levels.begin(), levels.end(), [ascending](const level_ptr& a, const level_ptr& b) {
192  const int cmp = translation::icompare(a->name(), b->name());
193  return ascending ? cmp < 0 : cmp > 0;
194  });
195 
196  break;
197  }
198 
199  tree_view& tree = find_widget<tree_view>(this, "campaign_tree", false);
200 
201  // Remember which campaign was selected...
202  std::string was_selected;
203  if(!tree.empty()) {
204  was_selected = tree.selected_item()->id();
205  tree.clear();
206  }
207 
208  boost::dynamic_bitset<> show_items;
209  show_items.resize(levels.size(), true);
210 
211  if(!last_search_words_.empty()) {
212  for(unsigned i = 0; i < levels.size(); ++i) {
213  bool found = false;
214  for(const auto& word : last_search_words_) {
215  found = translation::ci_search(levels[i]->name(), word) ||
216  translation::ci_search(levels[i]->data()["name"].t_str().base_str(), word) ||
217  translation::ci_search(levels[i]->description(), word) ||
218  translation::ci_search(levels[i]->data()["description"].t_str().base_str(), word) ||
219  translation::ci_search(levels[i]->data()["abbrev"], word) ||
220  translation::ci_search(levels[i]->data()["abbrev"].t_str().base_str(), word);
221 
222  if(!found) {
223  break;
224  }
225  }
226 
227  show_items[i] = found;
228  }
229  }
230 
231  bool exists_in_filtered_result = false;
232  for(unsigned i = 0; i < levels.size(); ++i) {
233  if(show_items[i]) {
234  add_campaign_to_tree(levels[i]->data());
235 
236  if (!exists_in_filtered_result) {
237  exists_in_filtered_result = levels[i]->id() == was_selected;
238  }
239  }
240  }
241 
242  if(!was_selected.empty() && exists_in_filtered_result) {
243  find_widget<tree_view_node>(this, was_selected, false).select_node();
244  } else {
245  campaign_selected();
246  }
247 }
248 
250 {
251  static bool force = false;
252  if(force) {
253  return;
254  }
255 
256  if(current_sorting_ == order) {
258  currently_sorted_asc_ = false;
259  } else {
260  currently_sorted_asc_ = true;
262  }
263  } else if(current_sorting_ == RANK) {
264  currently_sorted_asc_ = true;
265  current_sorting_ = order;
266  } else {
267  currently_sorted_asc_ = true;
268  current_sorting_ = order;
269 
270  force = true;
271 
272  if(order == NAME) {
273  find_widget<toggle_button>(this, "sort_time", false).set_value(0);
274  } else if(order == DATE) {
275  find_widget<toggle_button>(this, "sort_name", false).set_value(0);
276  }
277 
278  force = false;
279  }
280 
282 }
283 
284 void campaign_selection::filter_text_changed(const std::string& text)
285 {
286  const std::vector<std::string> words = utils::split(text, ' ');
287 
288  if(words == last_search_words_) {
289  return;
290  }
291 
292  last_search_words_ = words;
294 }
295 
297 {
298  text_box* filter = find_widget<text_box>(&window, "filter_box", false, true);
300  std::bind(&campaign_selection::filter_text_changed, this, std::placeholders::_2));
301 
302  /***** Setup campaign tree. *****/
303  tree_view& tree = find_widget<tree_view>(&window, "campaign_tree", false);
304 
306  std::bind(&campaign_selection::campaign_selected, this));
307 
308  toggle_button& sort_name = find_widget<toggle_button>(&window, "sort_name", false);
309  toggle_button& sort_time = find_widget<toggle_button>(&window, "sort_time", false);
310 
313 
316 
317  window.keyboard_capture(filter);
319 
320  /***** Setup campaign details. *****/
321  multi_page& pages = find_widget<multi_page>(&window, "campaign_details", false);
322 
323  for(const auto& level : engine_.get_levels_by_type_unfiltered(level_type::type::sp_campaign)) {
324  const config& campaign = level->data();
325 
326  /*** Add tree item ***/
327  add_campaign_to_tree(campaign);
328 
329  /*** Add detail item ***/
332 
333  item["label"] = campaign["description"];
334  item["use_markup"] = "true";
335 
336  if(!campaign["description_alignment"].empty()) {
337  item["text_alignment"] = campaign["description_alignment"];
338  }
339 
340  data.emplace("description", item);
341 
342  item["label"] = campaign["image"];
343  data.emplace("image", item);
344 
345  pages.add_page(data);
346  page_ids_.push_back(campaign["id"]);
347  }
348 
349  std::vector<std::string> dirs;
350  filesystem::get_files_in_dir(game_config::path + "/data/campaigns", nullptr, &dirs);
351  if(dirs.size() <= 15) {
352  config missing;
353  missing["icon"] = "units/unknown-unit.png";
354  missing["name"] = _("Missing Campaigns");
355  missing["completed"] = false;
356  missing["id"] = missing_campaign_;
357 
359 
362 
363  // TRANSLATORS: "more than 15" gives a little leeway to add or remove one without changing the translatable text.
364  // It's already ambiguous, 1.18 has 19 campaigns, if you include the tutorial and multiplayer-only World Conquest.
365  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.");
366  data.emplace("description", item);
367 
368  pages.add_page(data);
369  page_ids_.push_back(missing_campaign_);
370  }
371 
372  //
373  // Set up Mods selection dropdown
374  //
375  multimenu_button& mods_menu = find_widget<multimenu_button>(&window, "mods_menu", false);
376 
378  std::vector<config> mod_menu_values;
379  std::vector<std::string> enabled = engine_.active_mods();
380 
382  const bool active = std::find(enabled.begin(), enabled.end(), mod->id) != enabled.end();
383 
384  mod_menu_values.emplace_back("label", mod->name, "checkbox", active);
385 
386  mod_states_.push_back(active);
387  }
388 
389  mods_menu.set_values(mod_menu_values);
390  mods_menu.select_options(mod_states_);
391 
393  } else {
394  mods_menu.set_active(false);
395  mods_menu.set_label(_("active_modifications^None"));
396  }
397 
398  //
399  // Set up Difficulty dropdown
400  //
401  menu_button& diff_menu = find_widget<menu_button>(this, "difficulty_menu", false);
402 
403  diff_menu.set_use_markup(true);
405 
407 }
408 
410 {
411  tree_view& tree = find_widget<tree_view>(this, "campaign_tree", false);
414 
415  item["label"] = campaign["icon"];
416  data.emplace("icon", item);
417 
418  item["label"] = campaign["name"];
419  data.emplace("name", item);
420 
421  // We completed the campaign! Calculate the appropriate victory laurel.
422  if(campaign["completed"].to_bool()) {
423  config::const_child_itors difficulties = campaign.child_range("difficulty");
424 
425  auto did_complete_at = [](const config& c) { return c["completed_at"].to_bool(); };
426 
427  // Check for non-completion on every difficulty save the first.
428  const bool only_first_completed = difficulties.size() > 1 &&
429  std::none_of(difficulties.begin() + 1, difficulties.end(), did_complete_at);
430 
431  /*
432  * Criteria:
433  *
434  * - Use the gold laurel (hardest) for campaigns with only one difficulty OR
435  * if out of two or more difficulties, the last one has been completed.
436  *
437  * - Use the bronze laurel (easiest) only if the first difficulty out of two
438  * or more has been completed.
439  *
440  * - Use the silver laurel otherwise.
441  */
442  if(!difficulties.empty() && did_complete_at(difficulties.back())) {
444  } else if(only_first_completed && did_complete_at(difficulties.front())) {
446  } else {
448  }
449 
450  data.emplace("victory", item);
451  }
452 
453  tree.add_node("campaign", data).set_id(campaign["id"]);
454 }
455 
457 {
458  tree_view& tree = find_widget<tree_view>(&window, "campaign_tree", false);
459 
460  if(tree.empty()) {
461  return;
462  }
463 
464  assert(tree.selected_item());
465  if(!tree.selected_item()->id().empty()) {
466  auto iter = std::find(page_ids_.begin(), page_ids_.end(), tree.selected_item()->id());
467  if(iter != page_ids_.end()) {
468  choice_ = std::distance(page_ids_.begin(), iter);
469  }
470  }
471 
472 
473  rng_mode_ = RNG_MODE(std::clamp<unsigned>(find_widget<menu_button>(&window, "rng_menu", false).get_value(), RNG_DEFAULT, RNG_BIASED));
474 
476 }
477 
479 {
480  boost::dynamic_bitset<> new_mod_states =
481  find_widget<multimenu_button>(this, "mods_menu", false).get_toggle_states();
482 
483  // Get a mask of any mods that were toggled, regardless of new state
484  mod_states_ = mod_states_ ^ new_mod_states;
485 
486  for(unsigned i = 0; i < mod_states_.size(); i++) {
487  if(mod_states_[i]) {
489  }
490  }
491 
492  // Save the full toggle states for next time
493  mod_states_ = new_mod_states;
494 }
495 
496 } // namespace dialogs
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:161
child_itors child_range(config_key_type key)
Definition: config.cpp:277
boost::iterator_range< const_child_iterator > const_child_itors
Definition: config.hpp:285
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 > 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:62
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:44
void select_page(const unsigned page, const bool select=true)
Selects a page.
Definition: multi_page.cpp:105
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 &label)
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:61
bool empty() const
Definition: tree_view.cpp:100
tree_view_node & add_node(const std::string &id, const widget_data &data, const int index=-1)
Definition: tree_view.cpp:57
tree_view_node * selected_item()
Definition: tree_view.hpp:110
void set_id(const std::string &id)
Definition: widget.cpp:99
const std::string & id() const
Definition: widget.cpp:111
base class of top level items, the only item which needs to store the final canvases to draw on.
Definition: window.hpp:67
void keyboard_capture(widget *widget)
Definition: window.cpp:1224
void add_to_keyboard_chain(widget *widget)
Adds the widget to the keyboard chain.
Definition: window.cpp:1230
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
bool toggle_mod(int index, bool force=false)
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...
New lexcical_cast header.
#define REGISTER_DIALOG(window_id)
Wrapper for REGISTER_DIALOG2.
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:407
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:86
config generate_difficulty_config(const config &source)
Helper function to convert old difficulty markup.
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification &signal)
Connects a signal handler for getting a notification upon modification.
Definition: dispatcher.cpp:205
std::map< std::string, widget_item > widget_data
Definition: widget.hpp:35
std::map< std::string, t_string > widget_item
Definition: widget.hpp:32
std::pair< std::string, unsigned > item
Definition: help_impl.hpp:414
void set_modifications(const std::vector< std::string > &value, bool mp)
Definition: game.cpp:720
bool is_campaign_completed(const std::string &campaign_id)
Definition: game.cpp:293
bool ci_search(const std::string &s1, const std::string &s2)
Definition: gettext.cpp:567
std::vector< std::string > split(const config_attribute_value &val)
std::string_view data
Definition: picture.cpp:199
This file contains the settings handling of the widget library.
mock_char c
static map_location::DIRECTION n
#define a
#define b