The Battle for Wesnoth  1.19.5+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);
312  std::bind(&campaign_selection::filter_text_changed, this, std::placeholders::_2));
313 
314  /***** Setup campaign tree. *****/
315  tree_view& tree = find_widget<tree_view>("campaign_tree");
316 
318  std::bind(&campaign_selection::campaign_selected, this));
319 
320  toggle_button& sort_name = find_widget<toggle_button>("sort_name");
321  toggle_button& sort_time = find_widget<toggle_button>("sort_time");
322 
325 
328 
329  connect_signal_mouse_left_click(find_widget<button>("proceed"),
330  std::bind(&campaign_selection::proceed, this));
331 
332  keyboard_capture(filter);
333  add_to_keyboard_chain(&tree);
334 
335  /***** Setup campaign details. *****/
336  multi_page& pages = find_widget<multi_page>("campaign_details");
337 
338  // Setup completion filter
339  multimenu_button& filter_comp = find_widget<multimenu_button>("filter_completion");
340  connect_signal_notify_modified(filter_comp,
341  std::bind(&campaign_selection::sort_campaigns, this, RANK, 1));
342  for (unsigned j = 0; j < filter_comp.num_options(); j++) {
343  filter_comp.select_option(j);
344  }
345 
346  // Add campaigns to the list
347  for(const auto& level : engine_.get_levels_by_type_unfiltered(level_type::type::sp_campaign)) {
348  const config& campaign = level->data();
349 
350  /*** Add tree item ***/
351  add_campaign_to_tree(campaign);
352 
353  /*** Add detail item ***/
355  widget_item item;
356 
357  item["label"] = campaign["description"];
358  item["use_markup"] = "true";
359 
360  if(!campaign["description_alignment"].empty()) {
361  item["text_alignment"] = campaign["description_alignment"];
362  }
363 
364  data.emplace("description", item);
365 
366  item["label"] = campaign["image"];
367  data.emplace("image", item);
368 
369  pages.add_page(data);
370  page_ids_.push_back(campaign["id"]);
371  }
372 
373  //
374  // Addon Manager link
375  //
376  config addons;
377  addons["icon"] = "icons/icon-game.png~BLIT(icons/icon-addon-publish.png)";
378  addons["name"] = _("More campaigns...");
379  addons["completed"] = false;
380  addons["id"] = addons_;
381 
382  add_campaign_to_tree(addons);
383 
385  widget_item item;
386 
387  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!");
388  data.emplace("description", item);
389  pages.add_page(data);
390  page_ids_.push_back(addons_);
391 
392  std::vector<std::string> dirs;
393  filesystem::get_files_in_dir(game_config::path + "/data/campaigns", nullptr, &dirs);
394  if(dirs.size() <= 15) {
395  config missing;
396  missing["icon"] = "units/unknown-unit.png";
397  missing["name"] = _("Missing Campaigns");
398  missing["completed"] = false;
399  missing["id"] = missing_campaign_;
400 
402 
404  widget_item item;
405 
406  // TRANSLATORS: "more than 15" gives a little leeway to add or remove one without changing the translatable text.
407  // It's already ambiguous, 1.18 has 19 campaigns, if you include the tutorial and multiplayer-only World Conquest.
408  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.");
409  data.emplace("description", item);
410 
411  pages.add_page(data);
412  page_ids_.push_back(missing_campaign_);
413  }
414 
415  //
416  // Set up Mods selection dropdown
417  //
418  multimenu_button& mods_menu = find_widget<multimenu_button>("mods_menu");
419 
421  std::vector<config> mod_menu_values;
422  std::vector<std::string> enabled = engine_.active_mods();
423 
425  const bool active = std::find(enabled.begin(), enabled.end(), mod->id) != enabled.end();
426 
427  mod_menu_values.emplace_back("label", mod->name, "checkbox", active);
428 
429  mod_states_.push_back(active);
430  mod_ids_.emplace_back(mod->id);
431  }
432 
433  mods_menu.set_values(mod_menu_values);
434  mods_menu.select_options(mod_states_);
435 
437  } else {
438  mods_menu.set_active(false);
439  mods_menu.set_label(_("active_modifications^None"));
440  }
441 
442  //
443  // Set up Difficulty dropdown
444  //
445  menu_button& diff_menu = find_widget<menu_button>("difficulty_menu");
446 
447  diff_menu.set_use_markup(true);
449 
451 }
452 
454 {
455  tree_view& tree = find_widget<tree_view>("campaign_tree");
457  widget_item item;
458 
459  item["label"] = campaign["icon"];
460  data.emplace("icon", item);
461 
462  item["label"] = campaign["name"];
463  data.emplace("name", item);
464 
465  // We completed the campaign! Calculate the appropriate victory laurel.
466  if(campaign["completed"].to_bool()) {
467  config::const_child_itors difficulties = campaign.child_range("difficulty");
468 
469  auto did_complete_at = [](const config& c) { return c["completed_at"].to_bool(); };
470 
471  // Check for non-completion on every difficulty save the first.
472  const bool only_first_completed = difficulties.size() > 1 &&
473  std::none_of(difficulties.begin() + 1, difficulties.end(), did_complete_at);
474 
475  /*
476  * Criteria:
477  *
478  * - Use the gold laurel (hardest) for campaigns with only one difficulty OR
479  * if out of two or more difficulties, the last one has been completed.
480  *
481  * - Use the bronze laurel (easiest) only if the first difficulty out of two
482  * or more has been completed.
483  *
484  * - Use the silver laurel otherwise.
485  */
486  if(!difficulties.empty() && did_complete_at(difficulties.back())) {
488  } else if(only_first_completed && did_complete_at(difficulties.front())) {
490  } else {
491  item["label"] = game_config::images::victory_laurel;
492  }
493 
494  data.emplace("victory", item);
495  }
496 
497  auto& node = tree.add_node("campaign", data);
498  node.set_id(campaign["id"]);
500  node.find_widget<toggle_panel>("tree_view_node_label"),
501  std::bind(&campaign_selection::proceed, this)
502  );
503 }
504 
506 {
507  tree_view& tree = find_widget<tree_view>("campaign_tree");
508 
509  if(tree.empty()) {
510  return;
511  }
512 
513  assert(tree.selected_item());
514  const std::string& campaign_id = tree.selected_item()->id();
515  if(!campaign_id.empty()) {
516  if (campaign_id == addons_) {
518  } else {
519  auto iter = std::find(page_ids_.begin(), page_ids_.end(), campaign_id);
520  if(iter != page_ids_.end()) {
521  choice_ = std::distance(page_ids_.begin(), iter);
522  }
524  }
525  }
526 
527 
528  rng_mode_ = RNG_MODE(std::clamp<unsigned>(find_widget<menu_button>("rng_menu").get_value(), RNG_DEFAULT, RNG_BIASED));
529 
531 }
532 
534 {
535  boost::dynamic_bitset<> new_mod_states =
536  find_widget<multimenu_button>("mods_menu").get_toggle_states();
537 
538  // Get a mask of any mods that were toggled, regardless of new state
539  mod_states_ = mod_states_ ^ new_mod_states;
540 
541  for(unsigned i = 0; i < mod_states_.size(); i++) {
542  if(mod_states_[i]) {
544  }
545  }
546 
547  // Save the full toggle states for next time
548  mod_states_ = new_mod_states;
549 }
550 
551 } // namespace dialogs
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:172
child_itors child_range(config_key_type key)
Definition: config.cpp:272
boost::iterator_range< const_child_iterator > const_child_itors
Definition: config.hpp:296
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:43
void select_page(const unsigned page, const bool select=true)
Selects a page.
Definition: multi_page.cpp:104
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.
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.
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:1207
void add_to_keyboard_chain(widget *widget)
Adds the widget to the keyboard chain.
Definition: window.cpp:1213
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:1028
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:444
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:90
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)
Definition: markup.hpp:68
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:178
mock_char c
static map_location::direction n
#define b