diff --git a/CHANGELOG.md b/CHANGELOG.md index 1afca19c6cb..21ddd7df266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -127,6 +127,7 @@ Feature #7499: OpenMW-CS: Generate record filters by drag & dropping cell content to the filters field Feature #7546: Start the game on Fredas Feature #7568: Uninterruptable scripted music + Feature #7608: Make the missing dependencies warning when loading a savegame more helpful Feature #7618: Show the player character's health in the save details Feature #7625: Add some missing console error outputs Feature #7634: Support NiParticleBomb diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index f225ebf24e2..0c60fe97781 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -254,8 +254,8 @@ namespace MWBase = 0; virtual void staticMessageBox(std::string_view message) = 0; virtual void removeStaticMessageBox() = 0; - virtual void interactiveMessageBox( - std::string_view message, const std::vector& buttons = {}, bool block = false) + virtual void interactiveMessageBox(std::string_view message, const std::vector& buttons = {}, + bool block = false, int defaultFocus = -1) = 0; /// returns the index of the pressed button or -1 if no button was pressed diff --git a/apps/openmw/mwgui/messagebox.cpp b/apps/openmw/mwgui/messagebox.cpp index 49d474c826e..b22fb873fa7 100644 --- a/apps/openmw/mwgui/messagebox.cpp +++ b/apps/openmw/mwgui/messagebox.cpp @@ -46,6 +46,20 @@ namespace MWGui mLastButtonPressed = -1; } + void MessageBoxManager::resetInteractiveMessageBox() + { + if (mInterMessageBoxe) + { + mInterMessageBoxe->setVisible(false); + mInterMessageBoxe.reset(); + } + } + + void MessageBoxManager::setLastButtonPressed(int index) + { + mLastButtonPressed = index; + } + void MessageBoxManager::onFrame(float frameDuration) { for (auto it = mMessageBoxes.begin(); it != mMessageBoxes.end();) @@ -112,7 +126,7 @@ namespace MWGui } bool MessageBoxManager::createInteractiveMessageBox( - std::string_view message, const std::vector& buttons) + std::string_view message, const std::vector& buttons, bool immediate, int defaultFocus) { if (mInterMessageBoxe != nullptr) { @@ -120,7 +134,8 @@ namespace MWGui mInterMessageBoxe->setVisible(false); } - mInterMessageBoxe = std::make_unique(*this, std::string{ message }, buttons); + mInterMessageBoxe + = std::make_unique(*this, std::string{ message }, buttons, immediate, defaultFocus); mLastButtonPressed = -1; return true; @@ -200,13 +215,15 @@ namespace MWGui mMainWidget->setVisible(value); } - InteractiveMessageBox::InteractiveMessageBox( - MessageBoxManager& parMessageBoxManager, const std::string& message, const std::vector& buttons) + InteractiveMessageBox::InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message, + const std::vector& buttons, bool immediate, int defaultFocus) : WindowModal(MWBase::Environment::get().getWindowManager()->isGuiMode() ? "openmw_interactive_messagebox_notransp.layout" : "openmw_interactive_messagebox.layout") , mMessageBoxManager(parMessageBoxManager) , mButtonPressed(-1) + , mDefaultFocus(defaultFocus) + , mImmediate(immediate) { int textPadding = 10; // padding between text-widget and main-widget int textButtonPadding = 10; // padding between the text-widget und the button-widget @@ -363,6 +380,9 @@ namespace MWGui MyGUI::Widget* InteractiveMessageBox::getDefaultKeyFocus() { std::vector keywords{ "sOk", "sYes" }; + if (mDefaultFocus >= 0 && mDefaultFocus < static_cast(mButtons.size())) + return mButtons[mDefaultFocus]; + for (MyGUI::Button* button : mButtons) { for (const std::string& keyword : keywords) @@ -393,6 +413,12 @@ namespace MWGui { mButtonPressed = index; mMessageBoxManager.onButtonPressed(mButtonPressed); + if (!mImmediate) + return; + + mMessageBoxManager.setLastButtonPressed(mButtonPressed); + MWBase::Environment::get().getInputManager()->changeInputMode( + MWBase::Environment::get().getWindowManager()->isGuiMode()); return; } index++; diff --git a/apps/openmw/mwgui/messagebox.hpp b/apps/openmw/mwgui/messagebox.hpp index b10586549f8..bb61bd6bd99 100644 --- a/apps/openmw/mwgui/messagebox.hpp +++ b/apps/openmw/mwgui/messagebox.hpp @@ -25,7 +25,8 @@ namespace MWGui void onFrame(float frameDuration); void createMessageBox(std::string_view message, bool stat = false); void removeStaticMessageBox(); - bool createInteractiveMessageBox(std::string_view message, const std::vector& buttons); + bool createInteractiveMessageBox(std::string_view message, const std::vector& buttons, + bool immediate = false, int defaultFocus = -1); bool isInteractiveMessageBox(); int getMessagesCount(); @@ -40,6 +41,10 @@ namespace MWGui /// @param reset Reset the pressed button to -1 after reading it. int readPressedButton(bool reset = true); + void resetInteractiveMessageBox(); + + void setLastButtonPressed(int index); + typedef MyGUI::delegates::MultiDelegate EventHandle_Int; // Note: this delegate unassigns itself after it was fired, i.e. works once. @@ -88,7 +93,7 @@ namespace MWGui { public: InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message, - const std::vector& buttons); + const std::vector& buttons, bool immediate, int defaultFocus); void mousePressed(MyGUI::Widget* _widget); int readPressedButton(); @@ -107,6 +112,8 @@ namespace MWGui std::vector mButtons; int mButtonPressed; + int mDefaultFocus; + bool mImmediate; }; } diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index d98b7472bb1..7b3d35e0111 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -744,9 +744,9 @@ namespace MWGui } void WindowManager::interactiveMessageBox( - std::string_view message, const std::vector& buttons, bool block) + std::string_view message, const std::vector& buttons, bool block, int defaultFocus) { - mMessageBoxManager->createInteractiveMessageBox(message, buttons); + mMessageBoxManager->createInteractiveMessageBox(message, buttons, block, defaultFocus); updateVisible(); if (block) @@ -779,6 +779,8 @@ namespace MWGui frameRateLimiter.limit(); } + + mMessageBoxManager->resetInteractiveMessageBox(); } } diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 5f6b12b7e58..7ee0554a266 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -268,8 +268,8 @@ namespace MWGui enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) override; void staticMessageBox(std::string_view message) override; void removeStaticMessageBox() override; - void interactiveMessageBox( - std::string_view message, const std::vector& buttons = {}, bool block = false) override; + void interactiveMessageBox(std::string_view message, const std::vector& buttons = {}, + bool block = false, int defaultFocus = -1) override; int readPressedButton() override; ///< returns the index of the pressed button or -1 if no button was pressed ///< (->MessageBoxmanager->InteractiveMessageBox) diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index fb3590a3f00..826c0dbba64 100644 --- a/apps/openmw/mwstate/statemanagerimp.cpp +++ b/apps/openmw/mwstate/statemanagerimp.cpp @@ -2,6 +2,8 @@ #include +#include + #include #include @@ -440,7 +442,9 @@ void MWState::StateManager::loadGame(const Character* character, const std::file { ESM::SavedGame profile; profile.load(reader); - if (!verifyProfile(profile)) + const auto& selectedContentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); + auto missingFiles = profile.getMissingContentFiles(selectedContentFiles); + if (!missingFiles.empty() && !confirmLoading(missingFiles)) { cleanup(true); MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); @@ -668,30 +672,66 @@ void MWState::StateManager::update(float duration) } } -bool MWState::StateManager::verifyProfile(const ESM::SavedGame& profile) const +bool MWState::StateManager::confirmLoading(const std::vector& missingFiles) const { - const std::vector& selectedContentFiles = MWBase::Environment::get().getWorld()->getContentFiles(); - bool notFound = false; - for (const std::string& contentFile : profile.mContentFiles) + std::ostringstream stream; + for (auto& contentFile : missingFiles) + { + Log(Debug::Warning) << "Warning: Saved game dependency " << contentFile << " is missing."; + stream << contentFile << "\n"; + } + + auto fullList = stream.str(); + if (!fullList.empty()) + fullList.pop_back(); + + constexpr size_t missingPluginsDisplayLimit = 12; + + std::vector buttons; + buttons.emplace_back("#{Interface:Yes}"); + buttons.emplace_back("#{Interface:Copy}"); + buttons.emplace_back("#{Interface:No}"); + std::string message = "#{OMWEngine:MissingContentFilesConfirmation}"; + + auto l10n = MWBase::Environment::get().getL10nManager()->getContext("OMWEngine"); + message += l10n->formatMessage("MissingContentFilesList", { "files" }, { static_cast(missingFiles.size()) }); + auto cappedSize = std::min(missingFiles.size(), missingPluginsDisplayLimit); + if (cappedSize == missingFiles.size()) + { + message += fullList; + } + else { - if (std::find(selectedContentFiles.begin(), selectedContentFiles.end(), contentFile) - == selectedContentFiles.end()) + for (size_t i = 0; i < cappedSize - 1; ++i) { - Log(Debug::Warning) << "Warning: Saved game dependency " << contentFile << " is missing."; - notFound = true; + message += missingFiles[i]; + message += "\n"; } + + message += "..."; } - if (notFound) + + message + += l10n->formatMessage("MissingContentFilesListCopy", { "files" }, { static_cast(missingFiles.size()) }); + + int selectedButton = -1; + while (true) { - std::vector buttons; - buttons.emplace_back("#{Interface:Yes}"); - buttons.emplace_back("#{Interface:No}"); - MWBase::Environment::get().getWindowManager()->interactiveMessageBox( - "#{OMWEngine:MissingContentFilesConfirmation}", buttons, true); - int selectedButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); - if (selectedButton == 1 || selectedButton == -1) - return false; + auto windowManager = MWBase::Environment::get().getWindowManager(); + windowManager->interactiveMessageBox(message, buttons, true, selectedButton); + selectedButton = windowManager->readPressedButton(); + if (selectedButton == 0) + break; + + if (selectedButton == 1) + { + SDL_SetClipboardText(fullList.c_str()); + continue; + } + + return false; } + return true; } diff --git a/apps/openmw/mwstate/statemanagerimp.hpp b/apps/openmw/mwstate/statemanagerimp.hpp index df62ca7ebfc..c293209f34d 100644 --- a/apps/openmw/mwstate/statemanagerimp.hpp +++ b/apps/openmw/mwstate/statemanagerimp.hpp @@ -21,7 +21,7 @@ namespace MWState private: void cleanup(bool force = false); - bool verifyProfile(const ESM::SavedGame& profile) const; + bool confirmLoading(const std::vector& missingFiles) const; void writeScreenshot(std::vector& imageData) const; diff --git a/components/esm3/savedgame.cpp b/components/esm3/savedgame.cpp index e84cb27ad86..cec2b5e1892 100644 --- a/components/esm3/savedgame.cpp +++ b/components/esm3/savedgame.cpp @@ -61,4 +61,18 @@ namespace ESM esm.writeHNT("MHLT", mMaximumHealth); } + std::vector SavedGame::getMissingContentFiles( + const std::vector& allContentFiles) const + { + std::vector missingFiles; + for (const std::string& contentFile : mContentFiles) + { + if (std::find(allContentFiles.begin(), allContentFiles.end(), contentFile) == allContentFiles.end()) + { + missingFiles.emplace_back(contentFile); + } + } + + return missingFiles; + } } diff --git a/components/esm3/savedgame.hpp b/components/esm3/savedgame.hpp index 4632e98927b..f174340203a 100644 --- a/components/esm3/savedgame.hpp +++ b/components/esm3/savedgame.hpp @@ -40,6 +40,8 @@ namespace ESM void load(ESMReader& esm); void save(ESMWriter& esm) const; + + std::vector getMissingContentFiles(const std::vector& allContentFiles) const; }; } diff --git a/files/data/l10n/Interface/de.yaml b/files/data/l10n/Interface/de.yaml index 1cabad01a93..ac1a95a0eac 100644 --- a/files/data/l10n/Interface/de.yaml +++ b/files/data/l10n/Interface/de.yaml @@ -25,3 +25,4 @@ Yes: "Ja" #OK: "OK" #Off: "Off" #On: "On" +#Copy: "Copy" diff --git a/files/data/l10n/Interface/en.yaml b/files/data/l10n/Interface/en.yaml index df450b5c38d..82c1aeba1a7 100644 --- a/files/data/l10n/Interface/en.yaml +++ b/files/data/l10n/Interface/en.yaml @@ -22,3 +22,4 @@ None: "None" OK: "OK" Cancel: "Cancel" Close: "Close" +Copy: "Copy" diff --git a/files/data/l10n/Interface/fr.yaml b/files/data/l10n/Interface/fr.yaml index 5aa0260680b..bac43463644 100644 --- a/files/data/l10n/Interface/fr.yaml +++ b/files/data/l10n/Interface/fr.yaml @@ -22,3 +22,4 @@ None: "Aucun" OK: "Valider" Cancel: "Annuler" Close: "Fermer" +#Copy: "Copy" diff --git a/files/data/l10n/Interface/ru.yaml b/files/data/l10n/Interface/ru.yaml index 6d81dd77973..44b38a77b85 100644 --- a/files/data/l10n/Interface/ru.yaml +++ b/files/data/l10n/Interface/ru.yaml @@ -1,5 +1,6 @@ Cancel: "Отмена" Close: "Закрыть" +Copy: "Скопировать" DurationDay: "{days} д " DurationHour: "{hours} ч " DurationMinute: "{minutes} мин " diff --git a/files/data/l10n/Interface/sv.yaml b/files/data/l10n/Interface/sv.yaml index 5e9260cf976..aae63a1941c 100644 --- a/files/data/l10n/Interface/sv.yaml +++ b/files/data/l10n/Interface/sv.yaml @@ -14,3 +14,4 @@ Off: "Av" On: "På" Reset: "Återställ" Yes: "Ja" +#Copy: "Copy" diff --git a/files/data/l10n/OMWEngine/de.yaml b/files/data/l10n/OMWEngine/de.yaml index f2b4ee7e5a6..26838bd93cd 100644 --- a/files/data/l10n/OMWEngine/de.yaml +++ b/files/data/l10n/OMWEngine/de.yaml @@ -41,10 +41,22 @@ TimePlayed: "Spielzeit" #DeleteGameConfirmation: "Are you sure you want to delete this saved game?" #EmptySaveNameError: "Game can not be saved without a name!" #LoadGameConfirmation: "Do you want to load a saved game and lose the current one?" -#MissingContentFilesConfirmation: | +#MissingContentFilesConfirmation: |- # The currently selected content files do not match the ones used by this save game. # Errors may occur during load or game play. # Do you wish to continue? +#MissingContentFilesList: |- +# {files, plural, +# one{\n\nFound missing file: } +# few{\n\nFound {files} missing files:\n} +# other{\n\nFound {files} missing files:\n} +# } +#MissingContentFilesListCopy: |- +# {files, plural, +# one{\n\nPress Copy to place its name to the clipboard.} +# few{\n\nPress Copy to place their names to the clipboard.} +# other{\n\nPress Copy to place their names to the clipboard.} +# } #OverwriteGameConfirmation: "Are you sure you want to overwrite this saved game?" diff --git a/files/data/l10n/OMWEngine/en.yaml b/files/data/l10n/OMWEngine/en.yaml index 08df886f18f..ee2a33ee71c 100644 --- a/files/data/l10n/OMWEngine/en.yaml +++ b/files/data/l10n/OMWEngine/en.yaml @@ -34,10 +34,22 @@ DeleteGame: "Delete Game" DeleteGameConfirmation: "Are you sure you want to delete this saved game?" EmptySaveNameError: "Game can not be saved without a name!" LoadGameConfirmation: "Do you want to load a saved game and lose the current one?" -MissingContentFilesConfirmation: | +MissingContentFilesConfirmation: |- The currently selected content files do not match the ones used by this save game. Errors may occur during load or game play. Do you wish to continue? +MissingContentFilesList: |- + {files, plural, + one{\n\nFound missing file: } + few{\n\nFound {files} missing files:\n} + other{\n\nFound {files} missing files:\n} + } +MissingContentFilesListCopy: |- + {files, plural, + one{\n\nPress Copy to place its name to the clipboard.} + few{\n\nPress Copy to place their names to the clipboard.} + other{\n\nPress Copy to place their names to the clipboard.} + } OverwriteGameConfirmation: "Are you sure you want to overwrite this saved game?" SelectCharacter: "Select Character..." TimePlayed: "Time played" diff --git a/files/data/l10n/OMWEngine/fr.yaml b/files/data/l10n/OMWEngine/fr.yaml index 5a6209b44c0..689ccc59a52 100644 --- a/files/data/l10n/OMWEngine/fr.yaml +++ b/files/data/l10n/OMWEngine/fr.yaml @@ -37,10 +37,22 @@ DeleteGame: "Supprimer la partie" DeleteGameConfirmation: "Voulez-vous réellement supprimer cette partie sauvegardée ?" EmptySaveNameError: "Impossible de sauvegarder une partie lui donner un nom !" LoadGameConfirmation: "Voulez-vous charger cette autre partie ? Toute progression non sauvegardée sera perdue." -MissingContentFilesConfirmation: | +MissingContentFilesConfirmation: |- Les données de jeu actuellement sélectionnées ne correspondent pas à celle indiquée dans cette sauvegarde. Cela peut entraîner des erreurs lors du chargement, mais aussi lors de votre partie. Voulez-vous continuer ? +#MissingContentFilesList: |- +# {files, plural, +# one{\n\nFound missing file: } +# few{\n\nFound {files} missing files:\n} +# other{\n\nFound {files} missing files:\n} +# } +#MissingContentFilesListCopy: |- +# {files, plural, +# one{\n\nPress Copy to place its name to the clipboard.} +# few{\n\nPress Copy to place their names to the clipboard.} +# other{\n\nPress Copy to place their names to the clipboard.} +# } OverwriteGameConfirmation: "Écraser la sauvegarde précédente ?" diff --git a/files/data/l10n/OMWEngine/ru.yaml b/files/data/l10n/OMWEngine/ru.yaml index cbc71f91e44..b645b681b14 100644 --- a/files/data/l10n/OMWEngine/ru.yaml +++ b/files/data/l10n/OMWEngine/ru.yaml @@ -34,10 +34,22 @@ DeleteGame: "Удалить игру" DeleteGameConfirmation: "Вы уверены, что хотите удалить это сохранение?" EmptySaveNameError: "Имя сохранения не может быть пустым!" LoadGameConfirmation: "Вы хотите загрузить сохранение? Текущая игра будет потеряна." -MissingContentFilesConfirmation: | +MissingContentFilesConfirmation: |- Выбранные ESM/ESP файлы не соответствуют тем, которые использовались для этого сохранения. Во время загрузки или в процессе игры могут возникнуть ошибки. Вы хотите продолжить? +MissingContentFilesList: |- + {files, plural, + one{\n\nОтсутствует файл } + few{\n\nОтсутствуют {files} файла:\n} + other{\n\nОтсутствуют {files} файлов:\n} + } +MissingContentFilesListCopy: |- + {files, plural, + one{\n\nНажмите Скопировать, чтобы поместить его название в буфер обмена.} + few{\n\nНажмите Скопировать, чтобы поместить их названия в буфер обмена.} + other{\n\nНажмите Скопировать, чтобы поместить их названия в буфер обмена.} + } OverwriteGameConfirmation: "Вы уверены, что хотите перезаписать это сохранение?" SelectCharacter: "Выберите персонажа..." TimePlayed: "Время в игре" diff --git a/files/data/l10n/OMWEngine/sv.yaml b/files/data/l10n/OMWEngine/sv.yaml index 4ee69f70c7f..f4c9db031a8 100644 --- a/files/data/l10n/OMWEngine/sv.yaml +++ b/files/data/l10n/OMWEngine/sv.yaml @@ -35,10 +35,22 @@ DeleteGame: "Radera spel" DeleteGameConfirmation: "Är du säker på att du vill radera sparfilen?" EmptySaveNameError: "Spelet kan inte sparas utan ett namn!" LoadGameConfirmation: "Vill du ladda ett sparat spel och förlora det pågående spelet?" -MissingContentFilesConfirmation: | +MissingContentFilesConfirmation: |- De valda innehållsfilerna matchar inte filerna som används av denna sparfil. Fel kan uppstå vid laddning eller under spel. Vill du fortsätta? +#MissingContentFilesList: |- +# {files, plural, +# one{\n\nFound missing file: } +# few{\n\nFound {files} missing files:\n} +# other{\n\nFound {files} missing files:\n} +# } +#MissingContentFilesListCopy: |- +# {files, plural, +# one{\n\nPress Copy to place its name to the clipboard.} +# few{\n\nPress Copy to place their names to the clipboard.} +# other{\n\nPress Copy to place their names to the clipboard.} +# } OverwriteGameConfirmation: "Är du säker på att du vill skriva över det här sparade spelet?" SelectCharacter: "Välj spelfigur..."