1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-02-22 18:39:42 +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:
Zackhasacat 2023-11-20 00:19:43 +00:00
commit 6fb6c7a32f
20 changed files with 193 additions and 36 deletions

View file

@ -127,6 +127,7 @@
Feature #7499: OpenMW-CS: Generate record filters by drag & dropping cell content to the filters field Feature #7499: OpenMW-CS: Generate record filters by drag & dropping cell content to the filters field
Feature #7546: Start the game on Fredas Feature #7546: Start the game on Fredas
Feature #7568: Uninterruptable scripted music 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 #7618: Show the player character's health in the save details
Feature #7625: Add some missing console error outputs Feature #7625: Add some missing console error outputs
Feature #7634: Support NiParticleBomb Feature #7634: Support NiParticleBomb

View file

@ -254,8 +254,8 @@ namespace MWBase
= 0; = 0;
virtual void staticMessageBox(std::string_view message) = 0; virtual void staticMessageBox(std::string_view message) = 0;
virtual void removeStaticMessageBox() = 0; virtual void removeStaticMessageBox() = 0;
virtual void interactiveMessageBox( virtual void interactiveMessageBox(std::string_view message, const std::vector<std::string>& buttons = {},
std::string_view message, const std::vector<std::string>& buttons = {}, bool block = false) bool block = false, int defaultFocus = -1)
= 0; = 0;
/// returns the index of the pressed button or -1 if no button was pressed /// returns the index of the pressed button or -1 if no button was pressed

View file

