From 6161953106fe431d2cc3595f91f1acebf95f8c7b Mon Sep 17 00:00:00 2001 From: Petr Mikheev Date: Tue, 3 Oct 2023 02:04:18 +0200 Subject: [PATCH] Allow reading ESM4 books --- apps/openmw/mwgui/bookwindow.cpp | 14 ++- apps/openmw/mwgui/formatting.cpp | 114 +++++++++++++----- apps/openmw/mwgui/formatting.hpp | 11 +- apps/openmw/mwgui/scrollwindow.cpp | 12 +- .../source/reference/lua-scripting/events.rst | 18 +++ .../reference/lua-scripting/overview.rst | 3 +- files/data/scripts/omw/activationhandlers.lua | 7 ++ files/data/scripts/omw/ui.lua | 2 + 8 files changed, 135 insertions(+), 46 deletions(-) diff --git a/apps/openmw/mwgui/bookwindow.cpp b/apps/openmw/mwgui/bookwindow.cpp index 60a86851fa..ef875a18b9 100644 --- a/apps/openmw/mwgui/bookwindow.cpp +++ b/apps/openmw/mwgui/bookwindow.cpp @@ -4,6 +4,7 @@ #include #include +#include #include "../mwbase/environment.hpp" #include "../mwbase/windowmanager.hpp" @@ -83,7 +84,7 @@ namespace MWGui void BookWindow::setPtr(const MWWorld::Ptr& book) { - if (book.isEmpty() || book.getType() != ESM::REC_BOOK) + if (book.isEmpty() || (book.getType() != ESM::REC_BOOK && book.getType() != ESM::REC_BOOK4)) throw std::runtime_error("Invalid argument in BookWindow::setPtr"); mBook = book; @@ -93,11 +94,16 @@ namespace MWGui clearPages(); mCurrentPage = 0; - MWWorld::LiveCellRef* ref = mBook.get(); + const std::string* text; + if (book.getType() == ESM::REC_BOOK) + text = &book.get()->mBase->mText; + else + text = &book.get()->mBase->mText; + bool shrinkTextAtLastTag = book.getType() == ESM::REC_BOOK; Formatting::BookFormatter formatter; - mPages = formatter.markupToWidget(mLeftPage, ref->mBase->mText); - formatter.markupToWidget(mRightPage, ref->mBase->mText); + mPages = formatter.markupToWidget(mLeftPage, *text, shrinkTextAtLastTag); + formatter.markupToWidget(mRightPage, *text, shrinkTextAtLastTag); updatePages(); diff --git a/apps/openmw/mwgui/formatting.cpp b/apps/openmw/mwgui/formatting.cpp index 8479379976..7f62bbf49c 100644 --- a/apps/openmw/mwgui/formatting.cpp +++ b/apps/openmw/mwgui/formatting.cpp @@ -23,7 +23,7 @@ namespace MWGui::Formatting { /* BookTextParser */ - BookTextParser::BookTextParser(const std::string& text) + BookTextParser::BookTextParser(const std::string& text, bool shrinkTextAtLastTag) : mIndex(0) , mText(text) , mIgnoreNewlineTags(true) @@ -36,20 +36,25 @@ namespace MWGui::Formatting Misc::StringUtils::replaceAll(mText, "\r", {}); - // vanilla game does not show any text after the last EOL tag. - const std::string lowerText = Misc::StringUtils::lowerCase(mText); - size_t brIndex = lowerText.rfind("
"); - size_t pIndex = lowerText.rfind("

"); - mPlainTextEnd = 0; - if (brIndex != pIndex) + if (shrinkTextAtLastTag) { - if (brIndex != std::string::npos && pIndex != std::string::npos) - mPlainTextEnd = std::max(brIndex, pIndex); - else if (brIndex != std::string::npos) - mPlainTextEnd = brIndex; - else - mPlainTextEnd = pIndex; + // vanilla game does not show any text after the last EOL tag. + const std::string lowerText = Misc::StringUtils::lowerCase(mText); + size_t brIndex = lowerText.rfind("
"); + size_t pIndex = lowerText.rfind("

