The Battle for Wesnoth  1.19.27+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 namespace
41 {
42  // These arbitary strings are used instead of a campaign id for the special pages
43  // which can appear in the right-hand pane. The data for those pages is loaded
44  // from the tree_view's WML via ids hardcoded in pre_show().
45  const std::string PAGE_ID_GET_ADDONS = "////addons////";
46  const std::string PAGE_ID_LANDING_PAGE = "////landing-page////";
47  const std::string PAGE_ID_MISSING_CAMPAIGNS = "////missing-campaign////";
48 
49  // The campaign_rng_mode preference stores a string mirroring the savefile's random_mode RNG.
50  const std::vector<std::string> rng_mode_prefs{"default", "deterministic", "biased"};
51 };
52 
53 REGISTER_DIALOG(campaign_selection)
54 
56  : modal_dialog(window_id())
57  , engine_(eng)
58  , choice_(-1)
59  , rng_mode_(RNG_DEFAULT)
60  , mod_states_()
61  , page_ids_()
62  , difficulties_()
63  , current_difficulty_()
64  , current_sorting_(RANK)
65  , currently_sorted_asc_(true)
66  , mod_ids_()
67 {
68  set_show_even_without_video(true);
69  set_allow_plugin_skip(false);
70 }
71 
73 {
74  tree_view& tree = find_widget<tree_view>("campaign_tree");
75  const std::string& campaign_id = tree.selected_item() ? tree.selected_item()->id() : PAGE_ID_LANDING_PAGE;
76  if(campaign_id.empty()) {
77  return;
78  }
79 
80  const bool can_proceed = campaign_id != PAGE_ID_MISSING_CAMPAIGNS && campaign_id != PAGE_ID_LANDING_PAGE;
81  const bool actual_campaign = can_proceed && campaign_id != PAGE_ID_GET_ADDONS;
82 
83  button& ok_button = find_widget<button>("ok");
84  ok_button.set_active(can_proceed);
85  ok_button.set_label((campaign_id == PAGE_ID_GET_ADDONS) ? _("game^Get Add-ons") : _("game^Play"));
86 
87  auto iter = std::find(page_ids_.begin(), page_ids_.end(), campaign_id);
88  if(iter == page_ids_.end()) {
89  return;
90  }
91  const int choice = std::distance(page_ids_.begin(), iter);
92 
93  multi_page& pages = find_widget<multi_page>("campaign_details");
94  pages.select_page(choice);
95 
96  // The engine bounds-checks this and silently selects the first campaign if choice points to one
97  // of the non-campaign pages.
98  engine_.set_current_level(choice);
99 
100  styled_widget& background = find_widget<styled_widget>("campaign_background");
101  background.set_label(engine_.current_level().data()["background"].str());
102 
103  // Rebuild difficulty menu
104  difficulties_.clear();
105 
106  auto& diff_menu = find_widget<menu_button>("difficulty_menu");
107 
108  auto diff_config_range = engine_.current_level().data().child_range("difficulty");
109  const std::size_t difficulty_count = diff_config_range.size();
110 
111  if(diff_config_range.empty() || !actual_campaign) {
112  // Cosmetic bug: this leaves the difficulties of a previously-selected campaign visible
113  diff_menu.set_active(false);
114  return;
115  }
116 
117  std::vector<config> entry_list;
118  std::size_t n = 0, selection = 0;
119 
120  for(const auto& cfg : diff_config_range) {
121  config entry;
122 
123  // FIXME: description may have markup that will display weird on the menu_button proper
124  entry["label"] = cfg["label"].str() + " (" + cfg["description"].str() + ")";
125  entry["image"] = cfg["image"].str("misc/blank-hex.png");
126 
127  if(prefs::get().is_campaign_completed(campaign_id, cfg["define"])) {
128  std::string laurel;
129 
130  if(n + 1 >= difficulty_count) {
132  } else if(n == 0) {
134  } else {
136  }
137 
138  entry["image"] = laurel + "~BLIT(" + entry["image"].str() + ")";
139  }
140 
141  if(!cfg["description"].empty()) {
142  std::string desc;
143  if(cfg["auto_markup"].to_bool(true) == false) {
144  desc = cfg["description"].str();
145  } else {
146  desc = markup::span_color(font::GRAY_COLOR, "(", cfg["description"].str(), ")");
147  }
148 
149  // Icons get displayed instead of the labels on the dropdown menu itself,
150  // so we want to prepend each label to its description here
151  desc = cfg["label"].str() + "\n" + desc;
152 
153  entry["details"] = std::move(desc);
154  }
155 
156  entry_list.emplace_back(std::move(entry));
157  difficulties_.emplace_back(cfg["define"].str());
158 
159  if(cfg["default"].to_bool(false)) {
160  selection = n;
161  }
162 
163  ++n;
164  }
165 
166  diff_menu.set_active(true);
167  diff_menu.set_values(entry_list);
168  diff_menu.set_selected(selection);
169 }
170 
172 {
173  const std::size_t selection = find_widget<menu_button>("difficulty_menu").get_value();
174  current_difficulty_ = difficulties_.at(std::min(difficulties_.size() - 1, selection));
175 }
176 
178 {
179  using level_ptr = ng::create_engine::level_ptr;
180 
181  auto levels = engine_.get_levels_by_type_unfiltered(level_type::type::sp_campaign);
182 
183  switch(order) {
184  case RANK: // Already sorted by rank
185  // This'll actually never happen, but who knows if that'll ever change...
186  if(!ascending) {
187  std::reverse(levels.begin(), levels.end());
188  }
189 
190  break;
191 
192  case DATE:
193  std::sort(levels.begin(), levels.end(), [ascending](const level_ptr& a, const level_ptr& b) {
194  auto cpn_a = std::dynamic_pointer_cast<ng::campaign>(a);
195  auto cpn_b = std::dynamic_pointer_cast<ng::campaign>(b);
196 
197  if(cpn_b == nullptr) {
198  return cpn_a != nullptr;
199  }
200 
201  if(cpn_a == nullptr) {
202  return false;
203  }
204 
205  return ascending
206  ? cpn_a->dates().first < cpn_b->dates().first
207  : cpn_a->dates().first > cpn_b->dates().first;
208  });
209 
210  break;
211 
212  case NAME:
213  std::sort(levels.begin(), levels.end(), [ascending](const level_ptr& a, const level_ptr& b) {
214  const int cmp = translation::icompare(a->name(), b->name());
215  return ascending ? cmp < 0 : cmp > 0;
216  });
217 
218  break;
219  }
220 
221  tree_view& tree = find_widget<tree_view>("campaign_tree");
222 
223  // Remember which campaign was selected...
224  std::string was_selected;
225  if(tree.selected_item()) {
226  was_selected = tree.selected_item()->id();
227  }
228  tree.clear();
229 
230  boost::dynamic_bitset<> show_items;
231  show_items.resize(levels.size(), true);
232 
233  if(!last_search_words_.empty()) {
234  for(unsigned i = 0; i < levels.size(); ++i) {
235  bool found = false;
236  for(const auto& word : last_search_words_) {
237  found = translation::ci_search(levels[i]->name(), word) ||
238  translation::ci_search(levels[i]->data()["name"].t_str().base_str(), word) ||
239  translation::ci_search(levels[i]->description(), word) ||
240  translation::ci_search(levels[i]->data()["description"].t_str().base_str(), word) ||
241  translation::ci_search(levels[i]->data()["abbrev"], word) ||
242  translation::ci_search(levels[i]->data()["abbrev"].t_str().base_str(), word);
243 
244  if(!found) {
245  break;
246  }
247  }
248 
249  show_items[i] = found;
250  }
251  }
252 
253  // List of which options has been selected in the completion filter multimenu_button
254  boost::dynamic_bitset<> filter_comp_options = find_widget<multimenu_button>("filter_completion").get_toggle_states();
255 
256  bool exists_in_filtered_result = false;
257  for(unsigned i = 0; i < levels.size(); ++i) {
258  bool completed = prefs::get().is_campaign_completed(levels[i]->data()["id"]);
259  config::const_child_itors difficulties = levels[i]->data().child_range("difficulty");
260  auto did_complete_at = [](const config& c) { return c["completed_at"].to_bool(); };
261 
262  // Check for non-completion on every difficulty save the first.
263  const bool only_first_completed = difficulties.size() > 1 &&
264  std::none_of(difficulties.begin() + 1, difficulties.end(), did_complete_at);
265  const bool completed_easy = only_first_completed && did_complete_at(difficulties.front());
266  const bool completed_hardest = !difficulties.empty() && did_complete_at(difficulties.back());
267  const bool completed_mid = completed && !completed_hardest && !completed_easy;
268 
269  if( show_items[i] && (
270  ( (!completed) && filter_comp_options[0] ) // Selects all campaigns not finished by player
271  || ( completed && filter_comp_options[4] ) // Selects all campaigns finished by player
272  || ( completed_hardest && filter_comp_options[3] ) // Selects campaigns completed in hardest difficulty
273  || ( completed_easy && filter_comp_options[1] ) // Selects campaigns completed in easiest difficulty
274  || ( completed_mid && filter_comp_options[2]) // Selects campaigns completed in any other difficulty
275  )) {
276  add_campaign_to_tree(levels[i]->data());
277  if (!exists_in_filtered_result) {
278  exists_in_filtered_result = levels[i]->id() == was_selected;
279  }
280  }
281  }
282 
283  if(!was_selected.empty() && exists_in_filtered_result) {
284  find_widget<tree_view_node>(was_selected).select_node();
285  } else {
286  campaign_selected();
287  }
288 }
289 
291 {
292  static bool force = false;
293  if(force) {
294  return;
295  }
296 
297  if(current_sorting_ == order) {
299  currently_sorted_asc_ = false;
300  } else {
301  currently_sorted_asc_ = true;
303  }
304  } else if(current_sorting_ == RANK) {
305  currently_sorted_asc_ = true;
306  current_sorting_ = order;
307  } else {
308  currently_sorted_asc_ = true;
309  current_sorting_ = order;
310 
311  force = true;
312 
313  if(order == NAME) {
314  find_widget<toggle_button>("sort_time").set_value(0);
315  } else if(order == DATE) {
316  find_widget<toggle_button>("sort_name").set_value(0);
317  }
318 
319  force = false;
320  }
321 
323 }
324 
325 void campaign_selection::filter_text_changed(const std::string& text)
326 {
327  const std::vector<std::string> words = utils::split(text, ' ');
328 
329  if(words == last_search_words_) {
330  return;
331  }
332 
333  last_search_words_ = words;
335 }
336 
338 {
339  text_box* filter = find_widget<text_box>("filter_box", false, true);
340  filter->on_modified([this](const auto& box) { filter_text_changed(box.text()); });
341 
342  /***** Setup campaign tree. *****/
343  tree_view& tree = find_widget<tree_view>("campaign_tree");
344 
346  std::bind(&campaign_selection::campaign_selected, this));
347 
348  toggle_button& sort_name = find_widget<toggle_button>("sort_name");
349  toggle_button& sort_time = find_widget<toggle_button>("sort_time");
350 
353 
356 
357 #ifdef __IPHONEOS__
358  // On iOS, opening the campaign browser should not immediately summon the
359  // software keyboard just because the optional filter field exists.
360  keyboard_capture(&tree);
361 #else
363 #endif
364  add_to_keyboard_chain(&tree);
365 
366  /***** Setup campaign details. *****/
367  multi_page& pages = find_widget<multi_page>("campaign_details");
368 
369  // Setup completion filter
370  multimenu_button& filter_comp = find_widget<multimenu_button>("filter_completion");
371  connect_signal_notify_modified(filter_comp,
372  std::bind(&campaign_selection::sort_campaigns, this, RANK, 1));
373  for (unsigned j = 0; j < filter_comp.num_options(); j++) {
374  filter_comp.select_option(j);
375  }
376 
377  // Add campaigns to the list
378  for(const auto& level : engine_.get_levels_by_type_unfiltered(level_type::type::sp_campaign)) {
379  const config& campaign = level->data();
380 
381  /*** Add tree item ***/
382  add_campaign_to_tree(campaign);
383 
384  /*** Add detail item ***/
385  pages.add_page({
386  {"description", {
387  {"label", campaign["description"].t_str()},
388  {"text_alignment", campaign["description_alignment"].str("left")},
389  {"use_markup", "true"}
390  }},
391  {"image", {
392  {"label", campaign["image"].str()}
393  }}
394  });
395  page_ids_.push_back(campaign["id"]);
396  }
397 
398  //
399  // Special-purpose pages that appear as if they were campaigns.
400  //
401 
402  // The intro page is shown if nothing is selected in the tree_view, it isn't in the tree itself
403  pages.add_page("no_campaign_selected", -1, widget_data{});
404  page_ids_.push_back(PAGE_ID_LANDING_PAGE);
405 
406  // Addon Manager link
407  config addons;
408  addons["icon"] = "icons/icon-game.png~BLIT(icons/icon-addon-publish.png)";
409  addons["name"] = _("More campaigns...");
410  addons["completed"] = false;
411  addons["id"] = PAGE_ID_GET_ADDONS;
412 
413  add_campaign_to_tree(addons);
414 
415  pages.add_page("go_download_more_stuff", -1, widget_data{});
416  page_ids_.push_back(PAGE_ID_GET_ADDONS);
417 
418  // The data directory has few (or none) of the mainline campaigns
419  std::vector<std::string> dirs;
420  filesystem::get_files_in_dir(game_config::path + "/data/campaigns", nullptr, &dirs);
421  if(dirs.size() <= 15) {
422  config missing;
423  missing["icon"] = "units/unknown-unit.png";
424  missing["name"] = _("Missing Campaigns");
425  missing["completed"] = false;
426  missing["id"] = PAGE_ID_MISSING_CAMPAIGNS;
427 
429 
430  pages.add_page("missing_campaign_warning", -1, widget_data{});
431  page_ids_.push_back(PAGE_ID_MISSING_CAMPAIGNS);
432  }
433 
434  //
435  // Set up Mods selection dropdown
436  //
437  multimenu_button& mods_menu = find_widget<multimenu_button>("mods_menu");
438 
440  std::vector<config> mod_menu_values;
441  std::vector<std::string> enabled = engine_.active_mods();
442 
444  const bool active = std::find(enabled.begin(), enabled.end(), mod->id) != enabled.end();
445 
446  mod_menu_values.emplace_back("label", mod->name, "tooltip", mod->description, "checkbox", active);
447 
448  mod_states_.push_back(active);
449  mod_ids_.emplace_back(mod->id);
450  }
451 
452  mods_menu.set_values(mod_menu_values);
453  mods_menu.select_options(mod_states_);
454 
456  } else {
457  mods_menu.set_active(false);
458  mods_menu.set_label(_("active_modifications^None"));
459  }
460 
461  //
462  // Set up RNG mode dropdown
463  //
464  // New installs default to "Reduced RNG"; existing installs restore the last selection.
465  menu_button& rng_menu = find_widget<menu_button>("rng_menu");
466  auto rng_it = std::find(rng_mode_prefs.begin(), rng_mode_prefs.end(), prefs::get().campaign_rng_mode());
467  rng_menu.set_selected(rng_it != rng_mode_prefs.end()
468  ? static_cast<unsigned>(std::distance(rng_mode_prefs.begin(), rng_it))
469  : static_cast<unsigned>(RNG_DEFAULT));
470 
471  //
472  // Set up Difficulty dropdown
473  //
474  menu_button& diff_menu = find_widget<menu_button>("difficulty_menu");
475 
476  diff_menu.set_use_markup(true);
478 
480 
481  plugins_context_.reset(new plugins_context("Campaign Selection"));
482  plugins_context_->set_callback("create", [this](const config&) { set_retval(retval::OK); }, false);
483  plugins_context_->set_callback("quit", [this](const config&) { set_retval(retval::CANCEL); }, false);
484 
485  plugins_context_->set_accessor("find_level", [this](const config& cfg) {
486  const std::string id = cfg["id"].str();
487  auto result = engine_.find_level_by_id(id);
488  return config {
489  "index", result.second,
490  "type", level_type::get_string(result.first),
491  };
492  });
493 
494  plugins_context_->set_accessor_int("find_mod", [this](const config& cfg) {
496  });
497 
498  plugins_context_->set_callback("select_level", [this](const config& cfg) {
499  choice_ = cfg["index"].to_int();
501  }, true);
502 }
503 
505 {
506  // We completed the campaign! Calculate the appropriate victory laurel.
507  const auto get_laurel = [&campaign] {
508  if(!campaign["completed"].to_bool()) {
509  return std::string{};
510  }
511 
512  config::const_child_itors difficulties = campaign.child_range("difficulty");
513  auto did_complete_at = [](const config& c) { return c["completed_at"].to_bool(); };
514 
515  // Check for non-completion on every difficulty save the first.
516  const bool only_first_completed = difficulties.size() > 1 &&
517  std::none_of(difficulties.begin() + 1, difficulties.end(), did_complete_at);
518 
519  /*
520  * Criteria:
521  *
522  * - Use the gold laurel (hardest) for campaigns with only one difficulty OR
523  * if out of two or more difficulties, the last one has been completed.
524  *
525  * - Use the bronze laurel (easiest) only if the first difficulty out of two
526  * or more has been completed.
527  *
528  * - Use the silver laurel otherwise.
529  */
530  if(!difficulties.empty() && did_complete_at(difficulties.back())) {
532  } else if(only_first_completed && did_complete_at(difficulties.front())) {
534  } else {
536  }
537  };
538 
539  auto& node = find_widget<tree_view>("campaign_tree")
540  .add_node("campaign", {
541  {"icon", {
542  {"label", campaign["icon"].str()}
543  }},
544  {"name", {
545  {"label", campaign["name"].t_str()}
546  }},
547  {"victory", {
548  {"label", get_laurel()}
549  }}
550  });
551 
552  node.set_id(campaign["id"]);
553 }
554 
556 {
557  if(get_retval() != retval::OK) {
558  return;
559  }
560 
561  tree_view& tree = find_widget<tree_view>("campaign_tree");
562  if(tree.empty() || !tree.selected_item()) {
563  return;
564  }
565 
566  const std::string& campaign_id = tree.selected_item()->id();
567  if(!campaign_id.empty()) {
568  if (campaign_id == PAGE_ID_GET_ADDONS) {
570  } else {
571  auto iter = std::find(page_ids_.begin(), page_ids_.end(), campaign_id);
572  if(iter != page_ids_.end()) {
573  choice_ = std::distance(page_ids_.begin(), iter);
574  }
576  }
577  }
578 
579  rng_mode_ = RNG_MODE(std::clamp<unsigned>(find_widget<menu_button>("rng_menu").get_value(), RNG_DEFAULT, RNG_BIASED));
580  prefs::get().set_campaign_rng_mode(rng_mode_prefs[rng_mode_]);
582 }
583 
585 {
586  boost::dynamic_bitset<> new_mod_states =
587  find_widget<multimenu_button>("mods_menu").get_toggle_states();
588 
589  // Get a mask of any mods that were toggled, regardless of new state
590  mod_states_ = mod_states_ ^ new_mod_states;
591 
592  for(unsigned i = 0; i < mod_states_.size(); i++) {
593  if(mod_states_[i]) {
595  }
596  }
597 
598  // Save the full toggle states for next time
599  mod_states_ = new_mod_states;
600 }
601 
602 } // namespace dialogs
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:157
child_itors child_range(std::string_view key)
Definition: config.cpp:268
boost::iterator_range< const_child_iterator > const_child_itors
Definition: config.hpp:281
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.
virtual void pre_show() override
Actions to be taken before showing the window.
void sort_campaigns(CAMPAIGN_ORDER order, bool ascending)
virtual void post_show() override
Actions to be taken after the window has been shown.
void add_campaign_to_tree(const config &campaign)
void filter_text_changed(const std::string &text)
void campaign_selected()
Called when an item in the left-hand pane (the tree view) 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_
void set_selected(unsigned selected, bool fire_event=true)
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 * selected_item()
Definition: tree_view.hpp:108
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 get_retval()
Definition: window.hpp:394
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:1031
static std::string _(const char *str)
Definition: gettext.hpp:100
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:466
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
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:44
constexpr auto filter
Definition: ranges.hpp:42
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:142
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