@ -46,6 +46,20 @@ namespace MWGui
mLastButtonPressed = -1; mLastButtonPressed = -1;
} }
void MessageBoxManager::resetInteractiveMessageBox()
{
if (mInterMessageBoxe)
{
mInterMessageBoxe->setVisible(false);
mInterMessageBoxe.reset();
}
}
void MessageBoxManager::setLastButtonPressed(int index)
{
mLastButtonPressed = index;
}
void MessageBoxManager::onFrame(float frameDuration) void MessageBoxManager::onFrame(float frameDuration)
{ {
for (auto it = mMessageBoxes.begin(); it != mMessageBoxes.end();) for (auto it = mMessageBoxes.begin(); it != mMessageBoxes.end();)
@ -112,7 +126,7 @@ namespace MWGui
} }
bool MessageBoxManager::createInteractiveMessageBox( 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) if (mInterMessageBoxe != nullptr)
{ {
@ -120,7 +134,8 @@ namespace MWGui
mInterMessageBoxe->setVisible(false); 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; mLastButtonPressed = -1;
return true; return true;
@ -200,13 +215,15 @@ namespace MWGui
mMainWidget->setVisible(value); mMainWidget->setVisible(value);
} }
InteractiveMessageBox::InteractiveMessageBox( InteractiveMessageBox::InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message,
MessageBoxManager& parMessageBoxManager, const std::string& message, const std::vector<std::string>& buttons) const std::vector<std::string>& buttons, bool immediate, int defaultFocus)
: WindowModal(MWBase::Environment::get().getWindowManager()->isGuiMode() : WindowModal(MWBase::Environment::get().getWindowManager()->isGuiMode()
? "openmw_interactive_messagebox_notransp.layout" ? "openmw_interactive_messagebox_notransp.layout"
: "openmw_interactive_messagebox.layout") : "openmw_interactive_messagebox.layout")
, mMessageBoxManager(parMessageBoxManager) , mMessageBoxManager(parMessageBoxManager)
, mButtonPressed(-1) , mButtonPressed(-1)
, mDefaultFocus(defaultFocus)
, mImmediate(immediate)
{ {
int textPadding = 10; // padding between text-widget and main-widget int textPadding = 10; // padding between text-widget and main-widget
int textButtonPadding = 10; // padding between the text-widget und the button-widget int textButtonPadding = 10; // padding between the text-widget und the button-widget
@ -363,6 +380,9 @@ namespace MWGui
MyGUI::Widget* InteractiveMessageBox::getDefaultKeyFocus() MyGUI::Widget* InteractiveMessageBox::getDefaultKeyFocus()
{ {
std::vector<std::string> keywords{ "sOk", "sYes" }; 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 (MyGUI::Button* button : mButtons)
{ {
for (const std::string& keyword : keywords) for (const std::string& keyword : keywords)
@ -393,6 +413,12 @@ namespace MWGui
{ {
mButtonPressed = index; mButtonPressed = index;
mMessageBoxManager.onButtonPressed(mButtonPressed); mMessageBoxManager.onButtonPressed(mButtonPressed);
if (!mImmediate)
return;
mMessageBoxManager.setLastButtonPressed(mButtonPressed);
MWBase::Environment::get().getInputManager()->changeInputMode(
MWBase::Environment::get().getWindowManager()->isGuiMode());
return; return;
} }
index++; index++;

View file

@ -25,7 +25,8 @@ namespace MWGui
void onFrame(float frameDuration); void onFrame(float frameDuration);
void createMessageBox(std::string_view message, bool stat = false); void createMessageBox(std::string_view message, bool stat = false);
void removeStaticMessageBox(); 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(); bool isInteractiveMessageBox();
int getMessagesCount(); int getMessagesCount();
@ -40,6 +41,10 @@ namespace MWGui
/// @param reset Reset the pressed button to -1 after reading it. /// @param reset Reset the pressed button to -1 after reading it.
int readPressedButton(bool reset = true); int readPressedButton(bool reset = true);
void resetInteractiveMessageBox();
void setLastButtonPressed(int index);
typedef MyGUI::delegates::MultiDelegate<int> EventHandle_Int; typedef MyGUI::delegates::MultiDelegate<int> EventHandle_Int;
// Note: this delegate unassigns itself after it was fired, i.e. works once. // Note: this delegate unassigns itself after it was fired, i.e. works once.
@ -88,7 +93,7 @@ namespace MWGui
{ {
public: public:
InteractiveMessageBox(MessageBoxManager& parMessageBoxManager, const std::string& message, 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); void mousePressed(MyGUI::Widget* _widget);
int readPressedButton(); int readPressedButton();
@ -107,6 +112,8 @@ namespace MWGui
std::vector<MyGUI::Button*> mButtons; std::vector<MyGUI::Button*> mButtons;
int mButtonPressed; int mButtonPressed;
int mDefaultFocus;
bool mImmediate;
}; };
} }

View file

@ -744,9 +744,9 @@ namespace MWGui
} }
void WindowManager::interactiveMessageBox( 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(); updateVisible();
if (block) if (block)
@ -779,6 +779,8 @@ namespace MWGui
frameRateLimiter.limit(); frameRateLimiter.limit();
} }
mMessageBoxManager->resetInteractiveMessageBox();
} }
} }

View file

@ -268,8 +268,8 @@ namespace MWGui
enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) override; enum MWGui::ShowInDialogueMode showInDialogueMode = MWGui::ShowInDialogueMode_IfPossible) override;
void staticMessageBox(std::string_view message) override; void staticMessageBox(std::string_view message) override;
void removeStaticMessageBox() override; void removeStaticMessageBox() override;
void interactiveMessageBox( void interactiveMessageBox(std::string_view message, const std::vector<std::string>& buttons = {},
std::string_view message, const std::vector<std::string>& buttons = {}, bool block = false) override; bool block = false, int defaultFocus = -1) override;
int readPressedButton() override; ///< returns the index of the pressed button or -1 if no button was pressed int readPressedButton() override; ///< returns the index of the pressed button or -1 if no button was pressed
///< (->MessageBoxmanager->InteractiveMessageBox) ///< (->MessageBoxmanager->InteractiveMessageBox)

View file

