From 65cfa378b57401d47a867fb648209cac029ab987 Mon Sep 17 00:00:00 2001 From: Francisco Gama Franco Soares Martins Date: Thu, 28 May 2026 11:44:36 +0100 Subject: [PATCH] Add global statistics screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement a global statistics view with menu integration and persisted aggregation for worldmap and levelset progress. For example, coins collected in a completed level now appear in the global totals instead of showing zero. Co-authored-by: João Moura --- CMakeLists.txt | 4 +- data/locale/messages.pot | 49 +++ data/locale/pt.po | 49 +++ data/locale/pt_BR.po | 49 +++ src/supertux/game_session.cpp | 2 +- src/supertux/global_stats_manager.cpp | 174 +++++++++ src/supertux/global_stats_manager.hpp | 55 +++ src/supertux/levelset_screen.cpp | 28 +- src/supertux/levelset_screen.hpp | 9 +- src/supertux/menu/global_stats_menu.cpp | 63 ++++ src/supertux/menu/global_stats_menu.hpp | 37 ++ src/supertux/menu/main_menu.cpp | 1 + src/supertux/menu/main_menu.hpp | 1 + src/supertux/menu/menu_storage.cpp | 4 + src/supertux/menu/menu_storage.hpp | 1 + src/supertux/savegame.cpp | 112 +++++- src/supertux/savegame.hpp | 20 +- src/supertux/screen/global_stats_screen.cpp | 394 ++++++++++++++++++++ src/supertux/screen/global_stats_screen.hpp | 79 ++++ src/worldmap/worldmap.cpp | 11 +- src/worldmap/worldmap.hpp | 3 +- src/worldmap/worldmap_state.cpp | 33 +- src/worldmap/worldmap_state.hpp | 2 +- 23 files changed, 1154 insertions(+), 26 deletions(-) create mode 100644 src/supertux/global_stats_manager.cpp create mode 100644 src/supertux/global_stats_manager.hpp create mode 100644 src/supertux/menu/global_stats_menu.cpp create mode 100644 src/supertux/menu/global_stats_menu.hpp create mode 100644 src/supertux/screen/global_stats_screen.cpp create mode 100644 src/supertux/screen/global_stats_screen.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d9a110b0b79..21515f75e20 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -288,8 +288,8 @@ include(SuperTux/BuildMessagePot) ## Build list of sources for supertux binary set(SUPERTUX_SOURCES_C ${CMAKE_CURRENT_SOURCE_DIR}) -file(GLOB SUPERTUX_SOURCES_CXX RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} src/*/*.cpp src/supertux/menu/*.cpp src/video/sdl/*.cpp src/video/null/*.cpp) -file(GLOB SUPERTUX_SOURCES_HXX RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} src/*/*.hpp src/supertux/menu/*.hpp src/video/sdl/*.hpp src/video/null/*.hpp) +file(GLOB SUPERTUX_SOURCES_CXX RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} src/*/*.cpp src/supertux/menu/*.cpp src/supertux/screen/*.cpp src/video/sdl/*.cpp src/video/null/*.cpp) +file(GLOB SUPERTUX_SOURCES_HXX RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} src/*/*.hpp src/supertux/menu/*.hpp src/supertux/screen/*.hpp src/video/sdl/*.hpp src/video/null/*.hpp) file(GLOB SUPERTUX_RESOURCES RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} "${PROJECT_BINARY_DIR}/tmp/*.rc") if(HAVE_OPENGL) diff --git a/data/locale/messages.pot b/data/locale/messages.pot index 8408c34fb73..34719fc268f 100644 --- a/data/locale/messages.pot +++ b/data/locale/messages.pot @@ -5577,6 +5577,55 @@ msgstr "" msgid "Best" msgstr "" +#: src/supertux/menu/main_menu.cpp:56 +msgid "Statistics" +msgstr "" + +#: src/supertux/menu/global_stats_menu.cpp:33 +#: src/supertux/screen/global_stats_screen.cpp:129 +msgid "Global Statistics" +msgstr "" + +#: src/supertux/menu/global_stats_menu.cpp:35 +msgid "View Statistics" +msgstr "" + +#: src/supertux/screen/global_stats_screen.cpp:140 +msgid "Loading statistics..." +msgstr "" + +#: src/supertux/screen/global_stats_screen.cpp:146 +msgid "Please wait a moment" +msgstr "" + +#: src/supertux/screen/global_stats_screen.cpp:152 +msgid "Coins collected" +msgstr "" + +#: src/supertux/screen/global_stats_screen.cpp:153 +msgid "Secrets found" +msgstr "" + +#: src/supertux/screen/global_stats_screen.cpp:154 +msgid "Tux Dolls collected" +msgstr "" + +#: src/supertux/screen/global_stats_screen.cpp:155 +msgid "Levels completed" +msgstr "" + +#: src/supertux/screen/global_stats_screen.cpp:156 +msgid "Perfect levels" +msgstr "" + +#: src/supertux/screen/global_stats_screen.cpp:157 +msgid "Total play time" +msgstr "" + +#: src/supertux/screen/global_stats_screen.cpp:162 +msgid "Press jump, action, start or escape to return" +msgstr "" + #: src/supertux/tile_set.cpp:139 msgid "Others" msgstr "" diff --git a/data/locale/pt.po b/data/locale/pt.po index 66c0feb16a1..33094ef839e 100644 --- a/data/locale/pt.po +++ b/data/locale/pt.po @@ -5574,6 +5574,55 @@ msgstr "Tu" msgid "Best" msgstr "Recorde" +#: src/supertux/menu/main_menu.cpp:56 +msgid "Statistics" +msgstr "Estatísticas" + +#: src/supertux/menu/global_stats_menu.cpp:33 +#: src/supertux/screen/global_stats_screen.cpp:129 +msgid "Global Statistics" +msgstr "Estatísticas globais" + +#: src/supertux/menu/global_stats_menu.cpp:35 +msgid "View Statistics" +msgstr "Ver estatísticas" + +#: src/supertux/screen/global_stats_screen.cpp:140 +msgid "Loading statistics..." +msgstr "A carregar estatísticas..." + +#: src/supertux/screen/global_stats_screen.cpp:146 +msgid "Please wait a moment" +msgstr "Aguarda um momento" + +#: src/supertux/screen/global_stats_screen.cpp:152 +msgid "Coins collected" +msgstr "Moedas apanhadas" + +#: src/supertux/screen/global_stats_screen.cpp:153 +msgid "Secrets found" +msgstr "Segredos encontrados" + +#: src/supertux/screen/global_stats_screen.cpp:154 +msgid "Tux Dolls collected" +msgstr "Bonecos do Tux apanhados" + +#: src/supertux/screen/global_stats_screen.cpp:155 +msgid "Levels completed" +msgstr "Níveis concluídos" + +#: src/supertux/screen/global_stats_screen.cpp:156 +msgid "Perfect levels" +msgstr "Níveis perfeitos" + +#: src/supertux/screen/global_stats_screen.cpp:157 +msgid "Total play time" +msgstr "Tempo total de jogo" + +#: src/supertux/screen/global_stats_screen.cpp:162 +msgid "Press jump, action, start or escape to return" +msgstr "Prime saltar, ação, iniciar ou Escape para voltar" + #: src/supertux/tile_set.cpp:139 msgid "Others" msgstr "Outros" diff --git a/data/locale/pt_BR.po b/data/locale/pt_BR.po index e8717a7cf7d..df7679c5f80 100644 --- a/data/locale/pt_BR.po +++ b/data/locale/pt_BR.po @@ -5567,6 +5567,55 @@ msgstr "Você" msgid "Best" msgstr "Melhor" +#: src/supertux/menu/main_menu.cpp:56 +msgid "Statistics" +msgstr "Estatísticas" + +#: src/supertux/menu/global_stats_menu.cpp:33 +#: src/supertux/screen/global_stats_screen.cpp:129 +msgid "Global Statistics" +msgstr "Estatísticas Globais" + +#: src/supertux/menu/global_stats_menu.cpp:35 +msgid "View Statistics" +msgstr "Ver Estatísticas" + +#: src/supertux/screen/global_stats_screen.cpp:140 +msgid "Loading statistics..." +msgstr "Carregando estatísticas..." + +#: src/supertux/screen/global_stats_screen.cpp:146 +msgid "Please wait a moment" +msgstr "Aguarde um momento" + +#: src/supertux/screen/global_stats_screen.cpp:152 +msgid "Coins collected" +msgstr "Moedas coletadas" + +#: src/supertux/screen/global_stats_screen.cpp:153 +msgid "Secrets found" +msgstr "Segredos encontrados" + +#: src/supertux/screen/global_stats_screen.cpp:154 +msgid "Tux Dolls collected" +msgstr "Bonecos do Tux coletados" + +#: src/supertux/screen/global_stats_screen.cpp:155 +msgid "Levels completed" +msgstr "Níveis concluídos" + +#: src/supertux/screen/global_stats_screen.cpp:156 +msgid "Perfect levels" +msgstr "Níveis perfeitos" + +#: src/supertux/screen/global_stats_screen.cpp:157 +msgid "Total play time" +msgstr "Tempo total de jogo" + +#: src/supertux/screen/global_stats_screen.cpp:162 +msgid "Press jump, action, start or escape to return" +msgstr "Pressione pular, ação, iniciar ou Escape para voltar" + #: src/supertux/tile_set.cpp:139 msgid "Others" msgstr "Outros" diff --git a/src/supertux/game_session.cpp b/src/supertux/game_session.cpp index a201cab43d1..b8384d35f78 100644 --- a/src/supertux/game_session.cpp +++ b/src/supertux/game_session.cpp @@ -819,7 +819,7 @@ GameSession::finish(bool win) if (LevelsetScreen::current()) { - LevelsetScreen::current()->finished_level(win); + LevelsetScreen::current()->finished_level(win, m_level->m_stats, m_level->m_target_time); } } diff --git a/src/supertux/global_stats_manager.cpp b/src/supertux/global_stats_manager.cpp new file mode 100644 index 00000000000..dfbc36f3954 --- /dev/null +++ b/src/supertux/global_stats_manager.cpp @@ -0,0 +1,174 @@ +// SuperTux +// Copyright (C) 2026 SuperTux Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "supertux/global_stats_manager.hpp" + +#include +#include + +#include + +#include "physfs/util.hpp" +#include "supertux/levelset.hpp" +#include "supertux/savegame.hpp" +#include "supertux/world.hpp" +#include "util/file_system.hpp" +#include "util/log.hpp" +#include "worldmap/worldmap.hpp" + +namespace { + +void aggregate_level_state(const LevelState& level_state, GlobalStatsManager::Summary& summary) +{ + if (level_state.has_statistics) + { + summary.total_coins += level_state.coins; + summary.total_secrets += level_state.secrets; + summary.total_tuxdolls += level_state.tuxdolls; + } + + if (level_state.solved) + { + summary.solved_levels++; + if (level_state.has_statistics) + summary.total_time += level_state.time; + } + + if (level_state.perfect) + summary.perfect_levels++; +} + +} // namespace + +GlobalStatsManager::Summary::Summary() : + worlds(0), + total_coins(0), + total_secrets(0), + total_tuxdolls(0), + solved_levels(0), + total_levels(0), + perfect_levels(0), + total_time(0.f) +{ +} + +GlobalStatsManager::GlobalStatsManager() +{ +} + +GlobalStatsManager::Summary +GlobalStatsManager::aggregate() const +{ + Summary summary; + + for (const auto& world : collect_worlds()) + { + aggregate_world(*world, summary); + } + + return summary; +} + +std::vector > +GlobalStatsManager::collect_worlds() const +{ + std::vector world_dirs; + collect_worlds_from_directory("levels", world_dirs); + + physfsutil::enumerate_files_alphabetical("custom", [this, &world_dirs](const std::string& addon_filename) { + const std::string addon_path = FileSystem::join("custom", addon_filename); + const std::string addon_levels_path = FileSystem::join(addon_path, "levels"); + + if (physfsutil::is_directory(addon_levels_path)) + collect_worlds_from_directory(addon_levels_path, world_dirs); + + return false; + }); + + std::set unique_dirs(world_dirs.begin(), world_dirs.end()); + std::vector > worlds; + + for (const auto& world_dir : unique_dirs) + { + try + { + std::unique_ptr world = World::from_directory(world_dir); + if (world->is_worldmap()) + { + const std::string worldmap_filename = world->get_worldmap_filename(); + if (PHYSFS_exists(worldmap_filename.c_str()) || FileSystem::exists(worldmap_filename)) + worlds.push_back(std::move(world)); + } + else + { + worlds.push_back(std::move(world)); + } + } + catch (const std::exception& err) + { + log_warning << "Failed to load world info from '" << world_dir << "': " << err.what() << std::endl; + } + } + + return worlds; +} + +void +GlobalStatsManager::collect_worlds_from_directory(const std::string& directory, std::vector& world_dirs) const +{ + physfsutil::enumerate_files_alphabetical(directory, [&directory, &world_dirs](const std::string& filename) { + const std::string filepath = FileSystem::join(directory, filename); + if (physfsutil::is_directory(filepath)) + world_dirs.push_back(filepath); + + return false; + }); +} + +void +GlobalStatsManager::aggregate_world(const World& world, Summary& summary) const +{ + try + { + std::unique_ptr savegame = Savegame::from_current_profile(world.get_basename()); + + if (world.is_worldmap()) + { + const std::string worldmap_filename = physfsutil::realpath(world.get_worldmap_filename()); + worldmap::WorldMap worldmap(worldmap_filename, *savegame); + summary.total_levels += static_cast(worldmap.level_count()); + + WorldmapState state = savegame->get_worldmap_state(worldmap_filename); + for (const auto& level_state : state.level_states) + aggregate_level_state(level_state, summary); + } + else + { + Levelset levelset(world.get_basedir()); + summary.total_levels += levelset.get_num_levels(); + + LevelsetState state = savegame->get_levelset_state(world.get_basedir()); + for (const auto& level_state : state.level_states) + aggregate_level_state(level_state, summary); + } + + summary.worlds++; + } + catch (const std::exception& err) + { + log_warning << "Failed to aggregate statistics for '" << world.get_basedir() << "': " << err.what() << std::endl; + } +} diff --git a/src/supertux/global_stats_manager.hpp b/src/supertux/global_stats_manager.hpp new file mode 100644 index 00000000000..c73f529c99b --- /dev/null +++ b/src/supertux/global_stats_manager.hpp @@ -0,0 +1,55 @@ +// SuperTux +// Copyright (C) 2026 SuperTux Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include +#include +#include + +class World; + +class GlobalStatsManager final +{ +public: + struct Summary final + { + Summary(); + + int worlds; + int total_coins; + int total_secrets; + int total_tuxdolls; + int solved_levels; + int total_levels; + int perfect_levels; + float total_time; + }; + +public: + GlobalStatsManager(); + + Summary aggregate() const; + +private: + std::vector > collect_worlds() const; + void collect_worlds_from_directory(const std::string& directory, std::vector& world_dirs) const; + void aggregate_world(const World& world, Summary& summary) const; + +private: + GlobalStatsManager(const GlobalStatsManager&) = delete; + GlobalStatsManager& operator=(const GlobalStatsManager&) = delete; +}; diff --git a/src/supertux/levelset_screen.cpp b/src/supertux/levelset_screen.cpp index 1adcea6a10e..4bee8717181 100644 --- a/src/supertux/levelset_screen.cpp +++ b/src/supertux/levelset_screen.cpp @@ -22,6 +22,7 @@ #include "supertux/level.hpp" #include "supertux/levelset.hpp" #include "supertux/savegame.hpp" +#include "supertux/statistics.hpp" #include "supertux/screen_fade.hpp" #include "supertux/screen_manager.hpp" #include "util/file_system.hpp" @@ -36,6 +37,12 @@ LevelsetScreen::LevelsetScreen(const std::string& basedir, const std::string& le m_savegame(savegame), m_level_started(false), m_solved(false), + m_perfect(false), + m_best_coins(0), + m_best_tuxdolls(0), + m_best_secrets(0), + m_best_time(0.0f), + m_has_statistics(false), m_start_pos(start_pos), m_skip_intro(skip_intro) { @@ -52,6 +59,12 @@ LevelsetScreen::LevelsetScreen(const std::string& basedir, const std::string& le LevelsetState state = m_savegame.get_levelset_state(basedir); LevelState level_state = state.get_level_state(level_filename); m_solved = level_state.solved; + m_perfect = level_state.perfect; + m_best_coins = level_state.coins; + m_best_tuxdolls = level_state.tuxdolls; + m_best_secrets = level_state.secrets; + m_best_time = level_state.time; + m_has_statistics = level_state.has_statistics; } } @@ -67,9 +80,18 @@ LevelsetScreen::update(float dt_sec, const Controller& controller) } void -LevelsetScreen::finished_level(bool win) +LevelsetScreen::finished_level(bool win, const Statistics& statistics, float target_time) { m_solved = m_solved || win; + m_perfect = m_perfect || statistics.completed(target_time); + m_best_coins = std::max(m_best_coins, statistics.get_coins()); + m_best_tuxdolls = std::max(m_best_tuxdolls, statistics.get_tuxdolls()); + m_best_secrets = std::max(m_best_secrets, statistics.get_secrets()); + if (m_best_time == 0.0f) + m_best_time = statistics.get_time(); + else if (statistics.get_time() > 0.0f) + m_best_time = std::min(m_best_time, statistics.get_time()); + m_has_statistics = m_has_statistics || statistics.get_status() == Statistics::FINAL; } void @@ -81,7 +103,9 @@ LevelsetScreen::setup() { log_info << "Saving Levelset state" << std::endl; // this gets called when the GameSession is done and we return back to the - m_savegame.set_levelset_state(m_basedir, m_level_filename, m_solved); + m_savegame.set_levelset_state(m_basedir, m_level_filename, m_solved, + m_perfect, m_best_coins, m_best_tuxdolls, + m_best_secrets, m_best_time, m_has_statistics); m_savegame.save(); } ScreenManager::current()->pop_screen(); diff --git a/src/supertux/levelset_screen.hpp b/src/supertux/levelset_screen.hpp index 31ade67c330..d653dbdea96 100644 --- a/src/supertux/levelset_screen.hpp +++ b/src/supertux/levelset_screen.hpp @@ -24,6 +24,7 @@ #include "util/currenton.hpp" class Savegame; +class Statistics; class LevelsetScreen final : public Screen, public Currenton @@ -34,6 +35,12 @@ class LevelsetScreen final : public Screen, Savegame& m_savegame; bool m_level_started; bool m_solved; + bool m_perfect; + int m_best_coins; + int m_best_tuxdolls; + int m_best_secrets; + float m_best_time; + bool m_has_statistics; public: LevelsetScreen(const std::string& basedir, const std::string& level_filename, Savegame& savegame, @@ -47,7 +54,7 @@ class LevelsetScreen final : public Screen, virtual IntegrationStatus get_status() const override; - void finished_level(bool win); + void finished_level(bool win, const Statistics& statistics, float target_time); private: std::optional> m_start_pos; diff --git a/src/supertux/menu/global_stats_menu.cpp b/src/supertux/menu/global_stats_menu.cpp new file mode 100644 index 00000000000..d3f74b62c7b --- /dev/null +++ b/src/supertux/menu/global_stats_menu.cpp @@ -0,0 +1,63 @@ +// SuperTux +// Copyright (C) 2026 SuperTux Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "supertux/menu/global_stats_menu.hpp" + +#include + +#include "gui/menu_item.hpp" +#include "gui/menu_manager.hpp" +#include "supertux/fadetoblack.hpp" +#include "supertux/globals.hpp" +#include "supertux/screen/global_stats_screen.hpp" +#include "supertux/screen_manager.hpp" +#include "util/gettext.hpp" +#include "video/video_system.hpp" +#include "video/viewport.hpp" + +GlobalStatsMenu::GlobalStatsMenu() +{ + add_label(_("Global Statistics")); + add_hl(); + add_entry(MNID_VIEW_GLOBAL_STATS, _("View Statistics")); + add_hl(); + add_back(_("Back")); + + on_window_resize(); +} + +void +GlobalStatsMenu::on_window_resize() +{ + set_center_pos(static_cast(SCREEN_WIDTH) / 2.0f, + static_cast(SCREEN_HEIGHT) / 2.0f + 35.0f); +} + +void +GlobalStatsMenu::menu_action(MenuItem& item) +{ + switch (item.get_id()) + { + case MNID_VIEW_GLOBAL_STATS: + { + MenuManager::instance().clear_menu_stack(); + auto screen = std::make_unique(); + auto fade = std::make_unique(FadeToBlack::FADEOUT, 0.25f); + ScreenManager::current()->push_screen(std::move(screen), std::move(fade)); + break; + } + } +} diff --git a/src/supertux/menu/global_stats_menu.hpp b/src/supertux/menu/global_stats_menu.hpp new file mode 100644 index 00000000000..8c9171f31c6 --- /dev/null +++ b/src/supertux/menu/global_stats_menu.hpp @@ -0,0 +1,37 @@ +// SuperTux +// Copyright (C) 2026 SuperTux Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include "gui/menu.hpp" + +class GlobalStatsMenu final : public Menu +{ +public: + GlobalStatsMenu(); + + void menu_action(MenuItem& item) override; + void on_window_resize() override; + +private: + enum GlobalStatsMenuIDs { + MNID_VIEW_GLOBAL_STATS + }; + +private: + GlobalStatsMenu(const GlobalStatsMenu&) = delete; + GlobalStatsMenu& operator=(const GlobalStatsMenu&) = delete; +}; diff --git a/src/supertux/menu/main_menu.cpp b/src/supertux/menu/main_menu.cpp index be73fb7b583..44dcbed04a0 100644 --- a/src/supertux/menu/main_menu.cpp +++ b/src/supertux/menu/main_menu.cpp @@ -53,6 +53,7 @@ MainMenu::MainMenu() { add_entry(MNID_WORLDSET_STORY, _("Start Game")); add_entry(MNID_WORLDSET_CONTRIB, _("Contrib Levels")); + add_submenu(_("Statistics"), MenuStorage::GLOBAL_STATS_MENU, MNID_GLOBAL_STATS); // TODO: Manage to build OpenSSL for Emscripten so we can build CURL so we can // build the add-ons so we can re-enable them. // Also see src/addon/downloader.*pp diff --git a/src/supertux/menu/main_menu.hpp b/src/supertux/menu/main_menu.hpp index 2131cda97af..68ab0dc19ae 100644 --- a/src/supertux/menu/main_menu.hpp +++ b/src/supertux/menu/main_menu.hpp @@ -34,6 +34,7 @@ class MainMenu final : public Menu enum MainMenuIDs { MNID_WORLDSET_STORY, MNID_WORLDSET_CONTRIB, + MNID_GLOBAL_STATS, MNID_LEVELEDITOR, MNID_CREDITS, MNID_DONATE, diff --git a/src/supertux/menu/menu_storage.cpp b/src/supertux/menu/menu_storage.cpp index ea1ca67b514..50c3ac1f3a4 100644 --- a/src/supertux/menu/menu_storage.cpp +++ b/src/supertux/menu/menu_storage.cpp @@ -37,6 +37,7 @@ #include "supertux/menu/editor_sector_menu.hpp" #include "supertux/menu/editor_sectors_menu.hpp" #include "supertux/menu/game_menu.hpp" +#include "supertux/menu/global_stats_menu.hpp" #include "supertux/menu/integrations_menu.hpp" #include "supertux/menu/joystick_menu.hpp" #include "supertux/menu/keyboard_menu.hpp" @@ -95,6 +96,9 @@ MenuStorage::create(MenuId menu_id) case PROFILE_MENU: return std::make_unique(); + case GLOBAL_STATS_MENU: + return std::make_unique(); + case KEYBOARD_MENU: return std::unique_ptr(new KeyboardMenu(*InputManager::current())); diff --git a/src/supertux/menu/menu_storage.hpp b/src/supertux/menu/menu_storage.hpp index 137c4fce6d0..4492a698ab4 100644 --- a/src/supertux/menu/menu_storage.hpp +++ b/src/supertux/menu/menu_storage.hpp @@ -34,6 +34,7 @@ class MenuStorage final OPTIONS_MENU, INGAME_OPTIONS_MENU, PROFILE_MENU, + GLOBAL_STATS_MENU, CONTRIB_MENU, CONTRIB_WORLD_MENU, ADDON_MENU, diff --git a/src/supertux/savegame.cpp b/src/supertux/savegame.cpp index ec9c99147f7..36344fcbade 100644 --- a/src/supertux/savegame.cpp +++ b/src/supertux/savegame.cpp @@ -38,6 +38,40 @@ namespace { +void merge_level_state(LevelState& into, const LevelState& from) +{ + into.solved = into.solved || from.solved; + into.perfect = into.perfect || from.perfect; + into.coins = std::max(into.coins, from.coins); + into.tuxdolls = std::max(into.tuxdolls, from.tuxdolls); + into.secrets = std::max(into.secrets, from.secrets); + if (into.time == 0.0f) + into.time = from.time; + else if (from.time > 0.0f) + into.time = std::min(into.time, from.time); + into.has_statistics = into.has_statistics || from.has_statistics; +} + +void merge_level_states(std::vector& into, const std::vector& from) +{ + for (const auto& in_state : from) + { + auto it = std::find_if(into.begin(), into.end(), + [&in_state](const LevelState& state) + { + return state.filename == in_state.filename; + }); + if (it != into.end()) + { + merge_level_state(*it, in_state); + } + else + { + into.push_back(in_state); + } + } +} + std::vector get_level_states(ssq::Table& levels) { std::vector results; @@ -52,6 +86,19 @@ std::vector get_level_states(ssq::Table& levels) table.get("solved", level_state.solved); table.get("perfect", level_state.perfect); + try + { + const ssq::Table statistics = table.findTable("statistics"); + statistics.get("coins-collected", level_state.coins); + statistics.get("tuxdolls-collected", level_state.tuxdolls); + statistics.get("secrets-found", level_state.secrets); + statistics.get("time-needed", level_state.time); + level_state.has_statistics = true; + } + catch (const ssq::NotFoundException&) + { + } + results.push_back(std::move(level_state)); } catch (const ssq::TypeException&) @@ -359,6 +406,7 @@ WorldmapState Savegame::get_worldmap_state(const std::string& name) { WorldmapState result; + const std::string canonical_name = physfsutil::realpath(name); if (Editor::current()) log_warning << "Savegame::get_worldmap_state called while the editor is active" << std::endl; @@ -368,15 +416,39 @@ Savegame::get_worldmap_state(const std::string& name) ssq::Table worlds = m_state_table.getOrCreateTable("worlds"); // if a non-canonical entry is present, replace them with a canonical one - if (name != "/levels/world2/worldmap.stwm") + if (canonical_name != "/levels/world2/worldmap.stwm") { - std::string old_map_filename = name.substr(1); + const std::string old_map_filename = canonical_name.substr(1); if (worlds.hasEntry(old_map_filename.c_str())) - worlds.rename(old_map_filename.c_str(), name.c_str()); + worlds.rename(old_map_filename.c_str(), canonical_name.c_str()); } - ssq::Table levels = worlds.getOrCreateTable(name.c_str()).getOrCreateTable("levels"); - result.level_states = get_level_states(levels); + ssq::Table worldmap = worlds.getOrCreateTable(canonical_name.c_str()); + + if (worldmap.hasEntry("levels")) + { + ssq::Table levels = worldmap.findTable("levels"); + merge_level_states(result.level_states, get_level_states(levels)); + } + + for (const auto& [key, value] : worldmap.convertRaw()) + { + if (key == "levels") + continue; + + try + { + ssq::Table sector = value.toTable(); + if (sector.hasEntry("levels")) + { + ssq::Table levels = sector.findTable("levels"); + merge_level_states(result.level_states, get_level_states(levels)); + } + } + catch (const ssq::TypeException&) + { + } + } } catch(const std::exception& err) { @@ -430,7 +502,13 @@ Savegame::get_levelset_state(const std::string& basedir) void Savegame::set_levelset_state(const std::string& basedir, const std::string& level_filename, - bool solved) + bool solved, + bool perfect, + int coins, + int tuxdolls, + int secrets, + float time, + bool has_statistics) { LevelsetState state = get_levelset_state(basedir); @@ -446,6 +524,28 @@ Savegame::set_levelset_state(const std::string& basedir, bool old_solved = false; level.get("solved", old_solved); level.set("solved", solved || old_solved); + + bool old_perfect = false; + level.get("perfect", old_perfect); + level.set("perfect", perfect || old_perfect); + + if (has_statistics) + { + LevelState old_state = state.get_level_state(level_filename); + level.remove("statistics"); + ssq::Table statistics = level.addTable("statistics"); + statistics.set("coins-collected", std::max(old_state.coins, coins)); + statistics.set("tuxdolls-collected", std::max(old_state.tuxdolls, tuxdolls)); + statistics.set("secrets-found", std::max(old_state.secrets, secrets)); + + float best_time = old_state.time; + if (best_time == 0.0f) + best_time = time; + else if (time > 0.0f) + best_time = std::min(best_time, time); + + statistics.set("time-needed", best_time); + } } catch(const std::exception& err) { diff --git a/src/supertux/savegame.hpp b/src/supertux/savegame.hpp index 7b800b5509f..abb99ce4854 100644 --- a/src/supertux/savegame.hpp +++ b/src/supertux/savegame.hpp @@ -32,12 +32,22 @@ struct LevelState LevelState() : filename(), solved(false), - perfect(false) + perfect(false), + coins(0), + tuxdolls(0), + secrets(0), + time(0.0f), + has_statistics(false) {} std::string filename; bool solved; bool perfect; + int coins; + int tuxdolls; + int secrets; + float time; + bool has_statistics; }; struct LevelsetState @@ -86,7 +96,13 @@ class Savegame final LevelsetState get_levelset_state(const std::string& name); void set_levelset_state(const std::string& basedir, const std::string& level_filename, - bool solved); + bool solved, + bool perfect = false, + int coins = 0, + int tuxdolls = 0, + int secrets = 0, + float time = 0.0f, + bool has_statistics = false); std::vector get_worldmaps(); WorldmapState get_worldmap_state(const std::string& name); diff --git a/src/supertux/screen/global_stats_screen.cpp b/src/supertux/screen/global_stats_screen.cpp new file mode 100644 index 00000000000..913519ba8d5 --- /dev/null +++ b/src/supertux/screen/global_stats_screen.cpp @@ -0,0 +1,394 @@ +// SuperTux +// Copyright (C) 2026 SuperTux Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "supertux/screen/global_stats_screen.hpp" + +#include +#include + +#include + +#include "collision/collision_object.hpp" +#include "control/codecontroller.hpp" +#include "control/controller.hpp" +#include "gui/mousecursor.hpp" +#include "math/util.hpp" +#include "object/camera.hpp" +#include "object/music_object.hpp" +#include "object/player.hpp" +#include "supertux/constants.hpp" +#include "supertux/d_scope.hpp" +#include "supertux/fadetoblack.hpp" +#include "supertux/game_session.hpp" +#include "supertux/gameconfig.hpp" +#include "supertux/globals.hpp" +#include "supertux/profile_manager.hpp" +#include "supertux/resources.hpp" +#include "supertux/savegame.hpp" +#include "supertux/screen_manager.hpp" +#include "supertux/sector.hpp" +#include "supertux/statistics.hpp" +#include "supertux/tile.hpp" +#include "supertux/world.hpp" +#include "util/gettext.hpp" +#include "video/compositor.hpp" +#include "video/drawing_context.hpp" +#include "video/layer.hpp" +#include "video/surface.hpp" + +static const std::string DEFAULT_TITLE_LEVEL = "levels/misc/menu.stl"; + +Color GlobalStatsScreen::s_header_color(1.f, 1.f, 1.f); +Color GlobalStatsScreen::s_label_color(0.90f, 0.95f, 1.00f); +Color GlobalStatsScreen::s_value_color(0.72f, 0.95f, 1.00f); +Color GlobalStatsScreen::s_help_color(0.76f, 0.84f, 0.92f); + +std::optional GlobalStatsScreen::s_cached_summary; + +GlobalStatsScreen::GlobalStatsScreen() : + m_frame(Surface::from_file("images/engine/menu/frame.png")), + m_backdrop(Surface::from_file("images/engine/menu/score-backdrop.png")), + m_savegame(std::make_unique(ProfileManager::current()->get_current_profile(), "")), + m_controller(new CodeController()), + m_titlesession(), + m_summary(), + m_summary_ready(false), + m_load_requested(false), + m_jump_was_released(false) +{ +} + +GlobalStatsScreen::~GlobalStatsScreen() +{ +} + +void +GlobalStatsScreen::setup() +{ + MouseCursor::current()->set_visible(true); + refresh_title_background(); + s_cached_summary.reset(); +} + +void +GlobalStatsScreen::leave() +{ + if (m_titlesession) + { + m_titlesession->get_current_sector().deactivate(); + m_titlesession->leave(); + } + + MouseCursor::current()->set_visible(true); +} + +void +GlobalStatsScreen::update(float dt_sec, const Controller& controller) +{ + update_title_background(dt_sec * 0.6f); + + if (!m_summary_ready) + { + if (!m_load_requested) + { + m_load_requested = true; + return; + } + + load_summary(); + return; + } + + if (controller.pressed_any(Control::JUMP, Control::ACTION, Control::MENU_SELECT, + Control::START, Control::ESCAPE)) + { + ScreenManager::current()->pop_screen(std::make_unique(FadeToBlack::FADEOUT, 0.1f)); + } +} + +void +GlobalStatsScreen::draw(Compositor& compositor) +{ + constexpr int panel_layer = LAYER_GUI; + constexpr int text_layer = LAYER_GUI + 1; + + auto& context = compositor.make_context(); + draw_background(context); + + if (!m_summary_ready) + { + context.color().draw_center_text(Resources::normal_font, + _("Loading statistics..."), + Vector(0, context.get_height() / 2.f - Resources::normal_font->get_height() / 2.f), + text_layer, + s_header_color); + + context.color().draw_center_text(Resources::small_font, + _("Please wait a moment"), + Vector(0, context.get_height() / 2.f + Resources::small_font->get_height()), + text_layer, + s_help_color); + return; + } + + int py = static_cast(context.get_height() / 2.f - Resources::normal_font->get_height() * 5.f); + + context.color().draw_center_text(Resources::normal_font, + std::string("- ") + _("Global Statistics") + std::string(" -"), + Vector(0, static_cast(py)), + text_layer, + s_header_color); + py += static_cast(Resources::normal_font->get_height() * 2.f); + + draw_stat_line(context, py, _("Coins collected"), format_count(m_summary.total_coins)); + draw_stat_line(context, py, _("Secrets found"), format_count(m_summary.total_secrets)); + draw_stat_line(context, py, _("Tux Dolls collected"), format_count(m_summary.total_tuxdolls)); + draw_stat_line(context, py, _("Levels completed"), fmt::format("{} / {}", m_summary.solved_levels, m_summary.total_levels)); + draw_stat_line(context, py, _("Perfect levels"), format_count(m_summary.perfect_levels)); + draw_stat_line(context, py, _("Total play time"), format_time(m_summary.total_time)); + + py += static_cast(Resources::normal_font->get_height()); + context.color().draw_center_text(Resources::small_font, + _("Press jump, action, start or escape to return"), + Vector(0, static_cast(py)), + text_layer, + s_help_color); + + if (m_frame) + context.color().draw_surface_scaled(m_frame, context.get_rect(), panel_layer + 2); +} + +IntegrationStatus +GlobalStatsScreen::get_status() const +{ + IntegrationStatus status; + status.m_details.push_back("Viewing global statistics"); + return status; +} + +void +GlobalStatsScreen::draw_background(DrawingContext& context) const +{ + constexpr int panel_layer = LAYER_GUI; + + if (m_titlesession) + { + m_titlesession->get_current_sector().draw(context); + } + else + { + context.set_ambient_color(Color(1.f, 1.f, 1.f, 1.f)); + context.color().draw_gradient(Color(0.53f, 0.71f, 0.96f, 1.f), + Color(0.94f, 0.97f, 1.00f, 1.f), + LAYER_BACKGROUND0, + VERTICAL, + context.get_rect()); + } + + const float panel_width = std::min(context.get_width() - 120.f, 760.f); + const float panel_height = 360.f; + const Rectf panel(Vector((context.get_width() - panel_width) / 2.f, + (context.get_height() - panel_height) / 2.f - 10.f), + Sizef(panel_width, panel_height)); + + if (m_backdrop) + { + context.color().draw_surface_scaled(m_backdrop, panel, panel_layer); + } + else + { + context.color().draw_filled_rect(panel, + Color(0.18f, 0.25f, 0.35f, 0.72f), + 18.f, + panel_layer); + } +} + +void +GlobalStatsScreen::draw_stat_line(DrawingContext& context, int& py, + const std::string& label, const std::string& value) const +{ + constexpr int text_layer = LAYER_GUI + 1; + const float center_x = context.get_width() / 2.f; + const float label_x = center_x - 24.f; + const float value_x = center_x + 24.f; + + context.color().draw_text(Resources::normal_font, + label + ":", + Vector(label_x, static_cast(py)), + ALIGN_RIGHT, + text_layer, + s_label_color); + context.color().draw_text(Resources::normal_font, + value, + Vector(value_x, static_cast(py)), + ALIGN_LEFT, + text_layer, + s_value_color); + + py += static_cast(Resources::normal_font->get_height()); +} + +std::string +GlobalStatsScreen::format_count(int value) const +{ + return fmt::format("{}", value); +} + +std::string +GlobalStatsScreen::format_time(float value) const +{ + if (value <= 0.0f) + return _("Not available"); + + return Statistics::time_to_string(value); +} + +void +GlobalStatsScreen::load_summary() +{ + m_summary = GlobalStatsManager().aggregate(); + s_cached_summary = m_summary; + m_summary_ready = true; +} + +void +GlobalStatsScreen::refresh_title_background() +{ + bool level_init = false; + std::string title_level = DEFAULT_TITLE_LEVEL; + + if (g_config->custom_title_levels) + { + const std::string last_world = ProfileManager::current()->get_current_profile().get_last_world(); + if (!last_world.empty()) + { + const std::string savegame_title_level = Savegame::from_current_profile(last_world, true)->get_player_status().title_level; + if (savegame_title_level.empty()) + { + const auto world = World::from_directory("levels/" + last_world); + if (world) + title_level = world->get_title_level(); + } + else + { + title_level = savegame_title_level; + } + } + } + + if (title_level.empty()) + title_level = DEFAULT_TITLE_LEVEL; + + if (!m_titlesession || m_titlesession->get_level_file() != title_level) + { + std::unique_ptr new_session; + try + { + new_session = std::make_unique(title_level, *m_savegame, nullptr); + new_session->restart_level(false, true); + } + catch (const std::exception&) + { + if (!m_titlesession || m_titlesession->get_level_file() != DEFAULT_TITLE_LEVEL) + { + new_session = std::make_unique(DEFAULT_TITLE_LEVEL, *m_savegame, nullptr); + new_session->restart_level(false, true); + } + } + + if (new_session) + { + m_titlesession = std::move(new_session); + level_init = true; + } + } + + if (!m_titlesession) + return; + + Sector& sector = m_titlesession->get_current_sector(); + if (level_init || Sector::current() != §or) + setup_title_sector(sector); +} + +void +GlobalStatsScreen::setup_title_sector(Sector& sector) +{ + auto& music = sector.get_singleton_by_type(); + music.resume_music(true); + + Player& player = *(sector.get_players()[0]); + sector.activate(player.get_pos() - Vector(0.f, player.is_big() ? 0.f : 32.f)); + player.set_controller(m_controller.get()); + player.set_speedlimit(230.f); +} + +void +GlobalStatsScreen::update_title_background(float dt_sec) +{ + if (!m_titlesession) + return; + + Sector& sector = m_titlesession->get_current_sector(); + Player& player = *(sector.get_players()[0]); + + if (player.is_dying()) + { + m_titlesession->restart_level(true, true); + setup_title_sector(m_titlesession->get_current_sector()); + return; + } + + BIND_SECTOR(sector); + sector.update(dt_sec); + + m_controller->update(); + m_controller->press(Control::RIGHT); + + const Rectf& bbox = player.get_bbox(); + const Vector eye(bbox.get_right(), bbox.get_top() + bbox.get_height() / 2); + const Vector end(eye.x + 46.f, eye.y); + + auto result = sector.get_first_line_intersection(eye, end, false, player.get_collision_object()); + + bool shouldjump = result.is_valid; + if (shouldjump) + { + if (auto tile = std::get_if(&result.hit)) + shouldjump = !(*tile)->is_slope(); + else if (auto obj = std::get_if(&result.hit)) + shouldjump = ((*obj)->get_group() == COLGROUP_STATIC || + (*obj)->get_group() == COLGROUP_MOVING_STATIC); + } + + if (player.m_fall_mode == Player::FallMode::JUMPING || + (m_jump_was_released && shouldjump)) + { + m_controller->press(Control::JUMP); + m_jump_was_released = false; + } + else + { + m_jump_was_released = true; + } + + if (sector.get_width() - 320.f < player.get_pos().x) + { + sector.activate(DEFAULT_SECTOR_NAME); + sector.get_camera().reset(player.get_pos()); + } +} diff --git a/src/supertux/screen/global_stats_screen.hpp b/src/supertux/screen/global_stats_screen.hpp new file mode 100644 index 00000000000..12fabda3aec --- /dev/null +++ b/src/supertux/screen/global_stats_screen.hpp @@ -0,0 +1,79 @@ +// SuperTux +// Copyright (C) 2026 SuperTux Team +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include +#include +#include + +#include "supertux/global_stats_manager.hpp" +#include "supertux/screen.hpp" +#include "video/color.hpp" +#include "video/surface_ptr.hpp" + +class DrawingContext; +class CodeController; +class GameSession; +class Savegame; +class Sector; + +class GlobalStatsScreen final : public Screen +{ +private: + static Color s_header_color; + static Color s_label_color; + static Color s_value_color; + static Color s_help_color; + +public: + GlobalStatsScreen(); + ~GlobalStatsScreen() override; + + void setup() override; + void leave() override; + void draw(Compositor& compositor) override; + void update(float dt_sec, const Controller& controller) override; + IntegrationStatus get_status() const override; + +private: + void draw_background(DrawingContext& context) const; + void draw_stat_line(DrawingContext& context, int& py, const std::string& label, const std::string& value) const; + std::string format_count(int value) const; + std::string format_time(float value) const; + void load_summary(); + void refresh_title_background(); + void setup_title_sector(Sector& sector); + void update_title_background(float dt_sec); + +private: + static std::optional s_cached_summary; + +private: + SurfacePtr m_frame; + SurfacePtr m_backdrop; + std::unique_ptr m_savegame; + std::unique_ptr m_controller; + std::unique_ptr m_titlesession; + GlobalStatsManager::Summary m_summary; + bool m_summary_ready; + bool m_load_requested; + bool m_jump_was_released; + +private: + GlobalStatsScreen(const GlobalStatsScreen&) = delete; + GlobalStatsScreen& operator=(const GlobalStatsScreen&) = delete; +}; diff --git a/src/worldmap/worldmap.cpp b/src/worldmap/worldmap.cpp index 059858b76c1..10cb383f3f8 100644 --- a/src/worldmap/worldmap.cpp +++ b/src/worldmap/worldmap.cpp @@ -284,10 +284,17 @@ WorldMap::solved_level_count() const void -WorldMap::load_state() +WorldMap::load_state(bool create_missing) { WorldMapState state(*this); - state.load_state(); + state.load_state(create_missing); +} + +void +WorldMap::load_state(WorldMapSector& sector, bool create_missing) +{ + m_sector = §or; + load_state(create_missing); } void diff --git a/src/worldmap/worldmap.hpp b/src/worldmap/worldmap.hpp index 99bff9e742a..85d13c4b1cc 100644 --- a/src/worldmap/worldmap.hpp +++ b/src/worldmap/worldmap.hpp @@ -58,7 +58,8 @@ class WorldMap final : public Screen, size_t solved_level_count() const; /** Load worldmap state from squirrel state table */ - void load_state(); + void load_state(bool create_missing = true); + void load_state(WorldMapSector& sector, bool create_missing = true); /** Save worldmap state to squirrel state table */ void save_state(); diff --git a/src/worldmap/worldmap_state.cpp b/src/worldmap/worldmap_state.cpp index 25289337c1a..0720a7b7f38 100644 --- a/src/worldmap/worldmap_state.cpp +++ b/src/worldmap/worldmap_state.cpp @@ -59,7 +59,7 @@ WorldMapState::new_save(bool initial) } void -WorldMapState::load_state() +WorldMapState::load_state(bool create_missing) { log_debug << "loading worldmap state" << std::endl; @@ -125,17 +125,34 @@ WorldMapState::load_state() } catch (const std::exception& err) { - log_warning << "Not loading worldmap state: " << err.what() << std::endl; - new_save(true); + if (create_missing) + { + log_warning << "Not loading worldmap state: " << err.what() << std::endl; + new_save(true); + } + else + { + log_debug << "Not loading worldmap state: " << err.what() << std::endl; + } } } else { - log_warning << - fmt::format("Save version doesn't match (got {}), worldmap expects {}. " - "Creating a new save.", - savegame.get_save_version(), m_worldmap.get_save_version()); - new_save(false); + if (create_missing) + { + log_warning << + fmt::format("Save version doesn't match (got {}), worldmap expects {}. " + "Creating a new save.", + savegame.get_save_version(), m_worldmap.get_save_version()); + new_save(false); + } + else + { + log_debug << + fmt::format("Save version doesn't match (got {}), worldmap expects {}. " + "Not loading worldmap state.", + savegame.get_save_version(), m_worldmap.get_save_version()); + } } m_worldmap.m_in_level = false; diff --git a/src/worldmap/worldmap_state.hpp b/src/worldmap/worldmap_state.hpp index 5f843742c00..6093e00ea47 100644 --- a/src/worldmap/worldmap_state.hpp +++ b/src/worldmap/worldmap_state.hpp @@ -31,7 +31,7 @@ class WorldMapState final public: WorldMapState(WorldMap& worldmap); - void load_state(); + void load_state(bool create_missing = true); /// @param initial If this is an initial save. void save_state(bool initial = false) const;