From 2dae906dad00863dab4654b0e67cf799d534f7b9 Mon Sep 17 00:00:00 2001 From: MichalLabuda <29762723+MichalLabuda@users.noreply.github.com> Date: Thu, 18 Jun 2026 10:54:15 +0200 Subject: [PATCH 1/3] Addon presets --- libs/rttrConfig/src/files.h | 1 + libs/s25main/gameData/const_gui_ids.h | 1 + libs/s25main/ingameWindows/iwAddonPresets.cpp | 294 ++++++++++++++++++ libs/s25main/ingameWindows/iwAddonPresets.h | 57 ++++ libs/s25main/ingameWindows/iwAddons.cpp | 54 +++- libs/s25main/ingameWindows/iwAddons.h | 2 + 6 files changed, 402 insertions(+), 7 deletions(-) create mode 100644 libs/s25main/ingameWindows/iwAddonPresets.cpp create mode 100644 libs/s25main/ingameWindows/iwAddonPresets.h diff --git a/libs/rttrConfig/src/files.h b/libs/rttrConfig/src/files.h index 02178f8012..b4c0b1de7c 100644 --- a/libs/rttrConfig/src/files.h +++ b/libs/rttrConfig/src/files.h @@ -42,6 +42,7 @@ namespace folders { constexpr auto mbob = "/DATA/MBOB"; // nation graphics constexpr auto music = "/MUSIC"; constexpr auto playlists = "/playlists"; + constexpr auto addonPresets = "/PRESETS"; constexpr auto replays = "/REPLAYS"; constexpr auto save = "/SAVE"; constexpr auto screenshots = "/screenshots"; diff --git a/libs/s25main/gameData/const_gui_ids.h b/libs/s25main/gameData/const_gui_ids.h index 2347ce9198..32a23e9fc8 100644 --- a/libs/s25main/gameData/const_gui_ids.h +++ b/libs/s25main/gameData/const_gui_ids.h @@ -12,6 +12,7 @@ enum GUI_ID : unsigned { CGI_ACTION, CGI_ADDONS, + CGI_ADDON_PRESETS, CGI_AI_DEBUG, CGI_BUILDINGS, CGI_BUILDINGSPRODUCTIVITY, diff --git a/libs/s25main/ingameWindows/iwAddonPresets.cpp b/libs/s25main/ingameWindows/iwAddonPresets.cpp new file mode 100644 index 0000000000..c19ed8c1ea --- /dev/null +++ b/libs/s25main/ingameWindows/iwAddonPresets.cpp @@ -0,0 +1,294 @@ +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "iwAddonPresets.h" +#include "ListDir.h" +#include "Loader.h" +#include "RttrConfig.h" +#include "WindowManager.h" +#include "controls/ctrlEdit.h" +#include "controls/ctrlTable.h" +#include "controls/ctrlText.h" +#include "files.h" +#include "helpers/containerUtils.h" +#include "iwMsgbox.h" +#include "gameData/const_gui_ids.h" +#include "libsiedler2/ArchivItem_Ini.h" +#include "libsiedler2/ArchivItem_Text.h" +#include "libsiedler2/libsiedler2.h" +#include "s25util/Log.h" +#include "s25util/StringConversion.h" +#include "s25util/strAlgos.h" +#include +#include + +namespace bfs = boost::filesystem; + +namespace { +enum +{ + ID_tblPresets, + ID_edtName, + ID_btAction, + ID_btDelete, + ID_txtFolder, +}; +constexpr unsigned ID_msgboxDelete = 0; +constexpr unsigned ID_msgboxOverwrite = 1; +} // namespace + +static bfs::path GetPresetsDir() +{ + return RTTRCONFIG.ExpandPath(s25::folders::addonPresets); +} + +static std::optional> LoadStatesFromFile(const bfs::path& filePath) +{ + libsiedler2::Archiv archive; + if(libsiedler2::Load(filePath, archive) != 0) + { + LOG.write("Failed to load addon preset from %1%\n") % filePath; + return std::nullopt; + } + + const auto* ini = dynamic_cast(archive.find("addons")); + if(!ini) + return std::nullopt; + + std::map states; + for(unsigned i = 0; i < ini->size(); ++i) + { + const auto* item = dynamic_cast(ini->get(i)); + if(!item) + return std::nullopt; + unsigned id, status; + if(!s25util::tryFromStringClassic(item->getName(), id) + || !s25util::tryFromStringClassic(item->getText(), status)) + return std::nullopt; + states[id] = status; + } + return states; +} + +// iwAddonPresetsBase +iwAddonPresetsBase::iwAddonPresetsBase(const std::string& title, const std::string& actionLabel) + : IngameWindow(CGI_ADDON_PRESETS, IngameWindow::posLastOrCenter, Extent(440, 320), title, + LOADER.GetImageN("resource", 41)) +{ + using SRT = ctrlTable::SortType; + AddTable(ID_tblPresets, DrawPoint(20, 30), Extent(400, 200), TextureColor::Green2, NormalFont, + ctrlTable::Columns{{_("Preset Name"), 400, SRT::String}, {}}); + + const bfs::path presetsDir = GetPresetsDir(); + AddText(ID_txtFolder, DrawPoint(20, 236), presetsDir.string(), COLOR_YELLOW, FontStyle::TOP, SmallFont) + ->setMaxWidth(400); + + AddEdit(ID_edtName, DrawPoint(20, 254), Extent(400, 22), TextureColor::Green2, NormalFont); + + AddTextButton(ID_btAction, DrawPoint(20, 284), Extent(185, 22), TextureColor::Green2, actionLabel, NormalFont); + AddTextButton(ID_btDelete, DrawPoint(235, 284), Extent(185, 22), TextureColor::Red1, _("Delete"), NormalFont); + + bfs::create_directories(presetsDir); + RefreshTable(); +} + +void iwAddonPresetsBase::RefreshTable() +{ + auto* table = GetCtrl(ID_tblPresets); + table->DeleteAllItems(); + + for(const auto& file : ListDir(GetPresetsDir(), "ini")) + table->AddRow({file.stem().string(), file.string()}); + + table->SortRows(0, TableSortDir::Ascending); +} + +bfs::path iwAddonPresetsBase::GetSelectedFilePath() const +{ + const auto* table = GetCtrl(ID_tblPresets); + if(!table->GetSelection()) + return {}; + return table->GetItemText(*table->GetSelection(), 1); +} + +void iwAddonPresetsBase::Msg_EditEnter(const unsigned /*ctrl_id*/) +{ + DoAction(); +} + +void iwAddonPresetsBase::Msg_ButtonClick(const unsigned ctrl_id) +{ + switch(ctrl_id) + { + case ID_btAction: DoAction(); break; + case ID_btDelete: ConfirmDelete(); break; + default: break; + } +} + +void iwAddonPresetsBase::Msg_TableSelectItem(const unsigned /*ctrl_id*/, const boost::optional& selection) +{ + const auto* table = GetCtrl(ID_tblPresets); + GetCtrl(ID_edtName)->SetText(selection ? table->GetItemText(*selection, 0) : ""); +} + +void iwAddonPresetsBase::ConfirmDelete() +{ + if(GetSelectedFilePath().empty()) + return; + WINDOWMANAGER.Show(std::make_unique(_("Delete Preset"), + _("Are you sure you want to delete the selected preset?"), this, + MsgboxButton::YesNo, MsgboxIcon::QuestionRed, ID_msgboxDelete)); +} + +void iwAddonPresetsBase::Msg_MsgBoxResult(const unsigned msgbox_id, const MsgboxResult mbr) +{ + if(msgbox_id != ID_msgboxDelete || mbr != MsgboxResult::Yes) + return; + + const bfs::path filePath = GetSelectedFilePath(); + if(filePath.empty()) + return; + + boost::system::error_code ec; + bfs::remove(filePath, ec); + if(ec) + { + LOG.write("Failed to delete addon preset %1%: %2%\n") % filePath % ec.message(); + WINDOWMANAGER.Show(std::make_unique(_("Delete Failed"), _("Failed to delete the selected preset."), + this, MsgboxButton::Ok, MsgboxIcon::ExclamationRed)); + // Refresh so the list reflects the actual filesystem state (e.g. file became a directory) + RefreshTable(); + GetCtrl(ID_edtName)->SetText(""); + return; + } + + RefreshTable(); + GetCtrl(ID_edtName)->SetText(""); +} + +// iwSaveAddonPreset +iwSaveAddonPreset::iwSaveAddonPreset(std::map states) + : iwAddonPresetsBase(_("Save Addon Preset"), _("Save")), states_(std::move(states)) +{} + +bfs::path iwSaveAddonPreset::GetSaveFilePath() const +{ + std::string name = GetCtrl(ID_edtName)->GetText(); + name = bfs::path(name).filename().string(); + + // Trim leading/trailing whitespace + const auto first = name.find_first_not_of(" \t\r\n"); + if(first == std::string::npos) + return {}; + name = name.substr(first, name.find_last_not_of(" \t\r\n") - first + 1); + + if(name.empty() || name == "." || name == "..") + return {}; + if(s25util::toLower(bfs::path(name).extension().string()) != ".ini") + name += ".ini"; + return GetPresetsDir() / name; +} + +void iwSaveAddonPreset::DoAction() +{ + const bfs::path filePath = GetSaveFilePath(); + if(filePath.empty()) + return; + + // Reject Windows reserved device names (NUL, CON, COM1, etc.) — these cause failures on Windows + // even with an extension, and presets should be portable across platforms. + static constexpr std::array reservedNames{"con", "prn", "aux", "nul", "com1", "com2", "com3", "com4", + "com5", "com6", "com7", "com8", "com9", "lpt1", "lpt2", "lpt3", + "lpt4", "lpt5", "lpt6", "lpt7", "lpt8", "lpt9"}; + if(helpers::contains(reservedNames, s25util::toLower(filePath.stem().string()))) + { + WINDOWMANAGER.Show(std::make_unique( + _("Invalid Name"), _("This filename is reserved and cannot be used. Please choose a different name."), this, + MsgboxButton::Ok, MsgboxIcon::ExclamationRed)); + return; + } + + if(bfs::exists(filePath)) + { + WINDOWMANAGER.Show(std::make_unique( + _("Overwrite Preset"), _("A preset with this name already exists. Do you want to overwrite it?"), this, + MsgboxButton::YesNo, MsgboxIcon::QuestionRed, ID_msgboxOverwrite)); + return; + } + + SaveToPath(filePath); +} + +void iwSaveAddonPreset::SaveToPath(const bfs::path& filePath) +{ + auto iniItem = std::make_unique("addons"); + for(const auto& [id, status] : states_) + iniItem->setValue(s25util::toStringClassic(id), s25util::toStringClassic(status)); + + libsiedler2::Archiv archive; + archive.push(std::move(iniItem)); + + if(libsiedler2::Write(filePath, archive) != 0) + { + LOG.write("Failed to save addon preset to %1%\n") % filePath; + WINDOWMANAGER.Show(std::make_unique( + _("Save Failed"), _("Failed to save the preset. Please check the filename and try again."), this, + MsgboxButton::Ok, MsgboxIcon::ExclamationRed)); + RefreshTable(); + return; + } + + Close(); +} + +void iwSaveAddonPreset::Msg_TableChooseItem(const unsigned /*ctrl_id*/, const unsigned /*selection*/) +{ + DoAction(); +} + +void iwSaveAddonPreset::Msg_MsgBoxResult(const unsigned msgbox_id, const MsgboxResult mbr) +{ + if(msgbox_id == ID_msgboxOverwrite) + { + if(mbr == MsgboxResult::Yes) + { + const bfs::path filePath = GetSaveFilePath(); + if(!filePath.empty()) + SaveToPath(filePath); + } + return; + } + iwAddonPresetsBase::Msg_MsgBoxResult(msgbox_id, mbr); +} + +// iwLoadAddonPreset +iwLoadAddonPreset::iwLoadAddonPreset(std::function&)> onLoad) + : iwAddonPresetsBase(_("Load Addon Preset"), _("Load")), onLoad_(std::move(onLoad)) +{} + +void iwLoadAddonPreset::DoAction() +{ + const bfs::path filePath = GetSelectedFilePath(); + if(filePath.empty()) + return; + + const auto states = LoadStatesFromFile(filePath); + if(!states) + { + WINDOWMANAGER.Show(std::make_unique( + _("Load Failed"), + _("The selected preset could not be loaded. The file may be corrupted or have an invalid format."), this, + MsgboxButton::Ok, MsgboxIcon::ExclamationRed)); + return; + } + + onLoad_(*states); + Close(); +} + +void iwLoadAddonPreset::Msg_TableChooseItem(const unsigned /*ctrl_id*/, const unsigned /*selection*/) +{ + DoAction(); +} diff --git a/libs/s25main/ingameWindows/iwAddonPresets.h b/libs/s25main/ingameWindows/iwAddonPresets.h new file mode 100644 index 0000000000..5060dff4a9 --- /dev/null +++ b/libs/s25main/ingameWindows/iwAddonPresets.h @@ -0,0 +1,57 @@ +// Copyright (C) 2005 - 2026 Settlers Freaks (sf-team at siedler25.org) +// +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "IngameWindow.h" +#include +#include +#include +#include +#include + +/// Base class for the save/load addon preset windows +class iwAddonPresetsBase : public IngameWindow +{ +public: + explicit iwAddonPresetsBase(const std::string& title, const std::string& actionLabel); + +protected: + void RefreshTable(); + boost::filesystem::path GetSelectedFilePath() const; + + void Msg_EditEnter(unsigned ctrl_id) override; + void Msg_ButtonClick(unsigned ctrl_id) override; + void Msg_TableSelectItem(unsigned ctrl_id, const boost::optional& selection) override; + void Msg_MsgBoxResult(unsigned msgbox_id, MsgboxResult mbr) override; + +private: + virtual void DoAction() = 0; + void ConfirmDelete(); +}; + +class iwSaveAddonPreset : public iwAddonPresetsBase +{ +public: + explicit iwSaveAddonPreset(std::map states); + +private: + const std::map states_; + boost::filesystem::path GetSaveFilePath() const; + void SaveToPath(const boost::filesystem::path& filePath); + void DoAction() override; + void Msg_MsgBoxResult(unsigned msgbox_id, MsgboxResult mbr) override; + void Msg_TableChooseItem(unsigned ctrl_id, unsigned selection) override; +}; + +class iwLoadAddonPreset : public iwAddonPresetsBase +{ +public: + explicit iwLoadAddonPreset(std::function&)> onLoad); + +private: + std::function&)> onLoad_; + void DoAction() override; + void Msg_TableChooseItem(unsigned ctrl_id, unsigned selection) override; +}; diff --git a/libs/s25main/ingameWindows/iwAddons.cpp b/libs/s25main/ingameWindows/iwAddons.cpp index df219d49bb..218954ccbb 100644 --- a/libs/s25main/ingameWindows/iwAddons.cpp +++ b/libs/s25main/ingameWindows/iwAddons.cpp @@ -5,10 +5,12 @@ #include "iwAddons.h" #include "GlobalGameSettings.h" #include "Loader.h" +#include "WindowManager.h" #include "addons/Addon.h" #include "controls/ctrlOptionGroup.h" #include "controls/ctrlScrollBar.h" #include "helpers/containerUtils.h" +#include "iwAddonPresets.h" #include "gameData/const_gui_ids.h" #include "s25util/colors.h" #include @@ -20,6 +22,8 @@ enum ID_btApply, ID_btAbort, ID_btS2Defaults, + ID_btSavePreset, + ID_btLoadPreset, ID_grpAddonGroup, ID_scroll, ID_grpAddonsStart @@ -32,24 +36,28 @@ constexpr unsigned AddonGuiLineHeight = 30; iwAddons::iwAddons(GlobalGameSettings& ggs, Window* parent, AddonChangeAllowed policy, std::vector whitelistedAddons) - : IngameWindow(CGI_ADDONS, IngameWindow::posLastOrCenter, Extent(700, 500), _("Addon Settings"), - LOADER.GetImageN("resource", 41), true, CloseBehavior::Custom, parent), + : IngameWindow(CGI_ADDONS, IngameWindow::posLastOrCenter, Extent(700, 530), _("Addon Settings"), + LOADER.GetImageN("resource", 41), false, CloseBehavior::Custom, parent), ggs(ggs), policy_(policy), whitelistedAddons_(std::move(whitelistedAddons)) { AddText(ID_txtAddFeatures, DrawPoint(20, 30), _("Additional features:"), COLOR_YELLOW, FontStyle{}, NormalFont); Extent btSize(200, 22); if(policy != AddonChangeAllowed::None) + { + AddTextButton(ID_btSavePreset, DrawPoint(20, GetSize().y - 70), btSize, TextureColor::Green2, _("Save"), + NormalFont, _("Save Addon Preset")); + AddTextButton(ID_btLoadPreset, DrawPoint(250, GetSize().y - 70), btSize, TextureColor::Green2, _("Load"), + NormalFont, _("Load Addon Preset")); + AddTextButton(ID_btS2Defaults, DrawPoint(480, GetSize().y - 70), btSize, TextureColor::Grey, _("Default"), + NormalFont, _("Use S2 Defaults")); AddTextButton(ID_btApply, DrawPoint(20, GetSize().y - 40), btSize, TextureColor::Green2, _("Apply"), NormalFont, _("Apply Changes")); + } AddTextButton(ID_btAbort, DrawPoint(250, GetSize().y - 40), btSize, TextureColor::Red1, _("Abort"), NormalFont, _("Close Without Saving")); - if(policy != AddonChangeAllowed::None) - AddTextButton(ID_btS2Defaults, DrawPoint(480, GetSize().y - 40), btSize, TextureColor::Grey, _("Default"), - NormalFont, _("Use S2 Defaults")); - // Kategorien ctrlOptionGroup* optiongroup = AddOptionGroup(ID_grpAddonGroup, GroupSelectType::Check); btSize = Extent(120, 22); @@ -71,7 +79,7 @@ iwAddons::iwAddons(GlobalGameSettings& ggs, Window* parent, AddonChangeAllowed p ctrlScrollBar* scrollbar = AddScrollBar(ID_scroll, DrawPoint(GetSize().x - SCROLLBAR_WIDTH - 20, 90), - Extent(SCROLLBAR_WIDTH, GetSize().y - 140), SCROLLBAR_WIDTH, TextureColor::Green2, 1); + Extent(SCROLLBAR_WIDTH, GetSize().y - 170), SCROLLBAR_WIDTH, TextureColor::Green2, 1); scrollbar->SetPageSize(scrollbar->GetSize().y / AddonGuiLineHeight); for(unsigned i = 0; i < ggs.getNumAddons(); ++i) @@ -125,6 +133,23 @@ void iwAddons::Msg_ButtonClick(const unsigned ctrl_id) Close(); break; + case ID_btSavePreset: + { + std::map states; + for(unsigned i = 0; i < ggs.getNumAddons(); ++i) + { + const auto& group = *GetCtrl(ID_grpAddonsStart + i); + states[static_cast(ggs.getAddon(i)->getId())] = addonGuis_[i]->getStatus(group); + } + WINDOWMANAGER.ReplaceWindow(std::make_unique(std::move(states))); + } + break; + + case ID_btLoadPreset: + WINDOWMANAGER.ReplaceWindow(std::make_unique( + [this](const std::map& states) { applyAddonStates(states); })); + break; + case ID_btS2Defaults: // Load S2 Defaults // Standardeinstellungen aufs Fenster übertragen for(unsigned i = 0; i < ggs.getNumAddons(); ++i) @@ -165,6 +190,21 @@ void iwAddons::UpdateView(const AddonGroup selection) scrollbar->SetRange(numAddonsInCurCategory); } +void iwAddons::applyAddonStates(const std::map& states) +{ + for(unsigned i = 0; i < ggs.getNumAddons(); ++i) + { + const Addon* addon = ggs.getAddon(i); + if(!isReadOnly(addon->getId())) + { + const auto it = states.find(static_cast(addon->getId())); + const unsigned rawStatus = (it != states.end()) ? it->second : addon->getDefaultStatus(); + const unsigned status = (rawStatus < addon->getNumOptions()) ? rawStatus : addon->getDefaultStatus(); + addonGuis_[i]->setStatus(*GetCtrl(ID_grpAddonsStart + i), status); + } + } +} + bool iwAddons::isReadOnly(AddonId id) const { return policy_ == AddonChangeAllowed::None diff --git a/libs/s25main/ingameWindows/iwAddons.h b/libs/s25main/ingameWindows/iwAddons.h index 7fbbc94621..c0c6de5bfa 100644 --- a/libs/s25main/ingameWindows/iwAddons.h +++ b/libs/s25main/ingameWindows/iwAddons.h @@ -6,6 +6,7 @@ #include "IngameWindow.h" #include "addons/const_addons.h" +#include #include #include @@ -46,4 +47,5 @@ class iwAddons : public IngameWindow /// Aktualisiert die Addons, die angezeigt werden sollen void UpdateView(AddonGroup selection); bool isReadOnly(AddonId) const; + void applyAddonStates(const std::map& states); }; From 903624e273ac04a182bded024685fc04325cc891 Mon Sep 17 00:00:00 2001 From: MichalLabuda <29762723+MichalLabuda@users.noreply.github.com> Date: Thu, 18 Jun 2026 11:25:09 +0200 Subject: [PATCH 2/3] Fix load/save addon preset window height --- libs/s25main/ingameWindows/iwAddonPresets.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/s25main/ingameWindows/iwAddonPresets.cpp b/libs/s25main/ingameWindows/iwAddonPresets.cpp index c19ed8c1ea..29ddc75d77 100644 --- a/libs/s25main/ingameWindows/iwAddonPresets.cpp +++ b/libs/s25main/ingameWindows/iwAddonPresets.cpp @@ -73,7 +73,7 @@ static std::optional> LoadStatesFromFile(const bfs: // iwAddonPresetsBase iwAddonPresetsBase::iwAddonPresetsBase(const std::string& title, const std::string& actionLabel) - : IngameWindow(CGI_ADDON_PRESETS, IngameWindow::posLastOrCenter, Extent(440, 320), title, + : IngameWindow(CGI_ADDON_PRESETS, IngameWindow::posLastOrCenter, Extent(440, 330), title, LOADER.GetImageN("resource", 41)) { using SRT = ctrlTable::SortType; From 8ca030e48433ecce40adc1b7a2d98710e3e7478e Mon Sep 17 00:00:00 2001 From: MichalLabuda <29762723+MichalLabuda@users.noreply.github.com> Date: Thu, 18 Jun 2026 14:12:48 +0200 Subject: [PATCH 3/3] Fix LoadPresetsFromFile name, explicitly reject path separators in preset name --- libs/s25main/ingameWindows/iwAddonPresets.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/libs/s25main/ingameWindows/iwAddonPresets.cpp b/libs/s25main/ingameWindows/iwAddonPresets.cpp index 29ddc75d77..6f6c3ec05b 100644 --- a/libs/s25main/ingameWindows/iwAddonPresets.cpp +++ b/libs/s25main/ingameWindows/iwAddonPresets.cpp @@ -43,7 +43,7 @@ static bfs::path GetPresetsDir() return RTTRCONFIG.ExpandPath(s25::folders::addonPresets); } -static std::optional> LoadStatesFromFile(const bfs::path& filePath) +static std::optional> LoadPresetsFromFile(const bfs::path& filePath) { libsiedler2::Archiv archive; if(libsiedler2::Load(filePath, archive) != 0) @@ -176,7 +176,6 @@ iwSaveAddonPreset::iwSaveAddonPreset(std::map states) bfs::path iwSaveAddonPreset::GetSaveFilePath() const { std::string name = GetCtrl(ID_edtName)->GetText(); - name = bfs::path(name).filename().string(); // Trim leading/trailing whitespace const auto first = name.find_first_not_of(" \t\r\n"); @@ -193,6 +192,15 @@ bfs::path iwSaveAddonPreset::GetSaveFilePath() const void iwSaveAddonPreset::DoAction() { + const std::string rawName = GetCtrl(ID_edtName)->GetText(); + if(rawName.find_first_of("/\\") != std::string::npos) + { + WINDOWMANAGER.Show(std::make_unique( + _("Invalid Name"), _("The preset name must not contain path separators. Please choose a different name."), + this, MsgboxButton::Ok, MsgboxIcon::ExclamationRed)); + return; + } + const bfs::path filePath = GetSaveFilePath(); if(filePath.empty()) return; @@ -274,7 +282,7 @@ void iwLoadAddonPreset::DoAction() if(filePath.empty()) return; - const auto states = LoadStatesFromFile(filePath); + const auto states = LoadPresetsFromFile(filePath); if(!states) { WINDOWMANAGER.Show(std::make_unique(