@ -2,6 +2,8 @@
#include <filesystem> #include <filesystem>
#include <SDL_clipboard.h>
#include <components/debug/debuglog.hpp> #include <components/debug/debuglog.hpp>
#include <components/esm3/esmreader.hpp> #include <components/esm3/esmreader.hpp>
@ -440,7 +442,9 @@ void MWState::StateManager::loadGame(const Character* character, const std::file
{ {
ESM::SavedGame profile; ESM::SavedGame profile;
profile.load(reader); 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); cleanup(true);
MWBase::Environment::get().getWindowManager()->pushGuiMode(MWGui::GM_MainMenu); 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(); std::ostringstream stream;
bool notFound = false; for (auto& contentFile : missingFiles)
for (const std::string& contentFile : profile.mContentFiles)
{
if (std::find(selectedContentFiles.begin(), selectedContentFiles.end(), contentFile)
== selectedContentFiles.end())
{ {
Log(Debug::Warning) << "Warning: Saved game dependency " << contentFile << " is missing."; Log(Debug::Warning) << "Warning: Saved game dependency " << contentFile << " is missing.";
notFound = true; stream << contentFile << "\n";
} }
}
if (notFound) auto fullList = stream.str();
{ if (!fullList.empty())
fullList.pop_back();
constexpr size_t missingPluginsDisplayLimit = 12;
std::vector<std::string> buttons; std::vector<std::string> buttons;
buttons.emplace_back("#{Interface:Yes}"); buttons.emplace_back("#{Interface:Yes}");
buttons.emplace_back("#{Interface:Copy}");
buttons.emplace_back("#{Interface:No}"); buttons.emplace_back("#{Interface:No}");
MWBase::Environment::get().getWindowManager()->interactiveMessageBox( std::string message = "#{OMWEngine:MissingContentFilesConfirmation}";
"#{OMWEngine:MissingContentFilesConfirmation}", buttons, true);
int selectedButton = MWBase::Environment::get().getWindowManager()->readPressedButton(); auto l10n = MWBase::Environment::get().getL10nManager()->getContext("OMWEngine");
if (selectedButton == 1 || selectedButton == -1) 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)
{
message += missingFiles[i];
message += "\n";
}
message += "...";
}
message
+= l10n->formatMessage("MissingContentFilesListCopy", { "files" }, { static_cast<int>(missingFiles.size()) });
int selectedButton = -1;
while (true)
{
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 false;
} }
return true; return true;
} }

View file

@ -21,7 +21,7 @@ namespace MWState
private: private:
void cleanup(bool force = false); 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; void writeScreenshot(std::vector<char>& imageData) const;

View file

@ -61,4 +61,18 @@ namespace ESM
esm.writeHNT("MHLT", mMaximumHealth); 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;
}
} }

View file

@ -40,6 +40,8 @@ namespace ESM
void load(ESMReader& esm); void load(ESMReader& esm);
void save(ESMWriter& esm) const; void save(ESMWriter& esm) const;
std::vector<std::string_view> getMissingContentFiles(const std::vector<std::string>& allContentFiles) const;
}; };
} }

View file

@ -25,3 +25,4 @@ Yes: "Ja"
#OK: "OK" #OK: "OK"
#Off: "Off" #Off: "Off"
#On: "On" #On: "On"
#Copy: "Copy"

View file

@ -22,3 +22,4 @@ None: "None"
OK: "OK" OK: "OK"
Cancel: "Cancel" Cancel: "Cancel"
Close: "Close" Close: "Close"
Copy: "Copy"

View file

@ -22,3 +22,4 @@ None: "Aucun"
OK: "Valider" OK: "Valider"
Cancel: "Annuler" Cancel: "Annuler"
Close: "Fermer" Close: "Fermer"
#Copy: "Copy"

View file

@ -1,5 +1,6 @@
Cancel: "Отмена" Cancel: "Отмена"
Close: "Закрыть" Close: "Закрыть"
Copy: "Скопировать"
DurationDay: "{days} д " DurationDay: "{days} д "
DurationHour: "{hours} ч " DurationHour: "{hours} ч "
DurationMinute: "{minutes} мин " DurationMinute: "{minutes} мин "

View file

@ -14,3 +14,4 @@ Off: "Av"
On: "På" On: "På"
Reset: "Återställ" Reset: "Återställ"
Yes: "Ja" Yes: "Ja"
#Copy: "Copy"

View file

