-
Notifications
You must be signed in to change notification settings - Fork 102
Addon presets #1951
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MichalLabuda
wants to merge
20
commits into
Return-To-The-Roots:master
Choose a base branch
from
MichalLabuda:addon-presets
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Addon presets #1951
Changes from 3 commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
2dae906
Addon presets
MichalLabuda 903624e
Fix load/save addon preset window height
MichalLabuda 8ca030e
Fix LoadPresetsFromFile name, explicitly reject path separators in pr…
MichalLabuda 79b516a
Merge remote-tracking branch 'origin/master' into addon-presets
MichalLabuda 15e9bd2
Bump libutil submodule to latest master
MichalLabuda ef88aa5
Merge remote-tracking branch 'origin/master' into addon-presets
MichalLabuda 3d758c8
Centralize filename validation using libutil isValidFileName
MichalLabuda 133b0dd
Merge remote-tracking branch 'origin/master' into addon-presets
MichalLabuda 0fd4143
ctrlEdit: add EditType enum, add GetFileName()
MichalLabuda 1b1679f
ctrlEdit: cover case-insensitive ext test
MichalLabuda 2cc6a38
ctrlEdit: assert Filename type in GetFileName()
MichalLabuda aaa399f
Fix addon preset window lifetime issues
MichalLabuda 18fecb2
Addon presets: name edit drives load/delete
MichalLabuda b6cdb7f
Test selection/deselection driving addon preset name edit
MichalLabuda e71bbc8
Merge branch 'master' into addon-presets
MichalLabuda 997c45d
Fix GCC -Werror build and apply clang-format
MichalLabuda ca42a7e
Stack new modals on top; fix preset name matching
MichalLabuda 6e5f30a
Merge branch 'master' into addon-presets
MichalLabuda 5ae47af
Merge branch 'master' into addon-presets
MichalLabuda 320f32b
Merge branch 'master' into addon-presets
MichalLabuda File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,302 @@ | ||
| // 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 <boost/filesystem.hpp> | ||
| #include <optional> | ||
|
|
||
| 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<std::map<unsigned, unsigned>> LoadPresetsFromFile(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<const libsiedler2::ArchivItem_Ini*>(archive.find("addons")); | ||
| if(!ini) | ||
| return std::nullopt; | ||
|
|
||
| std::map<unsigned, unsigned> states; | ||
| for(unsigned i = 0; i < ini->size(); ++i) | ||
| { | ||
| const auto* item = dynamic_cast<const libsiedler2::ArchivItem_Text*>(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, 330), 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(); | ||
|
Flamefire marked this conversation as resolved.
Outdated
|
||
| 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<ctrlTable>(ID_tblPresets); | ||
|
Flamefire marked this conversation as resolved.
Outdated
|
||
| 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<ctrlTable>(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<unsigned>& selection) | ||
| { | ||
| const auto* table = GetCtrl<ctrlTable>(ID_tblPresets); | ||
| GetCtrl<ctrlEdit>(ID_edtName)->SetText(selection ? table->GetItemText(*selection, 0) : ""); | ||
| } | ||
|
|
||
| void iwAddonPresetsBase::ConfirmDelete() | ||
| { | ||
| if(GetSelectedFilePath().empty()) | ||
| return; | ||
| WINDOWMANAGER.Show(std::make_unique<iwMsgbox>(_("Delete Preset"), | ||
| _("Are you sure you want to delete the selected preset?"), this, | ||
|
Flamefire marked this conversation as resolved.
Outdated
|
||
| 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<iwMsgbox>(_("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<ctrlEdit>(ID_edtName)->SetText(""); | ||
| return; | ||
|
Flamefire marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| RefreshTable(); | ||
| GetCtrl<ctrlEdit>(ID_edtName)->SetText(""); | ||
| } | ||
|
|
||
| // iwSaveAddonPreset | ||
| iwSaveAddonPreset::iwSaveAddonPreset(std::map<unsigned, unsigned> states) | ||
| : iwAddonPresetsBase(_("Save Addon Preset"), _("Save")), states_(std::move(states)) | ||
| {} | ||
|
|
||
| bfs::path iwSaveAddonPreset::GetSaveFilePath() const | ||
| { | ||
| std::string name = GetCtrl<ctrlEdit>(ID_edtName)->GetText(); | ||
|
|
||
| // 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 std::string rawName = GetCtrl<ctrlEdit>(ID_edtName)->GetText(); | ||
|
MichalLabuda marked this conversation as resolved.
Outdated
|
||
| if(rawName.find_first_of("/\\") != std::string::npos) | ||
| { | ||
| WINDOWMANAGER.Show(std::make_unique<iwMsgbox>( | ||
| _("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; | ||
|
|
||
| // 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()))) | ||
|
MichalLabuda marked this conversation as resolved.
Outdated
|
||
| { | ||
| WINDOWMANAGER.Show(std::make_unique<iwMsgbox>( | ||
| _("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<iwMsgbox>( | ||
| _("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<libsiedler2::ArchivItem_Ini>("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<iwMsgbox>( | ||
| _("Save Failed"), _("Failed to save the preset. Please check the filename and try again."), this, | ||
| MsgboxButton::Ok, MsgboxIcon::ExclamationRed)); | ||
| RefreshTable(); | ||
| return; | ||
|
Flamefire marked this conversation as resolved.
|
||
| } | ||
|
|
||
| Close(); | ||
| } | ||
|
|
||
| void iwSaveAddonPreset::Msg_TableChooseItem(const unsigned /*ctrl_id*/, const unsigned /*selection*/) | ||
| { | ||
| DoAction(); | ||
|
Flamefire marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| 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); | ||
|
Flamefire marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| // iwLoadAddonPreset | ||
| iwLoadAddonPreset::iwLoadAddonPreset(std::function<void(const std::map<unsigned, unsigned>&)> 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 = LoadPresetsFromFile(filePath); | ||
| if(!states) | ||
| { | ||
| WINDOWMANAGER.Show(std::make_unique<iwMsgbox>( | ||
| _("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(); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 <boost/filesystem/path.hpp> | ||
| #include <boost/optional.hpp> | ||
| #include <functional> | ||
| #include <map> | ||
| #include <string> | ||
|
|
||
| /// 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<unsigned>& 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<unsigned, unsigned> states); | ||
|
|
||
| private: | ||
| const std::map<unsigned, unsigned> 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<void(const std::map<unsigned, unsigned>&)> onLoad); | ||
|
|
||
| private: | ||
| std::function<void(const std::map<unsigned, unsigned>&)> onLoad_; | ||
| void DoAction() override; | ||
| void Msg_TableChooseItem(unsigned ctrl_id, unsigned selection) override; | ||
| }; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.