diff --git a/CHANGELOG.md b/CHANGELOG.md index 1afca19c6c..21ddd7df26 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/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp index fb3590a3f0..5ee5b21aa2 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,64 @@ 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) { - if (std::find(selectedContentFiles.begin(), selectedContentFiles.end(), contentFile) - == selectedContentFiles.end()) + 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 + { + 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()) }); + + while (true) { - std::vector buttons; - buttons.emplace_back("#{Interface:Yes}"); - buttons.emplace_back("#{Interface:No}"); - MWBase::Environment::get().getWindowManager()->interactiveMessageBox( - "#{OMWEngine:MissingContentFilesConfirmation}", buttons, true); + MWBase::Environment::get().getWindowManager()->interactiveMessageBox(message, buttons, true); int selectedButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); - if (selectedButton == 1 || selectedButton == -1) - return false; + 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 df62ca7ebf..c293209f34 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 e84cb27ad8..cec2b5e189 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 4632e98927..f174340203 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 1cabad01a9..ac1a95a0ea 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 df450b5c38..82c1aeba1a 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 5aa0260680..bac4346364 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 6d81dd7797..44b38a77b8 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 5e9260cf97..aae63a1941 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 f2b4ee7e5a..26838bd93c 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 08df886f18..ee2a33ee71 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 5a6209b44c..689ccc59a5 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 cbc71f91e4..b645b681b1 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 1ee8bdc707..b9d2d44076 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..."