"); + mPlainTextEnd = 0; + if (brIndex != pIndex) + { + if (brIndex != std::string::npos && pIndex != std::string::npos) + mPlainTextEnd = std::max(brIndex, pIndex); + else if (brIndex != std::string::npos) + mPlainTextEnd = brIndex; + else + mPlainTextEnd = pIndex; + } } + else + mPlainTextEnd = mText.size(); registerTag("br", Event_BrTag); registerTag("p", Event_PTag); @@ -73,6 +78,17 @@ namespace MWGui::Formatting while (mIndex < mText.size()) { char ch = mText[mIndex]; + if (ch == '[') + { + constexpr std::string_view pageBreakTag = "[pagebreak]\n"; + if (std::string_view(mText.data() + mIndex, mText.size() - mIndex).starts_with(pageBreakTag)) + { + mIndex += pageBreakTag.size(); + flushBuffer(); + mIgnoreNewlineTags = false; + return Event_PageBreak; + } + } if (ch == '<') { const size_t tagStart = mIndex + 1; @@ -98,6 +114,8 @@ namespace MWGui::Formatting } } mIgnoreLineEndings = true; + if (type == Event_PTag && !mAttributes.empty()) + flushBuffer(); } else flushBuffer(); @@ -180,9 +198,9 @@ namespace MWGui::Formatting if (tag.empty()) return; - if (tag[0] == '"') + if (tag[0] == '"' || tag[0] == '\'') { - size_t quoteEndPos = tag.find('"', 1); + size_t quoteEndPos = tag.find(tag[0], 1); if (quoteEndPos == std::string::npos) throw std::runtime_error("BookTextParser Error: Missing end quote in tag"); value = tag.substr(1, quoteEndPos - 1); @@ -208,8 +226,8 @@ namespace MWGui::Formatting } /* BookFormatter */ - Paginator::Pages BookFormatter::markupToWidget( - MyGUI::Widget* parent, const std::string& markup, const int pageWidth, const int pageHeight) + Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget* parent, const std::string& markup, + const int pageWidth, const int pageHeight, bool shrinkTextAtLastTag) { Paginator pag(pageWidth, pageHeight); @@ -225,14 +243,16 @@ namespace MWGui::Formatting MyGUI::IntCoord(0, 0, pag.getPageWidth(), pag.getPageHeight()), MyGUI::Align::Left | MyGUI::Align::Top); paper->setNeedMouseFocus(false); - BookTextParser parser(markup); + BookTextParser parser(markup, shrinkTextAtLastTag); bool brBeforeLastTag = false; bool isPrevImg = false; + bool inlineImageInserted = false; for (;;) { BookTextParser::Events event = parser.next(); - if (event == BookTextParser::Event_BrTag || event == BookTextParser::Event_PTag) + if (event == BookTextParser::Event_BrTag + || (event == BookTextParser::Event_PTag && parser.getAttributes().empty())) continue; std::string plainText = parser.getReadyText(); @@ -272,6 +292,12 @@ namespace MWGui::Formatting if (!plainText.empty() || brBeforeLastTag || isPrevImg) { + if (inlineImageInserted) + { + pag.setCurrentTop(pag.getCurrentTop() - mTextStyle.mTextSize); + plainText = " " + plainText; + inlineImageInserted = false; + } TextElement elem(paper, pag, mBlockStyle, mTextStyle, plainText); elem.paginate(); } @@ -286,6 +312,10 @@ namespace MWGui::Formatting switch (event) { + case BookTextParser::Event_PageBreak: + pag << Paginator::Page(pag.getStartTop(), pag.getCurrentTop()); + pag.setStartTop(pag.getCurrentTop()); + break; case BookTextParser::Event_ImgTag: { const BookTextParser::Attributes& attr = parser.getAttributes(); @@ -293,22 +323,38 @@ namespace MWGui::Formatting auto srcIt = attr.find("src"); if (srcIt == attr.end()) continue; - auto widthIt = attr.find("width"); - if (widthIt == attr.end()) - continue; - auto heightIt = attr.find("height"); - if (heightIt == attr.end()) - continue; + int width = 0; + if (auto widthIt = attr.find("width"); widthIt != attr.end()) + width = MyGUI::utility::parseInt(widthIt->second); + int height = 0; + if (auto heightIt = attr.find("height"); heightIt != attr.end()) + height = MyGUI::utility::parseInt(heightIt->second); const std::string& src = srcIt->second; - int width = MyGUI::utility::parseInt(widthIt->second); - int height = MyGUI::utility::parseInt(heightIt->second); - auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); - std::string correctedSrc = Misc::ResourceHelpers::correctBookartPath(src, width, height, vfs); - bool exists = vfs->exists(correctedSrc); - if (!exists) + std::string correctedSrc; + + constexpr std::string_view imgPrefix = "img://"; + if (src.starts_with(imgPrefix)) + { + correctedSrc = src.substr(imgPrefix.size(), src.size() - imgPrefix.size()); + if (width == 0) + { + width = 50; + inlineImageInserted = true; + } + if (height == 0) + height = 50; + } + else + { + if (width == 0 || height == 0) + continue; + correctedSrc = Misc::ResourceHelpers::correctBookartPath(src, width, height, vfs); + } + + if (!vfs->exists(correctedSrc)) { Log(Debug::Warning) << "Warning: Could not find \"" << src << "\" referenced by an tag."; break; @@ -326,6 +372,7 @@ namespace MWGui::Formatting else handleFont(parser.getAttributes()); break; + case BookTextParser::Event_PTag: case BookTextParser::Event_DivTag: handleDiv(parser.getAttributes()); break; @@ -343,9 +390,10 @@ namespace MWGui::Formatting return pag.getPages(); } - Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget* parent, const std::string& markup) + Paginator::Pages BookFormatter::markupToWidget( + MyGUI::Widget* parent, const std::string& markup, bool shrinkTextAtLastTag) { - return markupToWidget(parent, markup, parent->getWidth(), parent->getHeight()); + return markupToWidget(parent, markup, parent->getWidth(), parent->getHeight(), shrinkTextAtLastTag); } void BookFormatter::resetFontProperties() diff --git a/apps/openmw/mwgui/formatting.hpp b/apps/openmw/mwgui/formatting.hpp index 421bda6f1d..9a215b200b 100644 --- a/apps/openmw/mwgui/formatting.hpp +++ b/apps/openmw/mwgui/formatting.hpp @@ -46,10 +46,11 @@ namespace MWGui Event_PTag, Event_ImgTag, Event_DivTag, - Event_FontTag + Event_FontTag, + Event_PageBreak, }; - BookTextParser(const std::string& text); + BookTextParser(const std::string& text, bool shrinkTextAtLastTag); Events next(); @@ -120,9 +121,9 @@ namespace MWGui class BookFormatter { public: - Paginator::Pages markupToWidget( - MyGUI::Widget* parent, const std::string& markup, const int pageWidth, const int pageHeight); - Paginator::Pages markupToWidget(MyGUI::Widget* parent, const std::string& markup); + Paginator::Pages markupToWidget(MyGUI::Widget* parent, const std::string& markup, const int pageWidth, + const int pageHeight, bool shrinkTextAtLastTag); + Paginator::Pages markupToWidget(MyGUI::Widget* parent, const std::string& markup, bool shrinkTextAtLastTag); private: void resetFontProperties(); diff --git a/apps/openmw/mwgui/scrollwindow.cpp b/apps/openmw/mwgui/scrollwindow.cpp index 3debf0d66d..0b1658fd84 100644 --- a/apps/openmw/mwgui/scrollwindow.cpp +++ b/apps/openmw/mwgui/scrollwindow.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "../mwbase/environment.hpp" @@ -42,17 +43,22 @@ namespace MWGui void ScrollWindow::setPtr(const MWWorld::Ptr& scroll) { - if (scroll.isEmpty() || scroll.getType() != ESM::REC_BOOK) + if (scroll.isEmpty() || (scroll.getType() != ESM::REC_BOOK && scroll.getType() != ESM::REC_BOOK4)) throw std::runtime_error("Invalid argument in ScrollWindow::setPtr"); mScroll = scroll; MWWorld::Ptr player = MWMechanics::getPlayer(); bool showTakeButton = scroll.getContainerStore() != &player.getClass().getContainerStore(player); - MWWorld::LiveCellRef* ref = mScroll.get(); + const std::string* text; + if (scroll.getType() == ESM::REC_BOOK) + text = &scroll.get()->mBase->mText; + else + text = &scroll.get()->mBase->mText; + bool shrinkTextAtLastTag = scroll.getType() == ESM::REC_BOOK; Formatting::BookFormatter formatter; - formatter.markupToWidget(mTextView, ref->mBase->mText, 390, mTextView->getHeight()); + formatter.markupToWidget(mTextView, *text, 390, mTextView->getHeight(), shrinkTextAtLastTag); MyGUI::IntSize size = mTextView->getChildAt(0)->getSize(); // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the diff --git a/docs/source/reference/lua-scripting/events.rst b/docs/source/reference/lua-scripting/events.rst index b0e600413a..7f0a764b86 100644 --- a/docs/source/reference/lua-scripting/events.rst +++ b/docs/source/reference/lua-scripting/events.rst @@ -32,6 +32,8 @@ Example: UI events --------- +**UiModeChanged** + Every time UI mode is changed built-in scripts send to player the event ``UiModeChanged`` with arguments ``oldMode, ``newMode`` (same as ``I.UI.getMode()``) and ``arg`` (for example in the mode ``Book`` the argument is the book the player is reading). @@ -43,6 +45,22 @@ and ``arg`` (for example in the mode ``Book`` the argument is the book the playe end } +**AddUiMode** + +Equivalent to ``I.UI.addMode``, but can be sent from another object or global script. + +.. code-block:: Lua + + player:sendEvent('AddUiMode', {mode = 'Book', target = book}) + +**SetUiMode** + +Equivalent to ``I.UI.setMode``, but can be sent from another object or global script. + +.. code-block:: Lua + + player:sendEvent('SetUiMode', {mode = 'Book', target = book}) + World events ------------ diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index 90014f637b..5515351e20 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -459,7 +459,8 @@ Using the interface: The order in which the scripts are started is important. So if one mod should override an interface provided by another mod, make sure that load order (i.e. the sequence of `lua-scripts=...` in `openmw.cfg`) is correct. -**Interfaces of built-in scripts** +Interfaces of built-in scripts +------------------------------ .. include:: tables/interfaces.rst diff --git a/files/data/scripts/omw/activationhandlers.lua b/files/data/scripts/omw/activationhandlers.lua index 2f63f59e92..edadc30f79 100644 --- a/files/data/scripts/omw/activationhandlers.lua +++ b/files/data/scripts/omw/activationhandlers.lua @@ -17,9 +17,16 @@ local function ESM4DoorActivation(door, actor) return false -- disable activation handling in C++ mwmechanics code end +local function ESM4BookActivation(book, actor) + if actor.type == types.Player then + actor:sendEvent('AddUiMode', { mode = 'Book', target = book }) + end +end + local handlersPerObject = {} local handlersPerType = {} +handlersPerType[types.ESM4Book] = { ESM4BookActivation } handlersPerType[types.ESM4Door] = { ESM4DoorActivation } local function onActivate(obj, actor) diff --git a/files/data/scripts/omw/ui.lua b/files/data/scripts/omw/ui.lua index 5837b01f36..6fd80bf394 100644 --- a/files/data/scripts/omw/ui.lua +++ b/files/data/scripts/omw/ui.lua @@ -240,5 +240,7 @@ return { }, eventHandlers = { UiModeChanged = onUiModeChangedEvent, + AddUiMode = function(options) addMode(options.mode, options) end, + SetUiMode = function(options) setMode(options.mode, options) end, }, }