The Battle for Wesnoth  1.19.15+dev
campaign_selection.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2009 - 2025
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 "gui/widgets/button.hpp"
25 #include "gui/widgets/text_box.hpp"
30 #include "gui/widgets/window.hpp"
32 #include "serialization/markup.hpp"
33 #include "utils/irdya_datetime.hpp"
34 
35 #include <functional>
36 
37 namespace gui2::dialogs
38 {
39 
40 REGISTER_DIALOG(campaign_selection)
41 
43  : modal_dialog(window_id())
44  , engine_(eng)
45  , choice_(-1)
46  , rng_mode_(RNG_DEFAULT)
47  , mod_states_()
48  , page_ids_()
49  , difficulties_()
50  , current_difficulty_()
51  , current_sorting_(RANK)
52  , currently_sorted_asc_(true)
53  , mod_ids_()
54 {
55  set_show_even_without_video(true);
56  set_allow_plugin_skip(false);
57 }
58 
60 {
61  tree_view& tree = find_widget<tree_view>("campaign_tree");
62  if(tree.empty()) {
63  return;
64  }
65 
66  assert(tree.selected_item());
67 
68  const std::string& campaign_id = tree.selected_item()->id();
69  if(campaign_id.empty()) {
70  return;
71  }
72 
73  auto iter = std::find(page_ids_.begin(), page_ids_.end(), campaign_id);
74 
75  button& ok_button = find_widget<button>("proceed");
76  ok_button.set_active(campaign_id != missing_campaign_);
77  ok_button.set_label((campaign_id == addons_) ? _("game^Get Add-ons") : _("game^Play"));
78 
79  const int choice = std::distance(page_ids_.begin(), iter);
80  if(iter == page_ids_.end()) {
81  return;
82  }
83 
84  multi_page& pages = find_widget<multi_page>("campaign_details");
85  pages.select_page(choice);
86 
87  engine_.set_current_level(choice);
88 
89  styled_widget& background = find_widget<styled_widget>("campaign_background");
90  background.set_label(engine_.current_level().data()["background"].str());
91 
92  // Rebuild difficulty menu
93  difficulties_.clear();
94 
95  auto& diff_menu = find_widget<menu_button>("difficulty_menu");
96 
97  auto diff_config_range = engine_.current_level().data().child_range("difficulty");
98  const std::size_t difficulty_count = diff_config_range.size();
99 
100  diff_menu.set_active(difficulty_count > 1);
101 
102  if(diff_config_range.empty()) {
103  return;
104  }
105 
106  std::vector<config> entry_list;
107  std::size_t n = 0, selection = 0;
108 
109  for(const auto& cfg : diff_config_range) {
110  config entry;
111 
112  // FIXME: description may have markup that will display weird on the menu_button proper
113  entry["label"] = cfg["label"].str() + " (" + cfg["description"].str() + ")";
114  entry["image"] = cfg["image"].str("misc/blank-hex.png");
115 
116  if(prefs::get().is_campaign_completed(campaign_id, cfg["define"])) {
117  std::string laurel;
118 
119  if(n + 1 >= difficulty_count) {
121  } else if(n == 0) {
123  } else {
125  }
126 
127  entry["image"] = laurel + "~BLIT(" + entry["image"].str() + ")";
128  }
129 
130  if(!cfg["description"].empty()) {
131  std::string desc;
132  if(cfg["auto_markup"].to_bool(true) == false) {
133  desc = cfg["description"].str();
134  } else {
135  desc = markup::span_color(font::GRAY_COLOR, "(", cfg["description"].str(), ")");
136  }
137 
138  // Icons get displayed instead of the labels on the dropdown menu itself,
139  // so we want to prepend each label to its description here
140  desc = cfg["label"].str() + "\n" + desc;
141 
142  entry["details"] = std::move(desc);
143  }
144 
145  entry_list.emplace_back(std::move(entry));
146  difficulties_.emplace_back(cfg["define"].str());
147 
148  if(cfg["default"].to_bool(false)) {
149  selection = n;
150  }
151 
152  ++n;
153  }
154 
155  diff_menu.set_values(entry_list);
156  diff_menu.set_selected(selection);
157 }
158 
160 {
161  const std::size_t selection = find_widget<menu_button>("difficulty_menu").get_value();
162  current_difficulty_ = difficulties_.at(std::min(difficulties_.size() - 1, selection));
163 }
164 
166 {
167  using level_ptr = ng::create_engine::level_ptr;
168 
169  auto levels = engine_.get_levels_by_type_unfiltered(level_type::type::sp_campaign);
170 
171  switch(order) {
172  case RANK: // Already sorted by rank
173  // This'll actually never happen, but who knows if that'll ever change...
174  if(!ascending) {
175  std::reverse(levels.begin(), levels.end());
176  }
177 
178  break;
179 
180  case DATE:
181  std::sort(levels.begin(), levels.end(), [ascending](const level_ptr& a, const level_ptr& b) {
182  auto cpn_a = std::dynamic_pointer_cast<ng::campaign>(a);
183  auto cpn_b = std::dynamic_pointer_cast<ng::campaign>(b);
184 
185  if(cpn_b == nullptr) {
186  return cpn_a != nullptr;
187  }
188 
189  if(cpn_a == nullptr) {
190  return false;
191  }
192 
193  return ascending
194  ? cpn_a->dates().first < cpn_b->dates().first
195  : cpn_a->dates().first > cpn_b->dates().first;
196  });
197 
198  break;
199 
200  case NAME:
201  std::sort(levels.begin(), levels.end(), [ascending](const level_ptr& a, const level_ptr& b) {
202  const int cmp = translation::icompare(a->name(), b->name());
203  return ascending ? cmp < 0 : cmp > 0;
204  });
205 
206  break;
207  }
208 
209  tree_view& tree = find_widget<tree_view>("campaign_tree");
210 
211  // Remember which campaign was selected...
212  std::string was_selected;
213  if(!tree.empty()) {
214  was_selected = tree.selected_item()->id();
215  tree.clear();
216  }
217 
218  boost::dynamic_bitset<> show_items;
219  show_items.resize(levels.size(), true);
220 
221  if(!last_search_words_.empty()) {
222  for(unsigned i = 0; i < levels.size(); ++i) {
223  bool found = false;
224  for(const auto& word : last_search_words_) {
225  found = translation::ci_search(levels[i]->name(), word) ||
226  translation::ci_search(levels[i]->data()["name"].t_str().base_str(), word) ||
227  translation::ci_search(levels[i]->description(), word) ||
228  translation::ci_search(levels[i]->data()["description"].t_str().base_str(), word) ||
229  translation::ci_search(levels[i]->data()["abbrev"], word) ||
230  translation::ci_search(levels[i]->data()["abbrev"].t_str().base_str(), word);
231 
232  if(!found) {
233  break;
234  }
235  }
236 
237  show_items[i] = found;
238  }
239  }
240 
241  // List of which options has been selected in the completion filter multimenu_button
242  boost::dynamic_bitset<> filter_comp_options = find_widget<multimenu_button>("filter_completion").get_toggle_states();
243 
244  bool exists_in_filtered_result = false;
245  for(unsigned i = 0; i < levels.size(); ++i) {
246  bool completed = prefs::get().is_campaign_completed(levels[i]->data()["id"]);
247  config::const_child_itors difficulties = levels[i]->data().child_range("difficulty");
248  auto did_complete_at = [](const config& c) { return c["completed_at"].to_bool(); };
249 
250  // Check for non-completion on every difficulty save the first.
251  const bool only_first_completed = difficulties.size() > 1 &&
252  std::none_of(difficulties.begin() + 1, difficulties.end(), did_complete_at);
253  const bool completed_easy = only_first_completed && did_complete_at(difficulties.front());
254  const bool completed_hardest = !difficulties.empty() && did_complete_at(difficulties.back());
255  const bool completed_mid = completed && !completed_hardest && !completed_easy;
256 
257  if( show_items[i] && (
258  ( (!completed) && filter_comp_options[0] ) // Selects all campaigns not finished by player
259  || ( completed && filter_comp_options[4] ) // Selects all campaigns finished by player
260  || ( completed_hardest && filter_comp_options[3] ) // Selects campaigns completed in hardest difficulty
261  || ( completed_easy && filter_comp_options[1] ) // Selects campaigns completed in easiest difficulty
262  || ( completed_mid && filter_comp_options[2]) // Selects campaigns completed in any other difficulty
263  )) {
264  add_campaign_to_tree(levels[i]->data());
265  if (!exists_in_filtered_result) {
266  exists_in_filtered_result = levels[i]->id() == was_selected;
267  }
268  }
269  }
270 
271  if(!was_selected.empty() && exists_in_filtered_result) {
272  find_widget<tree_view_node>(was_selected).select_node();
273  } else {
274  campaign_selected();
275  }
276 }
277 
279 {
280  static bool force = false;
281  if(force) {
282  return;
283  }
284 
285  if(current_sorting_ == order) {
287  currently_sorted_asc_ = false;
288  } else {
289  currently_sorted_asc_ = true;
291  }
292  } else if(current_sorting_ == RANK) {
293  currently_sorted_asc_ = true;
294  current_sorting_ = order;
295  } else {
296  currently_sorted_asc_ = true;
297  current_sorting_ = order;
298 
299  force = true;
300 
301  if(order == NAME) {
302  find_widget<toggle_button>("sort_time").set_value(0);
303  } else if(order == DATE) {
304  find_widget<toggle_button>("sort_name").set_value(0);
305  }
306 
307  force = false;
308  }
309 
311 }
312 
313 void campaign_selection::filter_text_changed(const std::string& text)
314 {
315  const std::vector<std::string> words = utils::split(text, ' ');
316 
317  if(words == last_search_words_) {
318  return;
319  }
320 
321  last_search_words_ = words;
323 }
324 
326 {
327  text_box* filter = find_widget<text_box>("filter_box", false, true);
328  filter->on_modified([this](const auto& box) { filter_text_changed(box.text()); });
329 
330  /***** Setup campaign tree. *****/
331  tree_view& tree = find_widget<tree_view>("campaign_tree");
332 
334  std::bind(&campaign_selection::campaign_selected, this));
335 
336  toggle_button& sort_name = find_widget<toggle_button>("sort_name");
337  toggle_button& sort_time = find_widget<toggle_button>("sort_time");
338 
341 
344 
345  connect_signal_mouse_left_click(find_widget<button>("proceed"),
346  std::bind(&campaign_selection::proceed, this));
347 
349  add_to_keyboard_chain(&tree);
350 
351  /***** Setup campaign details. *****/
352  multi_page& pages = find_widget<multi_page>("campaign_details");
353 
354  // Setup completion filter
355  multimenu_button& filter_comp = find_widget<multimenu_button>("filter_completion");
356  connect_signal_notify_modified(filter_comp,
357  std::bind(&campaign_selection::sort_campaigns, this, RANK, 1));
358  for (unsigned j = 0; j < filter_comp.num_options(); j++) {
359  filter_comp.select_option(j);
360  }
361 
362  // Add campaigns to the list
363  for(const auto& level : engine_.get_levels_by_type_unfiltered(level_type::type::sp_campaign)) {
364  const config& campaign = level->data();
365 
366  /*** Add tree item ***/
367  add_campaign_to_tree(campaign);
368 
369  /*** Add detail item ***/
370  pages.add_page({
371  {"description", {
372  {"label", campaign["description"].t_str()},
373  {"text_alignment", campaign["description_alignment"].str("left")},
374  {"use_markup", "true"}
375  }},
376  {"image", {
377  {"label", campaign["image"].str()}
378  }}
379  });
380  page_ids_.push_back(campaign["id"]);
381  }
382 
383  //
384  // Addon Manager link
385  //
386  config addons;
387  addons["icon"] = "icons/icon-game.png~BLIT(icons/icon-addon-publish.png)";
388  addons["name"] = _("More campaigns...");
389  addons["completed"] = false;
390  addons["id"] = addons_;
391 
392  add_campaign_to_tree(addons);
393 
394  pages.add_page("go_download_more_stuff", -1, widget_data{});
395  page_ids_.push_back(addons_);
396 
397  std::vector<std::string> dirs;
398  filesystem::get_files_in_dir(game_config::path + "/data/campaigns", nullptr, &dirs);
399  if(dirs.size() <= 15) {
400  config missing;
401  missing["icon"] = "units/unknown-unit.png";
402  missing["name"] = _("Missing Campaigns");
403  missing["completed"] = false;
404  missing["id"] = missing_campaign_;
405 
407 
408  pages.add_page("missing_campaign_warning", -1, widget_data{});
409  page_ids_.push_back(missing_campaign_);
410  }
411 
412  //
413  // Set up Mods selection dropdown
414  //
415  multimenu_button& mods_menu = find_widget<multimenu_button>("mods_menu");
416 
418  std::vector<config> mod_menu_values;
419  std::vector<std::string> enabled = engine_.active_mods();
420 
422  const bool active = std::find(enabled.begin(), enabled.end(), mod->id) != enabled.end();
423 
424  mod_menu_values.emplace_back("label", mod->name, "tooltip", mod->description, "checkbox", active);
425 
426  mod_states_.push_back(active);
427  mod_ids_.emplace_back(mod->id);
428  }
429 
430  mods_menu.set_values(mod_menu_values);
431  mods_menu.select_options(mod_states_);
432 
434  } else {
435  mods_menu.set_active(false);
436  mods_menu.set_label(_("active_modifications^None"));
437  }
438 
439  //
440  // Set up Difficulty dropdown
441  //
442  menu_button& diff_menu = find_widget<menu_button>("difficulty_menu");
443 
444  diff_menu.set_use_markup(true);
446 
448 
449  plugins_context_.reset(new plugins_context("Campaign Selection"));
450  plugins_context_->set_callback("create", [this](const config&) { set_retval(retval::OK); }, false);
451  plugins_context_->set_callback("quit", [this](const config&) { set_retval(retval::CANCEL); }, false);
452 
453  plugins_context_->set_accessor("find_level", [this](const config& cfg) {
454  const std::string id = cfg["id"].str();
455  auto result = engine_.find_level_by_id(id);
456  return config {
457  "index", result.second,
458  "type", level_type::get_string(result.first),
459  };
460  });
461 
462  plugins_context_->set_accessor_int("find_mod", [this](const config& cfg) {
464  });
465 
466  plugins_context_->set_callback("select_level", [this](const config& cfg) {
467  choice_ = cfg["index"].to_int();
469  }, true);
470 }
471 
473 {
474  // We completed the campaign! Calculate the appropriate victory laurel.
475  const auto get_laurel = [&campaign] {
476  if(!campaign["completed"].to_bool()) {
477  return std::string{};
478  }
479 
480  config::const_child_itors difficulties = campaign.child_range("difficulty");
481  auto did_complete_at = [](const config& c) { return c["completed_at"].to_bool(); };
482 
483  // Check for non-completion on every difficulty save the first.
484  const bool only_first_completed = difficulties.size() > 1 &&
485  std::none_of(difficulties.begin() + 1, difficulties.end(), did_complete_at);
486 
487  /*
488  * Criteria:
489  *
490  * - Use the gold laurel (hardest) for campaigns with only one difficulty OR
491  * if out of two or more difficulties, the last one has been completed.
492  *
493  * - Use the bronze laurel (easiest) only if the first difficulty out of two
494  * or more has been completed.
495  *
496  * - Use the silver laurel otherwise.
497  */
498  if(!difficulties.empty() && did_complete_at(difficulties.back())) {
500  } else if(only_first_completed && did_complete_at(difficulties.front())) {
502  } else {
504  }
505  };
506 
507  auto& node = find_widget<tree_view>("campaign_tree")
508  .add_node("campaign", {
509  {"icon", {
510  {"label", campaign["icon"].str()}
511  }},
512  {"name", {
513  {"label", campaign["name"].t_str()}
514  }},
515  {"victory", {
516  {"label", get_laurel()}
517  }}
518  });
519 
520  node.set_id(campaign["id"]);
522  node.find_widget<toggle_panel>("tree_view_node_label"),
523  std::bind(&campaign_selection::proceed, this)
524  );
525 }
526 
528 {
529  tree_view& tree = find_widget<tree_view>("campaign_tree");
530 
531  if(tree.empty()) {
532  return;
533  }
534 
535  assert(tree.selected_item());
536  const std::string& campaign_id = tree.selected_item()->id();
537  if(!campaign_id.empty()) {
538  if (campaign_id == addons_) {
540  } else {
541  auto iter = std::find(page_ids_.begin(), page_ids_.end(), campaign_id);
542  if(iter != page_ids_.end()) {
543  choice_ = std::distance(page_ids_.begin(), iter);
544  }
546  }
547  }
548 
549 
550  rng_mode_ = RNG_MODE(std::clamp<unsigned>(find_widget<menu_button>("rng_menu").get_value(), RNG_DEFAULT, RNG_BIASED));
551 
553 }
554 
556 {
557  boost::dynamic_bitset<> new_mod_states =
558  find_widget<multimenu_button>("mods_menu").get_toggle_states();
559 
560  // Get a mask of any mods that were toggled, regardless of new state
561  mod_states_ = mod_states_ ^ new_mod_states;
562 
563  for(unsigned i = 0; i < mod_states_.size(); i++) {
564  if(mod_states_[i]) {
566  }
567  }
568 
569  // Save the full toggle states for next time
570  mod_states_ = new_mod_states;
571 }
572 
573 } // 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:268
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_
Abstract base class for all modal dialogs.
std::unique_ptr< plugins_context > plugins_context_
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:98
tree_view_node * selected_item()
Definition: tree_view.hpp: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:387
void keyboard_capture(widget *widget)
Definition: window.cpp:1197
void add_to_keyboard_chain(widget *widget)
Adds the widget to the keyboard chain.
Definition: window.cpp:1211
int find_extra_by_id(const MP_EXTRA extra_type, const std::string &id) const
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::pair< level_type::type, int > find_level_by_id(const std::string &id) const
std::vector< level_ptr > get_levels_by_type_unfiltered(level_type::type type) const
void set_current_level(const std::size_t index)
level & current_level() const
const config & data() 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)
const config * cfg
Declarations for File-IO.
std::size_t i
Definition: function.cpp:1032
static std::string _(const char *str)
Definition: gettext.hpp:97
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:463
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:106
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:189
void connect_signal_mouse_left_click(dispatcher &dispatcher, const signal &signal)
Connects a signal handler for a left mouse button click.
Definition: dispatcher.cpp:163
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:184
std::map< std::string, widget_item > widget_data
Definition: widget.hpp:36
@ OK
Dialog was closed with the OK button.
Definition: retval.hpp:35
@ CANCEL
Dialog was closed with the CANCEL button.
Definition: retval.hpp:38
std::string span_color(const color_t &color, Args &&... data)
Applies Pango markup to the input specifying its display color.
Definition: markup.hpp:110
bool ci_search(const std::string &s1, const std::string &s2)
Case-insensitive search.
Definition: gettext.cpp:559
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:141
std::string_view data
Definition: picture.cpp:188
static std::string get_string(enum_type key)
Converts a enum to its string equivalent.
Definition: enum_base.hpp:46
mock_char c
static map_location::direction n
#define b