@ -41,10 +41,22 @@ TimePlayed: "Spielzeit"
#DeleteGameConfirmation: "Are you sure you want to delete this saved game?" #DeleteGameConfirmation: "Are you sure you want to delete this saved game?"
#EmptySaveNameError: "Game can not be saved without a name!" #EmptySaveNameError: "Game can not be saved without a name!"
#LoadGameConfirmation: "Do you want to load a saved game and lose the current one?" #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. # The currently selected content files do not match the ones used by this save game.
# Errors may occur during load or game play. # Errors may occur during load or game play.
# Do you wish to continue? # 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?" #OverwriteGameConfirmation: "Are you sure you want to overwrite this saved game?"

View file

@ -34,10 +34,22 @@ DeleteGame: "Delete Game"
DeleteGameConfirmation: "Are you sure you want to delete this saved game?" DeleteGameConfirmation: "Are you sure you want to delete this saved game?"
EmptySaveNameError: "Game can not be saved without a name!" EmptySaveNameError: "Game can not be saved without a name!"
LoadGameConfirmation: "Do you want to load a saved game and lose the current one?" 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. The currently selected content files do not match the ones used by this save game.
Errors may occur during load or game play. Errors may occur during load or game play.
Do you wish to continue? 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?" OverwriteGameConfirmation: "Are you sure you want to overwrite this saved game?"
SelectCharacter: "Select Character..." SelectCharacter: "Select Character..."
TimePlayed: "Time played" TimePlayed: "Time played"

View file

@ -37,10 +37,22 @@ DeleteGame: "Supprimer la partie"
DeleteGameConfirmation: "Voulez-vous réellement supprimer cette partie sauvegardée ?" DeleteGameConfirmation: "Voulez-vous réellement supprimer cette partie sauvegardée ?"
EmptySaveNameError: "Impossible de sauvegarder une partie lui donner un nom !" EmptySaveNameError: "Impossible de sauvegarder une partie lui donner un nom !"
LoadGameConfirmation: "Voulez-vous charger cette autre partie ? Toute progression non sauvegardée sera perdue." 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. 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. Cela peut entraîner des erreurs lors du chargement, mais aussi lors de votre partie.
Voulez-vous continuer ? 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 ?" OverwriteGameConfirmation: "Écraser la sauvegarde précédente ?"

View file

@ -34,10 +34,22 @@ DeleteGame: "Удалить игру"
DeleteGameConfirmation: "Вы уверены, что хотите удалить это сохранение?" DeleteGameConfirmation: "Вы уверены, что хотите удалить это сохранение?"
EmptySaveNameError: "Имя сохранения не может быть пустым!" EmptySaveNameError: "Имя сохранения не может быть пустым!"
LoadGameConfirmation: "Вы хотите загрузить сохранение? Текущая игра будет потеряна." LoadGameConfirmation: "Вы хотите загрузить сохранение? Текущая игра будет потеряна."
MissingContentFilesConfirmation: | MissingContentFilesConfirmation: |-
Выбранные ESM/ESP файлы не соответствуют тем, которые использовались для этого сохранения. Выбранные 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: "Вы уверены, что хотите перезаписать это сохранение?" OverwriteGameConfirmation: "Вы уверены, что хотите перезаписать это сохранение?"
SelectCharacter: "Выберите персонажа..." SelectCharacter: "Выберите персонажа..."
TimePlayed: "Время в игре" TimePlayed: "Время в игре"

View file

@ -35,10 +35,22 @@ DeleteGame: "Radera spel"
DeleteGameConfirmation: "Är du säker på att du vill radera sparfilen?" DeleteGameConfirmation: "Är du säker på att du vill radera sparfilen?"
EmptySaveNameError: "Spelet kan inte sparas utan ett namn!" EmptySaveNameError: "Spelet kan inte sparas utan ett namn!"
LoadGameConfirmation: "Vill du ladda ett sparat spel och förlora det pågående spelet?" 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. De valda innehållsfilerna matchar inte filerna som används av denna sparfil.
Fel kan uppstå vid laddning eller under spel. Fel kan uppstå vid laddning eller under spel.
Vill du fortsätta? 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?" OverwriteGameConfirmation: "Är du säker på att du vill skriva över det här sparade spelet?"
SelectCharacter: "Välj spelfigur..." SelectCharacter: "Välj spelfigur..."