The Battle for Wesnoth  1.19.7+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"
22 #include "gui/widgets/button.hpp"
26 #include "gui/widgets/text_box.hpp"
31 #include "gui/widgets/window.hpp"
33 #include "serialization/markup.hpp"
34 #include "utils/irdya_datetime.hpp"
35 
36 #include <functional>
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>("campaign_tree");
46  if(tree.empty()) {
47  return;
48  }
49 
50  assert(tree.selected_item());
51 
52  const std::string& campaign_id = tree.selected_item()->id();
53 
54  if(!campaign_id.empty()) {
55  auto iter = std::find(page_ids_.begin(), page_ids_.end(), campaign_id);
56 
57  button& ok_button = find_widget<button>("proceed");
58  ok_button.set_active(campaign_id != missing_campaign_);
59  ok_button.set_label((campaign_id == addons_) ? _("game^Get Add-ons") : _("game^Play"));
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>("campaign_details");
67  pages.select_page(choice);
68 
69  engine_.set_current_level(choice);
70 
71  styled_widget& background = find_widget<styled_widget>("campaign_background");
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>("difficulty_menu");
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(prefs::get().is_campaign_completed(campaign_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"].str() + ")";
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  if(!cfg["old_markup"].to_bool()) {
113  desc += markup::span_color(font::GRAY_COLOR, "(", cfg["description"].str(), ")");
114  } else {
115  desc += markup::span_color(font::GRAY_COLOR, cfg["description"].str());
116  }
117  }
118 
119  // Icons get displayed instead of the labels on the dropdown menu itself,
120  // so we want to prepend each label to its description here
121  desc = cfg["label"].str() + "\n" + desc;
122 
123  entry["details"] = std::move(desc);
124  }
125 
126  entry_list.emplace_back(std::move(entry));
127  difficulties_.emplace_back(cfg["define"].str());
128 
129  if(cfg["default"].to_bool(false)) {
130  selection = n;
131  }
132 
133  ++n;
134  }
135 
136  diff_menu.set_values(entry_list);
137  diff_menu.set_selected(selection);
138  }
139  }
140 }
141 
143 {
144  const std::size_t selection = find_widget<menu_button>("difficulty_menu").get_value();
145  current_difficulty_ = difficulties_.at(std::min(difficulties_.size() - 1, selection));
146 }
147 
149 {
150  using level_ptr = ng::create_engine::level_ptr;
151 
152  auto levels = engine_.get_levels_by_type_unfiltered(level_type::type::sp_campaign);
153 
154  switch(order) {
155  case RANK: // Already sorted by rank
156  // This'll actually never happen, but who knows if that'll ever change...
157  if(!ascending) {
158  std::reverse(levels.begin(), levels.end());
159  }
160 
161  break;
162 
163  case DATE:
164  std::sort(levels.begin(), levels.end(), [ascending](const level_ptr& a, const level_ptr& b) {
165  auto cpn_a = std::dynamic_pointer_cast<ng::campaign>(a);
166  auto cpn_b = std::dynamic_pointer_cast<ng::campaign>(b);
167 
168  if(cpn_b == nullptr) {
169  return cpn_a != nullptr;
170  }
171 
172  if(cpn_a == nullptr) {
173  return false;
174  }
175 
176  return ascending
177  ? cpn_a->dates().first < cpn_b->dates().first
178  : cpn_a->dates().first > cpn_b->dates().first;
179  });
180 
181  break;
182 
183  case NAME:
184  std::sort(levels.begin(), levels.end(), [ascending](const level_ptr& a, const level_ptr& b) {
185  const int cmp = translation::icompare(a->name(), b->name());
186  return ascending ? cmp < 0 : cmp > 0;
187  });
188 
189  break;
190  }
191 
192  tree_view& tree = find_widget<tree_view>("campaign_tree");
193 
194  // Remember which campaign was selected...
195  std::string was_selected;
196  if(!tree.empty()) {
197  was_selected = tree.selected_item()->id();
198  tree.clear();
199  }
200 
201  boost::dynamic_bitset<> show_items;
202  show_items.resize(levels.size(), true);
203 
204  if(!last_search_words_.empty()) {
205  for(unsigned i = 0; i < levels.size(); ++i) {
206  bool found = false;
207  for(const auto& word : last_search_words_) {
208  found = translation::ci_search(levels[i]->name(), word) ||
209  translation::ci_search(levels[i]->data()["name"].t_str().base_str(), word) ||
210  translation::ci_search(levels[i]->description(), word) ||
211  translation::ci_search(levels[i]->data()["description"].t_str().base_str(), word) ||
212  translation::ci_search(levels[i]->data()["abbrev"], word) ||
213  translation::ci_search(levels[i]->data()["abbrev"].t_str().base_str(), word);
214 
215  if(!found) {
216  break;
217  }
218  }
219 
220  show_items[i] = found;
221  }
222  }
223 
224  // List of which options has been selected in the completion filter multimenu_button
225  boost::dynamic_bitset<> filter_comp_options = find_widget<multimenu_button>("filter_completion").get_toggle_states();
226 
227  bool exists_in_filtered_result = false;
228  for(unsigned i = 0; i < levels.size(); ++i) {
229  bool completed = prefs::get().is_campaign_completed(levels[i]->data()["id"]);
230  config::const_child_itors difficulties = levels[i]->data().child_range("difficulty");
231  auto did_complete_at = [](const config& c) { return c["completed_at"].to_bool(); };
232 
233  // Check for non-completion on every difficulty save the first.
234  const bool only_first_completed = difficulties.size() > 1 &&
235  std::none_of(difficulties.begin() + 1, difficulties.end(), did_complete_at);
236  const bool completed_easy = only_first_completed && did_complete_at(difficulties.front());
237  const bool completed_hardest = !difficulties.empty() && did_complete_at(difficulties.back());
238  const bool completed_mid = completed && !completed_hardest && !completed_easy;
239 
240  if( show_items[i] && (
241  ( (!completed) && filter_comp_options[0] ) // Selects all campaigns not finished by player
242  || ( completed && filter_comp_options[4] ) // Selects all campaigns finished by player
243  || ( completed_hardest && filter_comp_options[3] ) // Selects campaigns completed in hardest difficulty
244  || ( completed_easy && filter_comp_options[1] ) // Selects campaigns completed in easiest difficulty
245  || ( completed_mid && filter_comp_options[2]) // Selects campaigns completed in any other difficulty
246  )) {
247  add_campaign_to_tree(levels[i]->data());
248  if (!exists_in_filtered_result) {
249  exists_in_filtered_result = levels[i]->id() == was_selected;
250  }
251  }
252  }
253 
254  if(!was_selected.empty() && exists_in_filtered_result) {
255  find_widget<tree_view_node>(was_selected).select_node();
256  } else {
257  campaign_selected();
258  }
259 }
260 
262 {
263  static bool force = false;
264  if(force) {
265  return;
266  }
267 
268  if(current_sorting_ == order) {
270  currently_sorted_asc_ = false;
271  } else {
272  currently_sorted_asc_ = true;
274  }
275  } else if(current_sorting_ == RANK) {
276  currently_sorted_asc_ = true;
277  current_sorting_ = order;
278  } else {
279  currently_sorted_asc_ = true;
280  current_sorting_ = order;
281 
282  force = true;
283 
284  if(order == NAME) {
285  find_widget<toggle_button>("sort_time").set_value(0);
286  } else if(order == DATE) {
287  find_widget<toggle_button>("sort_name").set_value(0);
288  }
289 
290  force = false;
291  }
292 
294 }
295 
296 void campaign_selection::filter_text_changed(const std::string& text)
297 {
298  const std::vector<std::string> words = utils::split(text, ' ');
299 
300  if(words == last_search_words_) {
301  return;
302  }
303 
304  last_search_words_ = words;
306 }
307 
309 {
310  text_box* filter = find_widget<text_box>("filter_box", false, true);
311  filter->on_modified([this](const auto& box) { filter_text_changed(box.text()); });
312 
313  /***** Setup campaign tree. *****/
314  tree_view& tree = find_widget<tree_view>("campaign_tree");
315 
317  std::bind(&campaign_selection::campaign_selected, this));
318 
319  toggle_button& sort_name = find_widget<toggle_button>("sort_name");
320  toggle_button& sort_time = find_widget<toggle_button>("sort_time");
321 
324 
327 
328  connect_signal_mouse_left_click(find_widget<button>("proceed"),
329  std::bind(&campaign_selection::proceed, this));
330 
332  add_to_keyboard_chain(&tree);
333 
334  /***** Setup campaign details. *****/
335  multi_page& pages = find_widget<multi_page>("campaign_details");
336 
337  // Setup completion filter
338  multimenu_button& filter_comp = find_widget<multimenu_button>("filter_completion");
339  connect_signal_notify_modified(filter_comp,
340  std::bind(&campaign_selection::sort_campaigns, this, RANK, 1));
341  for (unsigned j = 0; j < filter_comp.num_options(); j++) {
342  filter_comp.select_option(j);
343  }
344 
345  // Add campaigns to the list
346  for(const auto& level : engine_.get_levels_by_type_unfiltered(level_type::type::sp_campaign)) {
347  const config& campaign = level->data();
348 
349  /*** Add tree item ***/
350  add_campaign_to_tree(campaign);
351 
352  /*** Add detail item ***/
354  widget_item item;
355 
356  item["label"] = campaign["description"];
357  item["use_markup"] = "true";
358 
359  if(!campaign["description_alignment"].empty()) {
360  item["text_alignment"] = campaign["description_alignment"];
361  }
362 
363  data.emplace("description", item);
364 
365  item["label"] = campaign["image"];
366  data.emplace("image", item);
367 
368  pages.add_page(data);
369  page_ids_.push_back(campaign["id"]);
370  }
371 
372  //
373  // Addon Manager link
374  //
375  config addons;
376  addons["icon"] = "icons/icon-game.png~BLIT(icons/icon-addon-publish.png)";
377  addons["name"] = _("More campaigns...");
378  addons["completed"] = false;
379  addons["id"] = addons_;
380 
381  add_campaign_to_tree(addons);
382 
384  widget_item item;
385 
386  item["label"] = _("In addition to the mainline campaigns, Wesnoth also has an ever-growing list of add-on content created by other players available via the Add-ons server, included but not limited to more single and multiplayer campaigns, multiplayer maps, additional media and various other content! Be sure to give it a try!");
387  data.emplace("description", item);
388  pages.add_page(data);
389  page_ids_.push_back(addons_);
390 
391  std::vector<std::string> dirs;
392  filesystem::get_files_in_dir(game_config::path + "/data/campaigns", nullptr, &dirs);
393  if(dirs.size() <= 15) {
394  config missing;
395  missing["icon"] = "units/unknown-unit.png";
396  missing["name"] = _("Missing Campaigns");
397  missing["completed"] = false;
398  missing["id"] = missing_campaign_;
399 
401 
403  widget_item item;
404 
405  // TRANSLATORS: "more than 15" gives a little leeway to add or remove one without changing the translatable text.
406  // It's already ambiguous, 1.18 has 19 campaigns, if you include the tutorial and multiplayer-only World Conquest.
407  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.");
408  data.emplace("description", item);
409 
410  pages.add_page(data);
411  page_ids_.push_back(missing_campaign_);
412  }
413 
414  //
415  // Set up Mods selection dropdown
416  //
417  multimenu_button& mods_menu = find_widget<multimenu_button>("mods_menu");
418 
420  std::vector<config> mod_menu_values;
421  std::vector<std::string> enabled = engine_.active_mods();
422 
424  const bool active = std::find(enabled.begin(), enabled.end(), mod->id) != enabled.end();
425 
426  mod_menu_values.emplace_back("label", mod->name, "checkbox", active);
427 
428  mod_states_.push_back(active);
429  mod_ids_.emplace_back(mod->id);
430  }
431 
432  mods_menu.set_values(mod_menu_values);
433  mods_menu.select_options(mod_states_);
434 
436  } else {
437  mods_menu.set_active(false);
438  mods_menu.set_label(_("active_modifications^None"));
439  }
440 
441  //
442  // Set up Difficulty dropdown
443  //
444  menu_button& diff_menu = find_widget<menu_button>("difficulty_menu");
445 
446  diff_menu.set_use_markup(true);
448 
450 }
451 
453 {
454  tree_view& tree = find_widget<tree_view>("campaign_tree");
456  widget_item item;
457 
458  item["label"] = campaign["icon"];
459  data.emplace("icon", item);
460 
461  item["label"] = campaign["name"];
462  data.emplace("name", item);
463 
464  // We completed the campaign! Calculate the appropriate victory laurel.
465  if(campaign["completed"].to_bool()) {
466  config::const_child_itors difficulties = campaign.child_range("difficulty");
467 
468  auto did_complete_at = [](const config& c) { return c["completed_at"].to_bool(); };
469 
470  // Check for non-completion on every difficulty save the first.
471  const bool only_first_completed = difficulties.size() > 1 &&
472  std::none_of(difficulties.begin() + 1, difficulties.end(), did_complete_at);
473 
474  /*
475  * Criteria:
476  *
477  * - Use the gold laurel (hardest) for campaigns with only one difficulty OR
478  * if out of two or more difficulties, the last one has been completed.
479  *
480  * - Use the bronze laurel (easiest) only if the first difficulty out of two
481  * or more has been completed.
482  *
483  * - Use the silver laurel otherwise.
484  */
485  if(!difficulties.empty() && did_complete_at(difficulties.back())) {
487  } else if(only_first_completed && did_complete_at(difficulties.front())) {
489  } else {
490  item["label"] = game_config::images::victory_laurel;
491  }
492 
493  data.emplace("victory", item);
494  }
495 
496  auto& node = tree.add_node("campaign", data);
497  node.set_id(campaign["id"]);
499  node.find_widget<toggle_panel>("tree_view_node_label"),
500  std::bind(&campaign_selection::proceed, this)
501  );
502 }
503 
505 {
506  tree_view& tree = find_widget<tree_view>("campaign_tree");
507 
508  if(tree.empty()) {
509  return;
510  }
511 
512  assert(tree.selected_item());
513  const std::string& campaign_id = tree.selected_item()->id();
514  if(!campaign_id.empty()) {
515  if (campaign_id == addons_) {
517  } else {
518  auto iter = std::find(page_ids_.begin(), page_ids_.end(), campaign_id);
519  if(iter != page_ids_.end()) {
520  choice_ = std::distance(page_ids_.begin(), iter);
521  }
523  }
524  }
525 
526 
527  rng_mode_ = RNG_MODE(std::clamp<unsigned>(find_widget<menu_button>("rng_menu").get_value(), RNG_DEFAULT, RNG_BIASED));
528 
530 }
531 
533 {
534  boost::dynamic_bitset<> new_mod_states =
535  find_widget<multimenu_button>("mods_menu").get_toggle_states();
536 
537  // Get a mask of any mods that were toggled, regardless of new state
538  mod_states_ = mod_states_ ^ new_mod_states;
539 
540  for(unsigned i = 0; i < mod_states_.size(); i++) {
541  if(mod_states_[i]) {
543  }
544  }
545 
546  // Save the full toggle states for next time
547  mod_states_ = new_mod_states;
548 }
549 
550 } // namespace dialogs
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
child_itors child_range(config_key_type key)
Definition: config.cpp:272
boost::iterator_range< const_child_iterator > const_child_itors
Definition: config.hpp:282
Simple push button.
Definition: button.hpp:36
virtual void set_active(const bool active) override
See styled_widget::set_active.
Definition: button.cpp:64
void toggle_sorting_selection(CAMPAIGN_ORDER order)
RNG_MODE rng_mode_
whether the player checked the "Deterministic" checkbox.
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_
virtual void pre_show() override
Actions to be taken before showing the window.
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.
std::vector< std::string > last_search_words_
std::vector< std::string > page_ids_
grid & add_page(const widget_item &item)
Adds single page to the grid.
Definition: multi_page.cpp:58
void select_page(const unsigned page, const bool select=true)
Selects a page.
Definition: multi_page.cpp:119
void select_options(const 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.
virtual void set_label(const t_string &text)
virtual void set_use_markup(bool use_markup)
A widget that allows the user to input text in single line.
Definition: text_box.hpp:125
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:98
void set_id(const std::string &id)
Definition: widget.cpp:98
const std::string & id() const
Definition: widget.cpp:110
void set_retval(const int retval, const bool close_window=true)
Sets there return value of the window.
Definition: window.hpp:395
void keyboard_capture(widget *widget)
Definition: window.cpp:1193
void add_to_keyboard_chain(widget *widget)
Adds the widget to the keyboard chain.
Definition: window.cpp:1199
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
static prefs & get()
bool is_campaign_completed(const std::string &campaign_id)
void set_modifications(const std::vector< std::string > &value, bool mp=true)
Declarations for File-IO.
std::size_t i
Definition: function.cpp:1029
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:446
const color_t GRAY_COLOR
std::string victory_laurel_hardest
std::string victory_laurel
std::string victory_laurel_easy
std::string path
Definition: filesystem.cpp:92
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
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
void connect_signal_mouse_left_double_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button double click.
Definition: dispatcher.cpp:198
std::map< std::string, widget_item > widget_data
Definition: widget.hpp:36
std::map< std::string, t_string > widget_item
Definition: widget.hpp:33
@ OK
Dialog was closed with the OK button.
Definition: retval.hpp:35
std::string span_color(const color_t &color, Args &&... data)
Applies Pango markup to the input specifying its display color.
Definition: markup.hpp:87
bool ci_search(const std::string &s1, const std::string &s2)
Definition: gettext.cpp:565
constexpr auto reverse
Definition: ranges.hpp:40
constexpr auto filter
Definition: ranges.hpp:38
std::vector< std::string > split(const config_attribute_value &val)
auto * find(Container &container, const Value &value)
Convenience wrapper for using find on a container without needing to comare to end()
Definition: general.hpp:140
std::string_view data
Definition: picture.cpp:178
mock_char c
static map_location::direction n
#define b