The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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"
24#include "game_config.hpp"
26#include "gettext.hpp"
30#include "gui/widgets/label.hpp"
37#include "language.hpp"
38#include "picture.hpp"
42#include "utils/general.hpp"
43#include "utils/ci_searcher.hpp"
44#include "game_config_view.hpp"
45
46#include <functional>
47
48
49static 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
55namespace gui2::dialogs
56{
57
58REGISTER_DIALOG(game_load)
59
60bool 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
105 connect_signal_notify_modified(list, std::bind(&game_load::display_savegame, this));
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
112 keyboard_capture(filter);
113#endif
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
134 connect_signal_notify_modified(dir_list, std::bind(&game_load::handle_dir_select, this));
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
326void 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
332void 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}
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
474void 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_
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.
void apply_filter_text(const std::string &text)
Hides saves not matching the given filter.
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)
void evaluate_summary_string(std::stringstream &str, const config &cfg_summary)
void set_save_dir_list(menu_button &dir_list)
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.
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
message(const std::string &title, const std::string &message, const bool auto_close, const bool message_use_markup, const bool title_use_markup)
Definition message.hpp:40
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
void set_values(const std::vector<::config > &values, unsigned selected=0)
const ::config & get_value_config() const
Returns the entire config object for the selected row.
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()
Filename and modification date for a file list.
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.
#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:100
This file contains the window object, this object is a top level container which has the event manage...
auto string_table
Definition language.hpp:81
#define REGISTER_DIALOG(window_id)
Wrapper for REGISTER_DIALOG2.
bool open_object(const std::string &path_or_url)
Opens the specified object with the default application configured for its type.
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.
const std::string unicode_bullet
Definition constants.cpp:47
Game configuration data as global variables.
const bool & debug
unsigned int tile_size
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:855
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.
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 i
#define e