The Battle for Wesnoth  1.19.8+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 
44  : modal_dialog(window_id())
45  , engine_(eng)
46  , choice_(-1)
47  , rng_mode_(RNG_DEFAULT)
48  , mod_states_()
49  , page_ids_()
50  , difficulties_()
51  , current_difficulty_()
52  , current_sorting_(RANK)
53  , currently_sorted_asc_(true)
54  , mod_ids_()
55 {
56  set_show_even_without_video(true);
57  set_allow_plugin_skip(false);
58 }
59 
61 {
62  tree_view& tree = find_widget<tree_view>("campaign_tree");
63  if(tree.empty()) {
64  return;
65  }
66 
67  assert(tree.selected_item());
68 
69  const std::string& campaign_id = tree.selected_item()->id();
70 
71  if(!campaign_id.empty()) {
72  auto iter = std::find(page_ids_.begin(), page_ids_.end(), campaign_id);
73 
74  button& ok_button = find_widget<button>("proceed");
75  ok_button.set_active(campaign_id != missing_campaign_);
76  ok_button.set_label((campaign_id == addons_) ? _("game^Get Add-ons") : _("game^Play"));
77 
78  const int choice = std::distance(page_ids_.begin(), iter);
79  if(iter == page_ids_.end()) {
80  return;
81  }
82 
83  multi_page& pages = find_widget<multi_page>("campaign_details");
84  pages.select_page(choice);
85 
86  engine_.set_current_level(choice);
87 
88  styled_widget& background = find_widget<styled_widget>("campaign_background");
89  background.set_label(engine_.current_level().data()["background"].str());
90 
91  // Rebuild difficulty menu
92  difficulties_.clear();
93 
94  auto& diff_menu = find_widget<menu_button>("difficulty_menu");
95 
96  const auto& diff_config = generate_difficulty_config(engine_.current_level().data());
97  diff_menu.set_active(diff_config.child_count("difficulty") > 1);
98 
99  if(!diff_config.empty()) {
100  std::vector<config> entry_list;
101  unsigned n = 0, selection = 0, max_n = diff_config.child_count("difficulty");
102 
103  for(const auto& cfg : diff_config.child_range("difficulty")) {
104  config entry;
105 
106  // FIXME: description may have markup that will display weird on the menu_button proper
107  entry["label"] = cfg["label"].str() + " (" + cfg["description"].str() + ")";
108  entry["image"] = cfg["image"].str("misc/blank-hex.png");
109 
110  if(prefs::get().is_campaign_completed(campaign_id, cfg["define"])) {
111  std::string laurel;
112 
113  if(n + 1 >= max_n) {
115  } else if(n == 0) {
117  } else {
119  }
120 
121  entry["image"] = laurel + "~BLIT(" + entry["image"].str() + ")";
122  }
123 
124  if(!cfg["description"].empty()) {
125  std::string desc;
126  if(cfg["auto_markup"].to_bool(true) == false) {
127  desc = cfg["description"].str();
128  } else {
129  if(!cfg["old_markup"].to_bool()) {
130  desc += markup::span_color(font::GRAY_COLOR, "(", cfg["description"].str(), ")");
131  } else {
132  desc += markup::span_color(font::GRAY_COLOR, cfg["description"].str());
133  }
134  }
135 
136  // Icons get displayed instead of the labels on the dropdown menu itself,
137  // so we want to prepend each label to its description here
138  desc = cfg["label"].str() + "\n" + desc;
139 
140  entry["details"] = std::move(desc);
141  }
142 
143  entry_list.emplace_back(std::move(entry));
144  difficulties_.emplace_back(cfg["define"].str());
145 
146  if(cfg["default"].to_bool(false)) {
147  selection = n;
148  }
149 
150  ++n;
151  }
152 
153  diff_menu.set_values(entry_list);
154  diff_menu.set_selected(selection);
155  }
156  }
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 ***/
371  widget_item item;
372 
373  item["label"] = campaign["description"];
374  item["use_markup"] = "true";
375 
376  if(!campaign["description_alignment"].empty()) {
377  item["text_alignment"] = campaign["description_alignment"];
378  }
379 
380  data.emplace("description", item);
381 
382  item["label"] = campaign["image"];
383  data.emplace("image", item);
384 
385  pages.add_page(data);
386  page_ids_.push_back(campaign["id"]);
387  }
388 
389  //
390  // Addon Manager link
391  //
392  config addons;
393  addons["icon"] = "icons/icon-game.png~BLIT(icons/icon-addon-publish.png)";
394  addons["name"] = _("More campaigns...");
395  addons["completed"] = false;
396  addons["id"] = addons_;
397 
398  add_campaign_to_tree(addons);
399 
401  widget_item item;
402 
403  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!");
404  data.emplace("description", item);
405  pages.add_page(data);
406  page_ids_.push_back(addons_);
407 
408  std::vector<std::string> dirs;
409  filesystem::get_files_in_dir(game_config::path + "/data/campaigns", nullptr, &dirs);
410  if(dirs.size() <= 15) {
411  config missing;
412  missing["icon"] = "units/unknown-unit.png";
413  missing["name"] = _("Missing Campaigns");
414  missing["completed"] = false;
415  missing["id"] = missing_campaign_;
416 
418 
420  widget_item item;
421 
422  // TRANSLATORS: "more than 15" gives a little leeway to add or remove one without changing the translatable text.
423  // It's already ambiguous, 1.18 has 19 campaigns, if you include the tutorial and multiplayer-only World Conquest.
424  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.");
425  data.emplace("description", item);
426 
427  pages.add_page(data);
428  page_ids_.push_back(missing_campaign_);
429  }
430 
431  //
432  // Set up Mods selection dropdown
433  //
434  multimenu_button& mods_menu = find_widget<multimenu_button>("mods_menu");
435 
437  std::vector<config> mod_menu_values;
438  std::vector<std::string> enabled = engine_.active_mods();
439 
441  const bool active = std::find(enabled.begin(), enabled.end(), mod->id) != enabled.end();
442 
443  mod_menu_values.emplace_back("label", mod->name, "checkbox", active);
444 
445  mod_states_.push_back(active);
446  mod_ids_.emplace_back(mod->id);
447  }
448 
449  mods_menu.set_values(mod_menu_values);
450  mods_menu.select_options(mod_states_);
451 
453  } else {
454  mods_menu.set_active(false);
455  mods_menu.set_label(_("active_modifications^None"));
456  }
457 
458  //
459  // Set up Difficulty dropdown
460  //
461  menu_button& diff_menu = find_widget<menu_button>("difficulty_menu");
462 
463  diff_menu.set_use_markup(true);
465 
467 
468  plugins_context_.reset(new plugins_context("Campaign Selection"));
469  plugins_context_->set_callback("create", [this](const config&) { set_retval(retval::OK); }, false);
470  plugins_context_->set_callback("quit", [this](const config&) { set_retval(retval::CANCEL); }, false);
471 
472  plugins_context_->set_accessor("find_level", [this](const config& cfg) {
473  const std::string id = cfg["id"].str();
474  auto result = engine_.find_level_by_id(id);
475  return config {
476  "index", result.second,
477  "type", level_type::get_string(result.first),
478  };
479  });
480 
481  plugins_context_->set_accessor_int("find_mod", [this](const config& cfg) {
483  });
484 
485  plugins_context_->set_callback("select_level", [this](const config& cfg) {
486  choice_ = cfg["index"].to_int();
488  }, true);
489 }
490 
492 {
493  tree_view& tree = find_widget<tree_view>("campaign_tree");
495  widget_item item;
496 
497  item["label"] = campaign["icon"];
498  data.emplace("icon", item);
499 
500  item["label"] = campaign["name"];
501  data.emplace("name", item);
502 
503  // We completed the campaign! Calculate the appropriate victory laurel.
504  if(campaign["completed"].to_bool()) {
505  config::const_child_itors difficulties = campaign.child_range("difficulty");
506 
507  auto did_complete_at = [](const config& c) { return c["completed_at"].to_bool(); };
508 
509  // Check for non-completion on every difficulty save the first.
510  const bool only_first_completed = difficulties.size() > 1 &&
511  std::none_of(difficulties.begin() + 1, difficulties.end(), did_complete_at);
512 
513  /*
514  * Criteria:
515  *
516  * - Use the gold laurel (hardest) for campaigns with only one difficulty OR
517  * if out of two or more difficulties, the last one has been completed.
518  *
519  * - Use the bronze laurel (easiest) only if the first difficulty out of two
520  * or more has been completed.
521  *
522  * - Use the silver laurel otherwise.
523  */
524  if(!difficulties.empty() && did_complete_at(difficulties.back())) {
526  } else if(only_first_completed && did_complete_at(difficulties.front())) {
528  } else {
529  item["label"] = game_config::images::victory_laurel;
530  }
531 
532  data.emplace("victory", item);
533  }
534 
535  auto& node = tree.add_node("campaign", data);
536  node.set_id(campaign["id"]);
538  node.find_widget<toggle_panel>("tree_view_node_label"),
539  std::bind(&campaign_selection::proceed, this)
540  );
541 }
542 
544 {
545  tree_view& tree = find_widget<tree_view>("campaign_tree");
546 
547  if(tree.empty()) {
548  return;
549  }
550 
551  assert(tree.selected_item());
552  const std::string& campaign_id = tree.selected_item()->id();
553  if(!campaign_id.empty()) {
554  if (campaign_id == addons_) {
556  } else {
557  auto iter = std::find(page_ids_.begin(), page_ids_.end(), campaign_id);
558  if(iter != page_ids_.end()) {
559  choice_ = std::distance(page_ids_.begin(), iter);
560  }
562  }
563  }
564 
565 
566  rng_mode_ = RNG_MODE(std::clamp<unsigned>(find_widget<menu_button>("rng_menu").get_value(), RNG_DEFAULT, RNG_BIASED));
567 
569 }
570 
572 {
573  boost::dynamic_bitset<> new_mod_states =
574  find_widget<multimenu_button>("mods_menu").get_toggle_states();
575 
576  // Get a mask of any mods that were toggled, regardless of new state
577  mod_states_ = mod_states_ ^ new_mod_states;
578 
579  for(unsigned i = 0; i < mod_states_.size(); i++) {
580  if(mod_states_[i]) {
582  }
583  }
584 
585  // Save the full toggle states for next time
586  mod_states_ = new_mod_states;
587 }
588 
589 } // 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_
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: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
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)
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
@ 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: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
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