mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-01-06 00:55:50 +00:00
Merge branch 'misisng_plugins' into 'master'
Display missing plugins upon savegame loading Closes #7608 See merge request OpenMW/openmw!3594
This commit is contained in:
commit
6fb6c7a32f
@ -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
|
||||
|
@ -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<std::string>& buttons = {}, bool block = false)
|
||||
virtual void interactiveMessageBox(std::string_view message, const std::vector<std::string>& buttons = {},
|
||||
bool block = false, int defaultFocus = -1)
|
||||
= 0;
|
||||
|
||||
/// returns the index of the pressed button or -1 if no button was pressed
|
||||
|
@ -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<std::string>& buttons)
|
||||
std::string_view message, const std::vector<std::string>& buttons, bool immediate, int defaultFocus)
|
||||
{
|
||||
if (mInterMessageBoxe != nullptr)
|
||||
{
|
||||
@ -120,7 +134,8 @@ namespace MWGui
|
||||
mInterMessageBoxe->setVisible(false);
|
||||
}
|
||||
|
||||
mInterMessageBoxe = std::make_unique<InteractiveMessageBox>(*this, std::string{ message }, buttons);
|
||||
mInterMessageBoxe
|
||||
= std::make_unique<InteractiveMessageBox>(*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<std::string>& buttons)
|
||||
InteractiveMessageBox::InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message,
|
||||
const std::vector<std::string>& 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<std::string> keywords{ "sOk", "sYes" };
|
||||
if (mDefaultFocus >= 0 && mDefaultFocus < static_cast<int>(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++;
|
||||
|
@ -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<std::string>& buttons);
|
||||
bool createInteractiveMessageBox(std::string_view message, const std::vector<std::string>& 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<int> 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<std::string>& buttons);
|
||||
const std::vector<std::string>& buttons, bool immediate, int defaultFocus);
|
||||
void mousePressed(MyGUI::Widget* _widget);
|
||||
int readPressedButton();
|
||||
|
||||
@ -107,6 +112,8 @@ namespace MWGui
|
||||
std::vector<MyGUI::Button*> mButtons;
|
||||
|
||||
int mButtonPressed;
|
||||
int mDefaultFocus;
|
||||
bool mImmediate;
|
||||
};
|
||||
|
||||
}
|
||||
|
@ -744,9 +744,9 @@ namespace MWGui
|
||||
}
|
||||
|
||||
void WindowManager::interactiveMessageBox(
|
||||
std::string_view message, const std::vector<std::string>& buttons, bool block)
|
||||
std::string_view message, const std::vector<std::string>& 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<std::string>& buttons = {}, bool block = false) override;
|
||||
void interactiveMessageBox(std::string_view message, const std::vector<std::string>& 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)
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include <SDL_clipboard.h>
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
|
||||
#include <components/esm3/esmreader.hpp>
|
||||
@ -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<std::string_view>& missingFiles) const
|
||||
{
|
||||
const std::vector<std::string>& 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<std::string> 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<int>(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<int>(missingFiles.size()) });
|
||||
|
||||
int selectedButton = -1;
|
||||
while (true)
|
||||
{
|
||||
std::vector<std::string> 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;
|
||||
}
|
||||
|
||||
|
@ -21,7 +21,7 @@ namespace MWState
|
||||
private:
|
||||
void cleanup(bool force = false);
|
||||
|
||||
bool verifyProfile(const ESM::SavedGame& profile) const;
|
||||
bool confirmLoading(const std::vector<std::string_view>& missingFiles) const;
|
||||
|
||||
void writeScreenshot(std::vector<char>& imageData) const;
|
||||
|
||||
|
@ -61,4 +61,18 @@ namespace ESM
|
||||
esm.writeHNT("MHLT", mMaximumHealth);
|
||||
}
|
||||
|
||||
std::vector<std::string_view> SavedGame::getMissingContentFiles(
|
||||
const std::vector<std::string>& allContentFiles) const
|
||||
{
|
||||
std::vector<std::string_view> missingFiles;
|
||||
for (const std::string& contentFile : mContentFiles)
|
||||
{
|
||||
if (std::find(allContentFiles.begin(), allContentFiles.end(), contentFile) == allContentFiles.end())
|
||||
{
|
||||
missingFiles.emplace_back(contentFile);
|
||||
}
|
||||
}
|
||||
|
||||
return missingFiles;
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,8 @@ namespace ESM
|
||||
|
||||
void load(ESMReader& esm);
|
||||
void save(ESMWriter& esm) const;
|
||||
|
||||
std::vector<std::string_view> getMissingContentFiles(const std::vector<std::string>& allContentFiles) const;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -25,3 +25,4 @@ Yes: "Ja"
|
||||
#OK: "OK"
|
||||
#Off: "Off"
|
||||
#On: "On"
|
||||
#Copy: "Copy"
|
||||
|
@ -22,3 +22,4 @@ None: "None"
|
||||
OK: "OK"
|
||||
Cancel: "Cancel"
|
||||
Close: "Close"
|
||||
Copy: "Copy"
|
||||
|
@ -22,3 +22,4 @@ None: "Aucun"
|
||||
OK: "Valider"
|
||||
Cancel: "Annuler"
|
||||
Close: "Fermer"
|
||||
#Copy: "Copy"
|
||||
|
@ -1,5 +1,6 @@
|
||||
Cancel: "Отмена"
|
||||
Close: "Закрыть"
|
||||
Copy: "Скопировать"
|
||||
DurationDay: "{days} д "
|
||||
DurationHour: "{hours} ч "
|
||||
DurationMinute: "{minutes} мин "
|
||||
|
@ -14,3 +14,4 @@ Off: "Av"
|
||||
On: "På"
|
||||
Reset: "Återställ"
|
||||
Yes: "Ja"
|
||||
#Copy: "Copy"
|
||||
|
@ -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?"
|
||||
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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 ?"
|
||||
|
||||
|
||||
|
@ -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: "Время в игре"
|
||||
|
@ -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..."
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user