The Battle for Wesnoth  1.19.15+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 
108  add_to_keyboard_chain(&list);
109 
110  list.set_sorters(
111  [this](const std::size_t i) { return games_[i].name(); },
112  [this](const std::size_t i) { return games_[i].modified(); }
113  );
114 
116 
117  connect_signal_mouse_left_click(find_widget<button>("delete"),
118  std::bind(&game_load::delete_button_callback, this));
119 
120  connect_signal_mouse_left_click(find_widget<button>("browse_saves_folder"),
121  std::bind(&game_load::browse_button_callback, this));
122 
123  menu_button& dir_list = find_widget<menu_button>("dirList");
124 
125  dir_list.set_use_markup(true);
126  set_save_dir_list(dir_list);
127 
129 
131 }
132 
134 {
135  const auto other_dirs = filesystem::find_other_version_saves_dirs();
136  if(other_dirs.empty()) {
138  return;
139  }
140 
141  std::vector<config> options;
142 
143  // The first option in the list is the current version's save dir
144  options.emplace_back("label", _("game_version^Current Version"), "path", "");
145 
146  for(const auto& known_dir : other_dirs) {
147  options.emplace_back(
148  "label", VGETTEXT("game_version^Wesnoth $version", utils::string_map{{"version", known_dir.version}}),
149  "path", known_dir.path
150  );
151  }
152 
153  dir_list.set_values(options);
154 }
155 
157 {
158  listbox& list = find_widget<listbox>("savegame_list");
159 
160  list.clear();
161 
162  games_ = save_index_manager_->get_saves_list();
163 
164  for(const auto& game : games_) {
165  std::string name = game.name();
166  utils::ellipsis_truncate(name, 40);
167 
168  list.add_row(widget_data{
169  { "filename", {
170  { "label", std::move(name) }
171  }},
172  { "date", {
173  { "label", game.format_time_summary() }
174  }},
175  });
176  }
177 
178  find_widget<button>("delete").set_active(!save_index_manager_->read_only());
179 }
180 
182 {
183  filename_ = game.name();
184  summary_ = game.summary();
185 
186  find_widget<minimap>("minimap")
187  .set_map_data(summary_["map_data"]);
188 
189  find_widget<label>("lblScenario")
190  .set_label(summary_["label"]);
191 
192  listbox& leader_list = find_widget<listbox>("leader_list");
193 
194  leader_list.clear();
195 
196  const std::string sprite_scale_mod = (formatter() << "~SCALE_INTO(" << game_config::tile_size << ',' << game_config::tile_size << ')').str();
197 
198  unsigned li = 0;
199  for(const auto& leader : summary_.child_range("leader")) {
201  widget_item item;
202 
203  // First, we evaluate whether the leader image as provided exists.
204  // If not, we try getting a binary path-independent path. If that still doesn't
205  // work, we fallback on unknown-unit.png.
206  std::string leader_image = leader["leader_image"].str();
207  if(!::image::exists(leader_image)) {
208  auto indep_path = filesystem::get_independent_binary_file_path("images", leader_image);
209 
210  // The leader TC modifier isn't appending if the independent image path can't
211  // be resolved during save_index entry creation, so we need to add it here.
212  if(indep_path) {
213  leader_image = indep_path.value() + leader["leader_image_tc_modifier"].str();
214  }
215  }
216 
217  if(leader_image.empty()) {
218  leader_image = "units/unknown-unit.png" + leader["leader_image_tc_modifier"].str();
219  } else {
220  // Scale down any sprites larger than 72x72
221  leader_image += sprite_scale_mod + "~FL(horiz)";
222  }
223 
224  item["label"] = leader_image;
225  data.emplace("imgLeader", item);
226 
227  item["label"] = leader["leader_name"];
228  data.emplace("leader_name", item);
229 
230  item["label"] = leader["gold"];
231  data.emplace("leader_gold", item);
232 
233  // TRANSLATORS: "reserve" refers to units on the recall list
234  item["label"] = VGETTEXT("$active active, $reserve reserve", {{"active", leader["units"]}, {"reserve", leader["recall_units"]}});
235  data.emplace("leader_troops", item);
236 
237  leader_list.add_row(data);
238 
239  // FIXME: hack. In order to use the listbox in view-only mode, you also need to
240  // disable the max number of "selected items", since in this mode, "selected" is
241  // synonymous with "visible". This basically just flags all rows as visible. Need
242  // a better solution at some point
243  leader_list.select_row(li++, true);
244  }
245 
246  std::stringstream str;
247  str << game.format_time_local() << "\n";
249 
250  // The new label value may have more or less lines than the previous value, so invalidate the layout.
251  find_widget<styled_widget>("slblSummary").set_label(str.str());
252  //invalidate_layout();
253 
254  toggle_button& replay_toggle = dynamic_cast<toggle_button&>(*show_replay_->get_widget());
255  toggle_button& cancel_orders_toggle = dynamic_cast<toggle_button&>(*cancel_orders_->get_widget());
256  toggle_button& change_difficulty_toggle = dynamic_cast<toggle_button&>(*change_difficulty_->get_widget());
257 
258  const bool is_replay = savegame::is_replay_save(summary_);
259  const bool is_scenario_start = summary_["turn"].empty();
260 
261  // Always toggle show_replay on if the save is a replay
262  replay_toggle.set_value(is_replay);
263  replay_toggle.set_active(!is_replay && !is_scenario_start);
264 
265  // Cancel orders doesn't make sense on replay saves or start-of-scenario saves
266  cancel_orders_toggle.set_active(!is_replay && !is_scenario_start);
267 
268  // Changing difficulty doesn't make sense on non-start-of-scenario saves
269  change_difficulty_toggle.set_active(!is_replay && is_scenario_start);
270 }
271 
272 // This is a wrapper that prevents a corrupted save file (if it happens to be
273 // the first in the list) from making the dialog fail to open.
275 {
276  bool successfully_displayed_a_game = false;
277 
278  try {
279  const int selected_row = find_widget<listbox>("savegame_list").get_selected_row();
280  if(selected_row < 0) {
281  find_widget<button>("delete").set_active(false);
282  } else {
283  find_widget<button>("delete").set_active(!save_index_manager_->read_only());
285  successfully_displayed_a_game = true;
286  }
287  } catch(const config::error& e) {
288  // Clear the UI widgets, show an error message.
289  const std::string preamble = _("The selected file is corrupt: ");
290  const std::string message = e.message.empty() ? "(no details)" : e.message;
291  ERR_GAMELOADDLG << preamble << message;
292  }
293 
294  if(!successfully_displayed_a_game) {
295  find_widget<minimap>("minimap").set_map_data("");
296  find_widget<label>("lblScenario")
297  .set_label("");
298  find_widget<styled_widget>("slblSummary")
299  .set_label("");
300 
301  listbox& leader_list = find_widget<listbox>("leader_list");
302  leader_list.clear();
303 
304  toggle_button& replay_toggle = dynamic_cast<toggle_button&>(*show_replay_->get_widget());
305  toggle_button& cancel_orders_toggle = dynamic_cast<toggle_button&>(*cancel_orders_->get_widget());
306  toggle_button& change_difficulty_toggle = dynamic_cast<toggle_button&>(*change_difficulty_->get_widget());
307 
308  replay_toggle.set_active(false);
309  cancel_orders_toggle.set_active(false);
310  change_difficulty_toggle.set_active(false);
311  }
312 
313  // Disable Load button if nothing is selected or if the currently selected file can't be loaded
314  find_widget<button>("ok").set_active(successfully_displayed_a_game);
315 
316  // Disable 'Enter' loading in the same circumstance
317  set_enter_disabled(!successfully_displayed_a_game);
318 }
319 
320 void game_load::apply_filter_text(const std::string& text)
321 {
322  find_widget<listbox>("savegame_list").filter_rows_by(
323  [this, match = translation::make_ci_matcher(text)](std::size_t row) { return match(games_[row].name()); });
324 }
325 
326 void game_load::evaluate_summary_string(std::stringstream& str, const config& cfg_summary)
327 {
328  if(cfg_summary["corrupt"].to_bool()) {
329  str << "\n" << markup::span_color("#f00", _("(Invalid)"));
330  // \todo: this skips the catch() statement in display_savegame. Low priority, as the
331  // dialog's state is reasonable; the "load" button is inactive, the "delete" button is
332  // active, and (cosmetic bug) it leaves the "change difficulty" toggle active. Can be
333  // triggered by creating an empty file in the save directory.
334  return;
335  }
336 
337  const std::string& campaign_type = cfg_summary["campaign_type"];
338  const std::string campaign_id = cfg_summary["campaign"];
339  auto campaign_type_enum = campaign_type::get_enum(campaign_type);
340 
341  if(campaign_type_enum) {
342  switch(*campaign_type_enum) {
343  case campaign_type::type::scenario: {
344  const auto campaign = cache_config_.find_child("campaign", "id", campaign_id);
345  utils::string_map symbols;
346 
347  if(campaign) {
348  symbols["campaign_name"] = (*campaign)["name"];
349  } else {
350  // Fallback to nontranslatable campaign id.
351  symbols["campaign_name"] = "(" + campaign_id + ")";
352  }
353 
354  str << VGETTEXT("Campaign: $campaign_name", symbols);
355 
356  // Display internal id for debug purposes if we didn't above
357  if(game_config::debug && campaign) {
358  str << '\n' << "(" << campaign_id << ")";
359  }
360  break;
361  }
362  case campaign_type::type::multiplayer:
363  str << _("Multiplayer");
364  break;
365  case campaign_type::type::tutorial:
366  str << _("Tutorial");
367  break;
368  case campaign_type::type::test:
369  str << _("Test scenario");
370  break;
371  }
372  } else {
373  str << campaign_type;
374  }
375 
376  str << "\n";
377 
378  if(savegame::is_replay_save(cfg_summary)) {
379  str << _("Replay");
380  } else if(!cfg_summary["turn"].empty()) {
381  str << _("Turn") << " " << cfg_summary["turn"];
382  } else {
383  str << _("Scenario start");
384  }
385 
386  if(campaign_type_enum) {
387  switch (*campaign_type_enum) {
388  case campaign_type::type::scenario:
389  case campaign_type::type::multiplayer: {
390  // 'SCENARIO' or SP should only ever be campaigns
391  // 'MULTIPLAYER' may be a campaign with difficulty or single scenario without difficulty
392  // For the latter do not show the difficulty - even though it will be listed as
393  // NORMAL -> Medium in the save file it should not be considered valid (GitHub Issue #5321)
394  if(auto campaign = cache_config_.find_child("campaign", "id", campaign_id)) {
395  str << "\n" << _("Difficulty: ");
396  try {
397  const config& difficulty = campaign->find_mandatory_child("difficulty", "define", cfg_summary["difficulty"]);
398  std::ostringstream ss;
399  ss << difficulty["label"] << " (" << difficulty["description"] << ")";
400  str << ss.str();
401  }
402  catch (const config::error&) {
403  // fall back to standard difficulty string in case of exception
404  str << string_table[cfg_summary["difficulty"]];
405  }
406  }
407 
408  break;
409  }
410  case campaign_type::type::tutorial:
411  case campaign_type::type::test:
412  break;
413  }
414  } else {
415  }
416 
417  if(!cfg_summary["version"].empty()) {
418  str << "\n" << _("Version: ") << cfg_summary["version"];
419  }
420 
421  const std::vector<std::string>& active_mods = utils::split(cfg_summary["active_mods"]);
422  if(!active_mods.empty()) {
423  str << "\n" << _("Modifications: ");
424  for(const auto& mod_id : active_mods) {
425  std::string mod_name;
426  try {
427  mod_name = cache_config_.find_mandatory_child("modification", "id", mod_id)["name"].str();
428  } catch(const config::error&) {
429  // Fallback to nontranslatable mod id.
430  mod_name = "(" + mod_id + ")";
431  }
432 
433  str << "\n" << font::unicode_bullet << " " << mod_name;
434  }
435  }
436 }
438 {
440 }
441 
443 {
444  listbox& list = find_widget<listbox>("savegame_list");
445 
446  const std::size_t index = std::size_t(list.get_selected_row());
447  if(index < games_.size()) {
448 
449  // See if we should ask the user for deletion confirmation
450  if(prefs::get().ask_delete()) {
451  if(!gui2::dialogs::game_delete::execute()) {
452  return;
453  }
454  }
455 
456  // Delete the file
457  save_index_manager_->delete_game(games_[index].name());
458 
459  // Remove it from the list of saves
460  games_.erase(games_.begin() + index);
461 
462  list.remove_row(index);
463 
465  }
466 }
467 
468 void game_load::key_press_callback(const SDL_Keycode key)
469 {
470  //
471  // Don't delete games when we're typing in the textbox!
472  //
473  // I'm not sure if this check was necessary when I first added this feature
474  // (I didn't check at the time), but regardless, it's needed now. If it turns
475  // out I screwed something up in my refactoring, I'll remove
476  //
477  // - vultraz, 2017-08-28
478  //
479  if(find_widget<text_box>("txtFilter").get_state() == text_box_base::FOCUSED) {
480  return;
481  }
482 
483  if(key == SDLK_DELETE) {
485  }
486 }
487 
489 {
490  menu_button& dir_list = find_widget<menu_button>("dirList");
491 
492  const auto& path = dir_list.get_value_config()["path"].str();
493  if(path.empty()) {
495  } else {
496  save_index_manager_ = std::make_shared<savegame::save_index_class>(path);
497  }
498 
500  if(auto* filter = find_widget<text_box>("txtFilter", false, true)) {
501  apply_filter_text(filter->get_value());
502  }
504 }
505 
506 } // 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:158
config & find_mandatory_child(config_key_type key, const std::string &name, const std::string &value)
Definition: config.cpp:800
child_itors child_range(config_key_type key)
Definition: config.cpp:268
bool empty() const
Definition: config.cpp:839
std::ostringstream wrapper.
Definition: formatter.hpp:40
optional_const_config find_child(config_key_type key, const std::string &name, const std::string &value) const
const config & find_mandatory_child(config_key_type 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:181
void apply_filter_text(const std::string &text)
Hides saves not matching the given filter.
Definition: game_load.cpp:320
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:468
void evaluate_summary_string(std::stringstream &str, const config &cfg_summary)
Definition: game_load.cpp:326
void set_save_dir_list(menu_button &dir_list)
Definition: game_load.cpp:133
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:156
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:289
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:1032
#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:218
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:911
const std::string unicode_bullet
Definition: constants.cpp:47
Game configuration data as global variables.
Definition: build_info.cpp:61
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:840
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:38
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