The Battle for Wesnoth  1.19.22+dev
game_load.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2008 - 2025
3  by Jörg Hinrichs <joerg.hinrichs@alice-dsl.de>
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 "desktop/open.hpp"
21 #include "filesystem.hpp"
22 #include "formatter.hpp"
23 #include "formula/string_utils.hpp"
24 #include "game_config.hpp"
25 #include "game_config_manager.hpp"
26 #include "gettext.hpp"
27 #include "gui/auxiliary/field.hpp"
29 #include "gui/widgets/button.hpp"
30 #include "gui/widgets/label.hpp"
31 #include "gui/widgets/listbox.hpp"
32 #include "gui/widgets/minimap.hpp"
34 #include "gui/widgets/text_box.hpp"
36 #include "gui/widgets/window.hpp"
37 #include "language.hpp"
38 #include "picture.hpp"
41 #include "serialization/markup.hpp"
42 #include "utils/general.hpp"
43 #include "utils/ci_searcher.hpp"
44 #include "game_config_view.hpp"
45 
46 #include <functional>
47 
48 
49 static lg::log_domain log_gameloaddlg{"gui/dialogs/game_load_dialog"};
50 #define ERR_GAMELOADDLG LOG_STREAM(err, log_gameloaddlg)
51 #define WRN_GAMELOADDLG LOG_STREAM(warn, log_gameloaddlg)
52 #define LOG_GAMELOADDLG LOG_STREAM(info, log_gameloaddlg)
53 #define DBG_GAMELOADDLG LOG_STREAM(debug, log_gameloaddlg)
54 
55 namespace gui2::dialogs
56 {
57 
58 REGISTER_DIALOG(game_load)
59 
60 bool game_load::execute(savegame::load_game_metadata& data)
61 {
62  if(savegame::save_index_class::default_saves_dir()->get_saves_list().empty()) {
63  bool found_files = false;
64  for(const auto& dir : filesystem::find_other_version_saves_dirs()) {
65  if(!found_files) {
66  // this needs to be a shared_ptr because get_saves_list() uses shared_from_this
67  auto index = std::make_shared<savegame::save_index_class>(dir.path);
68  found_files = !index->get_saves_list().empty();
69  }
70  }
71 
72  if(!found_files) {
73  gui2::show_transient_message(_("No Saved Games"), _("There are no saved games to load."));
74  return false;
75  }
76  }
77 
78  return game_load(data).show();
79 }
80 
82  : modal_dialog(window_id())
84  , save_index_manager_(data.manager)
85  , change_difficulty_(register_bool("change_difficulty", true, data.select_difficulty))
86  , show_replay_(register_bool("show_replay", true, data.show_replay))
87  , cancel_orders_(register_bool("cancel_orders", true, data.cancel_orders))
88  , summary_(data.summary)
89  , games_()
90  , cache_config_(game_config_manager::get()->game_config())
91 {
92 }
93 
95 {
96  // Allow deleting saves with the Delete key.
97  connect_signal_pre_key_press(*this, std::bind(&game_load::key_press_callback, this, std::placeholders::_5));
98 
99  text_box* filter = find_widget<text_box>("txtFilter", false, true);
100 
101  filter->on_modified([this](const auto& box) { apply_filter_text(box.text()); });
102 
103  listbox& list = find_widget<listbox>("savegame_list");
104 
106 
107 #ifdef __IPHONEOS__
108  // On iOS, opening the load dialog should not immediately summon the
109  // software keyboard just because the optional filter field exists.
110  keyboard_capture(&list);
111 #else
113 #endif
114  add_to_keyboard_chain(&list);
115 
116  list.set_sorters(
117  [this](const std::size_t i) { return games_[i].name(); },
118  [this](const std::size_t i) { return games_[i].modified(); }
119  );
120 
122 
123  connect_signal_mouse_left_click(find_widget<button>("delete"),
124  std::bind(&game_load::delete_button_callback, this));
125 
126  connect_signal_mouse_left_click(find_widget<button>("browse_saves_folder"),
127  std::bind(&game_load::browse_button_callback, this));
128 
129  menu_button& dir_list = find_widget<menu_button>("dirList");
130 
131  dir_list.set_use_markup(true);
132  set_save_dir_list(dir_list);
133 
135 
137 }
138 
140 {
141  const auto other_dirs = filesystem::find_other_version_saves_dirs();
142  if(other_dirs.empty()) {
144  return;
145  }
146 
147  std::vector<config> options;
148 
149  // The first option in the list is the current version's save dir
150  options.emplace_back("label", _("game_version^Current Version"), "path", "");
151 
152  for(const auto& known_dir : other_dirs) {
153  options.emplace_back(
154  "label", VGETTEXT("game_version^Wesnoth $version", utils::string_map{{"version", known_dir.version}}),
155  "path", known_dir.path
156  );
157  }
158 
159  dir_list.set_values(options);
160 }
161 
163 {
164  listbox& list = find_widget<listbox>("savegame_list");
165 
166  list.clear();
167 
168  games_ = save_index_manager_->get_saves_list();
169 
170  for(const auto& game : games_) {
171  std::string name = game.name();
172  utils::ellipsis_truncate(name, 40);
173 
174  list.add_row(widget_data{
175  { "filename", {
176  { "label", std::move(name) }
177  }},
178  { "date", {
179  { "label", game.format_time_summary() }
180  }},
181  });
182  }
183 
184  find_widget<button>("delete").set_active(!save_index_manager_->read_only());
185 }
186 
188 {
189  filename_ = game.name();
190  summary_ = game.summary();
191 
192  find_widget<minimap>("minimap")
193  .set_map_data(summary_["map_data"]);
194 
195  find_widget<label>("lblScenario")
196  .set_label(summary_["label"].t_str());
197 
198  listbox& leader_list = find_widget<listbox>("leader_list");
199 
200  leader_list.clear();
201 
202  const std::string sprite_scale_mod = (formatter() << "~SCALE_INTO(" << game_config::tile_size << ',' << game_config::tile_size << ')').str();
203 
204  unsigned li = 0;
205  for(const auto& leader : summary_.child_range("leader")) {
207  widget_item item;
208 
209  // First, we evaluate whether the leader image as provided exists.
210  // If not, we try getting a binary path-independent path. If that still doesn't
211  // work, we fallback on unknown-unit.png.
212  std::string leader_image = leader["leader_image"].str();
213  if(!::image::exists(leader_image)) {
214  auto indep_path = filesystem::get_independent_binary_file_path("images", leader_image);
215 
216  // The leader TC modifier isn't appending if the independent image path can't
217  // be resolved during save_index entry creation, so we need to add it here.
218  if(indep_path) {
219  leader_image = indep_path.value() + leader["leader_image_tc_modifier"].str();
220  }
221  }
222 
223  if(leader_image.empty()) {
224  leader_image = "units/unknown-unit.png" + leader["leader_image_tc_modifier"].str();
225  } else {
226  // Scale down any sprites larger than 72x72
227  leader_image += sprite_scale_mod + "~FL(horiz)";
228  }
229 
230  item["label"] = leader_image;
231  data.emplace("imgLeader", item);
232 
233  item["label"] = leader["leader_name"].t_str();
234  data.emplace("leader_name", item);
235 
236  item["label"] = leader["gold"].str();
237  data.emplace("leader_gold", item);
238 
239  // TRANSLATORS: "reserve" refers to units on the recall list
240  item["label"] = VGETTEXT("$active active, $reserve reserve", {{"active", leader["units"].str()}, {"reserve", leader["recall_units"].str()}});
241  data.emplace("leader_troops", item);
242 
243  leader_list.add_row(data);
244 
245  // FIXME: hack. In order to use the listbox in view-only mode, you also need to
246  // disable the max number of "selected items", since in this mode, "selected" is
247  // synonymous with "visible". This basically just flags all rows as visible. Need
248  // a better solution at some point
249  leader_list.select_row(li++, true);
250  }
251 
252  std::stringstream str;
253  str << game.format_time_local() << "\n";
255 
256  // The new label value may have more or less lines than the previous value, so invalidate the layout.
257  find_widget<styled_widget>("slblSummary").set_label(str.str());
258  //invalidate_layout();
259 
260  toggle_button& replay_toggle = dynamic_cast<toggle_button&>(*show_replay_->get_widget());
261  toggle_button& cancel_orders_toggle = dynamic_cast<toggle_button&>(*cancel_orders_->get_widget());
262  toggle_button& change_difficulty_toggle = dynamic_cast<toggle_button&>(*change_difficulty_->get_widget());
263 
264  const bool is_replay = savegame::is_replay_save(summary_);
265  const bool is_scenario_start = summary_["turn"].empty();
266 
267  // Always toggle show_replay on if the save is a replay
268  replay_toggle.set_value(is_replay);
269  replay_toggle.set_active(!is_replay && !is_scenario_start);
270 
271  // Cancel orders doesn't make sense on replay saves or start-of-scenario saves
272  cancel_orders_toggle.set_active(!is_replay && !is_scenario_start);
273 
274  // Changing difficulty doesn't make sense on non-start-of-scenario saves
275  change_difficulty_toggle.set_active(!is_replay && is_scenario_start);
276 }
277 
278 // This is a wrapper that prevents a corrupted save file (if it happens to be
279 // the first in the list) from making the dialog fail to open.
281 {
282  bool successfully_displayed_a_game = false;
283 
284  try {
285  const int selected_row = find_widget<listbox>("savegame_list").get_selected_row();
286  if(selected_row < 0) {
287  find_widget<button>("delete").set_active(false);
288  } else {
289  find_widget<button>("delete").set_active(!save_index_manager_->read_only());
291  successfully_displayed_a_game = true;
292  }
293  } catch(const config::error& e) {
294  // Clear the UI widgets, show an error message.
295  const std::string preamble = _("The selected file is corrupt: ");
296  const std::string message = e.message.empty() ? "(no details)" : e.message;
297  ERR_GAMELOADDLG << preamble << message;
298  }
299 
300  if(!successfully_displayed_a_game) {
301  find_widget<minimap>("minimap").set_map_data("");
302  find_widget<label>("lblScenario")
303  .set_label("");
304  find_widget<styled_widget>("slblSummary")
305  .set_label("");
306 
307  listbox& leader_list = find_widget<listbox>("leader_list");
308  leader_list.clear();
309 
310  toggle_button& replay_toggle = dynamic_cast<toggle_button&>(*show_replay_->get_widget());
311  toggle_button& cancel_orders_toggle = dynamic_cast<toggle_button&>(*cancel_orders_->get_widget());
312  toggle_button& change_difficulty_toggle = dynamic_cast<toggle_button&>(*change_difficulty_->get_widget());
313 
314  replay_toggle.set_active(false);
315  cancel_orders_toggle.set_active(false);
316  change_difficulty_toggle.set_active(false);
317  }
318 
319  // Disable Load button if nothing is selected or if the currently selected file can't be loaded
320  find_widget<button>("ok").set_active(successfully_displayed_a_game);
321 
322  // Disable 'Enter' loading in the same circumstance
323  set_enter_disabled(!successfully_displayed_a_game);
324 }
325 
326 void game_load::apply_filter_text(const std::string& text)
327 {
328  find_widget<listbox>("savegame_list").filter_rows_by(
329  [this, match = translation::make_ci_matcher(text)](std::size_t row) { return match(games_[row].name()); });
330 }
331 
332 void game_load::evaluate_summary_string(std::stringstream& str, const config& cfg_summary)
333 {
334  if(cfg_summary["corrupt"].to_bool()) {
335  str << "\n" << markup::span_color("#f00", _("(Invalid)"));
336  // \todo: this skips the catch() statement in display_savegame. Low priority, as the
337  // dialog's state is reasonable; the "load" button is inactive, the "delete" button is
338  // active, and (cosmetic bug) it leaves the "change difficulty" toggle active. Can be
339  // triggered by creating an empty file in the save directory.
340  return;
341  }
342 
343  const std::string& campaign_type = cfg_summary["campaign_type"];
344  const std::string campaign_id = cfg_summary["campaign"];
345  auto campaign_type_enum = campaign_type::get_enum(campaign_type);
346 
347  if(campaign_type_enum) {
348  switch(*campaign_type_enum) {
349  case campaign_type::type::scenario: {
350  const auto campaign = cache_config_.find_child("campaign", "id", campaign_id);
351  utils::string_map symbols;
352 
353  if(campaign) {
354  symbols["campaign_name"] = (*campaign)["name"].t_str();
355  } else {
356  // Fallback to nontranslatable campaign id.
357  symbols["campaign_name"] = "(" + campaign_id + ")";
358  }
359 
360  str << VGETTEXT("Campaign: $campaign_name", symbols);
361 
362  // Display internal id for debug purposes if we didn't above
363  if(game_config::debug && campaign) {
364  str << '\n' << "(" << campaign_id << ")";
365  }
366  break;
367  }
368  case campaign_type::type::multiplayer:
369  str << _("Multiplayer");
370  break;
371  case campaign_type::type::tutorial:
372  str << _("Tutorial");
373  break;
374  case campaign_type::type::test:
375  str << _("Test scenario");
376  break;
377  }
378  } else {
379  str << campaign_type;
380  }
381 
382  str << "\n";
383 
384  if(savegame::is_replay_save(cfg_summary)) {
385  str << _("Replay");
386  } else if(!cfg_summary["turn"].empty()) {
387  str << _("Turn") << " " << cfg_summary["turn"];
388  } else {
389  str << _("Scenario start");
390  }
391 
392  if(campaign_type_enum) {
393  switch (*campaign_type_enum) {
394  case campaign_type::type::scenario:
395  case campaign_type::type::multiplayer: {
396  // 'SCENARIO' or SP should only ever be campaigns
397  // 'MULTIPLAYER' may be a campaign with difficulty or single scenario without difficulty
398  // For the latter do not show the difficulty - even though it will be listed as
399  // NORMAL -> Medium in the save file it should not be considered valid (GitHub Issue #5321)
400  if(auto campaign = cache_config_.find_child("campaign", "id", campaign_id)) {
401  str << "\n" << _("Difficulty: ");
402  try {
403  const config& difficulty = campaign->find_mandatory_child("difficulty", "define", cfg_summary["difficulty"]);
404  std::ostringstream ss;
405  ss << difficulty["label"] << " (" << difficulty["description"] << ")";
406  str << ss.str();
407  }
408  catch (const config::error&) {
409  // fall back to standard difficulty string in case of exception
410  str << string_table[cfg_summary["difficulty"]];
411  }
412  }
413 
414  break;
415  }
416  case campaign_type::type::tutorial:
417  case campaign_type::type::test:
418  break;
419  }
420  } else {
421  }
422 
423  if(!cfg_summary["version"].empty()) {
424  str << "\n" << _("Version: ") << cfg_summary["version"];
425  }
426 
427  const std::vector<std::string>& active_mods = utils::split(cfg_summary["active_mods"]);
428  if(!active_mods.empty()) {
429  str << "\n" << _("Modifications: ");
430  for(const auto& mod_id : active_mods) {
431  std::string mod_name;
432  try {
433  mod_name = cache_config_.find_mandatory_child("modification", "id", mod_id)["name"].str();
434  } catch(const config::error&) {
435  // Fallback to nontranslatable mod id.
436  mod_name = "(" + mod_id + ")";
437  }
438 
439  str << "\n" << font::unicode_bullet << " " << mod_name;
440  }
441  }
442 }
444 {
446 }
447 
449 {
450  listbox& list = find_widget<listbox>("savegame_list");
451 
452  const std::size_t index = std::size_t(list.get_selected_row());
453  if(index < games_.size()) {
454 
455  // See if we should ask the user for deletion confirmation
456  if(prefs::get().ask_delete()) {
457  if(!gui2::dialogs::game_delete::execute()) {
458  return;
459  }
460  }
461 
462  // Delete the file
463  save_index_manager_->delete_game(games_[index].name());
464 
465  // Remove it from the list of saves
466  games_.erase(games_.begin() + index);
467 
468  list.remove_row(index);
469 
471  }
472 }
473 
474 void game_load::key_press_callback(const SDL_Keycode key)
475 {
476  //
477  // Don't delete games when we're typing in the textbox!
478  //
479  // I'm not sure if this check was necessary when I first added this feature
480  // (I didn't check at the time), but regardless, it's needed now. If it turns
481  // out I screwed something up in my refactoring, I'll remove
482  //
483  // - vultraz, 2017-08-28
484  //
485  if(find_widget<text_box>("txtFilter").get_state() == text_box_base::FOCUSED) {
486  return;
487  }
488 
489  if(key == SDLK_DELETE) {
491  }
492 }
493 
495 {
496  menu_button& dir_list = find_widget<menu_button>("dirList");
497 
498  const auto& path = dir_list.get_value_config()["path"].str();
499  if(path.empty()) {
501  } else {
502  save_index_manager_ = std::make_shared<savegame::save_index_class>(path);
503  }
504 
506  if(auto* filter = find_widget<text_box>("txtFilter", false, true)) {
507  apply_filter_text(filter->get_value());
508  }
510 }
511 
512 } // namespace dialogs
std::string filename_
Definition: action_wml.cpp:534
string_enums::enum_base< campaign_type_defines > campaign_type
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:157
config & find_mandatory_child(std::string_view key, const std::string &name, const std::string &value)
Definition: config.cpp:784
child_itors child_range(std::string_view key)
Definition: config.cpp:268
bool empty() const
Definition: config.cpp:823
std::ostringstream wrapper.
Definition: formatter.hpp:40
const config & find_mandatory_child(std::string_view key, const std::string &name, const std::string &value) const
optional_const_config find_child(std::string_view key, const std::string &name, const std::string &value) const
void display_savegame_internal(const savegame::save_info &game)
Part of display_savegame that might throw a config::error if the savegame data is corrupt.
Definition: game_load.cpp:187
void apply_filter_text(const std::string &text)
Hides saves not matching the given filter.
Definition: game_load.cpp:326
field_bool * show_replay_
Definition: game_load.hpp:67
virtual void pre_show() override
Actions to be taken before showing the window.
Definition: game_load.cpp:94
void key_press_callback(const SDL_Keycode key)
Definition: game_load.cpp:474
void evaluate_summary_string(std::stringstream &str, const config &cfg_summary)
Definition: game_load.cpp:332
void set_save_dir_list(menu_button &dir_list)
Definition: game_load.cpp:139
field_bool * cancel_orders_
Definition: game_load.hpp:68
const game_config_view & cache_config_
Definition: game_load.hpp:73
std::shared_ptr< savegame::save_index_class > & save_index_manager_
Definition: game_load.hpp:64
std::string & filename_
Definition: game_load.hpp:63
std::vector< savegame::save_info > games_
Definition: game_load.hpp:72
void populate_game_list()
Update (both internally and visually) the list of games.
Definition: game_load.cpp:162
game_load(savegame::load_game_metadata &data)
Definition: game_load.cpp:81
field_bool * change_difficulty_
Definition: game_load.hpp:66
Main class to show messages to the user.
Definition: message.hpp:36
Abstract base class for all modal dialogs.
bool show(const unsigned auto_close_time=0)
Shows the window.
styled_widget * get_widget()
Definition: field.hpp:192
void set_active(const bool active)
Activates all children.
Definition: grid.cpp:167
The listbox class.
Definition: listbox.hpp:41
grid & add_row(const widget_item &item, const int index=-1)
When an item in the list is selected by the user we need to update the state.
Definition: listbox.cpp:92
void set_sorters(Args &&... functors)
Registers sorting controls using magic index IDs.
Definition: listbox.hpp:306
bool select_row(const unsigned row, const bool select=true)
Selects a row.
Definition: listbox.cpp:267
void remove_row(const unsigned row, unsigned count=1)
Removes a row in the listbox.
Definition: listbox.cpp:108
void clear()
Removes all the rows in the listbox, clearing it.
Definition: listbox.cpp:147
int get_selected_row() const
Returns the first selected row.
Definition: listbox.cpp:290
const ::config & get_value_config() const
Returns the entire config object for the selected row.
Definition: menu_button.hpp:70
void set_values(const std::vector<::config > &values, unsigned selected=0)
virtual unsigned get_state() const override
See styled_widget::get_state.
Definition: panel.cpp:61
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
virtual void set_value(unsigned selected, bool fire_event=false) override
Inherited from selectable_item.
virtual void set_active(const bool active) override
See styled_widget::set_active.
void set_visible(const visibility visible)
Definition: widget.cpp:479
@ invisible
The user set the widget invisible, that means:
void set_enter_disabled(const bool enter_disabled)
Disable the enter key.
Definition: window.hpp:313
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
static prefs & get()
static std::shared_ptr< save_index_class > default_saves_dir()
Returns an instance for managing saves in filesystem::get_saves_dir()
Definition: save_index.cpp:202
Filename and modification date for a file list.
Definition: save_index.hpp:28
Implements some helper classes to ease adding fields to a dialog and hide the synchronization needed.
Declarations for File-IO.
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
std::size_t i
Definition: function.cpp:1031
#define ERR_GAMELOADDLG
Definition: game_load.cpp:50
static lg::log_domain log_gameloaddlg
Definition: game_load.cpp:49
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...
auto string_table
Definition: language.hpp:68
CURSOR_TYPE get()
Definition: cursor.cpp:206
bool open_object([[maybe_unused]] const std::string &path_or_url)
Definition: open.cpp:50
utils::optional< std::string > get_independent_binary_file_path(const std::string &type, const std::string &filename)
Returns an asset path to filename for binary path-independent use in saved games.
std::vector< other_version_dir > find_other_version_saves_dirs()
Searches for directories containing saves created by other versions of Wesnoth.
Definition: filesystem.cpp:914
const std::string unicode_bullet
Definition: constants.cpp:47
Game configuration data as global variables.
Definition: build_info.cpp:67
std::string path
Definition: filesystem.cpp:106
const bool & debug
Definition: game_config.cpp:95
unsigned int tile_size
Definition: game_config.cpp:55
REGISTER_DIALOG(editor_edit_unit)
void connect_signal_pre_key_press(dispatcher &dispatcher, const signal_keyboard &signal)
Connects the signal for 'snooping' on the keypress.
Definition: dispatcher.cpp:158
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
std::map< std::string, widget_item > widget_data
Definition: widget.hpp:36
std::map< std::string, t_string > widget_item
Definition: widget.hpp:33
void show_transient_message(const std::string &title, const std::string &message, const std::string &image, const bool message_use_markup, const bool title_use_markup)
Shows a transient message to the user.
bool exists(const image::locator &i_locator)
Returns true if the given image actually exists, without loading it.
Definition: picture.cpp:851
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 is_replay_save(const config &summary)
Definition: savegame.hpp:103
auto make_ci_matcher(std::string_view filter_text)
Returns a function which performs locale-aware case-insensitive search.
Definition: ci_searcher.hpp:24
std::size_t index(std::string_view str, const std::size_t index)
Codepoint index corresponding to the nth character in a UTF-8 string.
Definition: unicode.cpp:70
constexpr auto filter
Definition: ranges.hpp:42
void ellipsis_truncate(std::string &str, const std::size_t size)
Truncates a string to a given utf-8 character count and then appends an ellipsis.
std::map< std::string, t_string > string_map
std::vector< std::string > split(const config_attribute_value &val)
Desktop environment interaction functions.
std::string_view data
Definition: picture.cpp:188
std::string filename
Filename.
The base template for associating string values with enum values.
Definition: enum_base.hpp:33
static constexpr utils::optional< enum_type > get_enum(const std::string_view value)
Converts a string into its enum equivalent.
Definition: enum_base.hpp:57
#define e