Allow reading ESM4 books

macos_ci_fix
Petr Mikheev 1 year ago
parent 15e6ababf1
commit 6161953106

@ -4,6 +4,7 @@
#include <MyGUI_TextBox.h> #include <MyGUI_TextBox.h>
#include <components/esm3/loadbook.hpp> #include <components/esm3/loadbook.hpp>
#include <components/esm4/loadbook.hpp>
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
#include "../mwbase/windowmanager.hpp" #include "../mwbase/windowmanager.hpp"
@ -83,7 +84,7 @@ namespace MWGui
void BookWindow::setPtr(const MWWorld::Ptr& book) 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"); throw std::runtime_error("Invalid argument in BookWindow::setPtr");
mBook = book; mBook = book;
@ -93,11 +94,16 @@ namespace MWGui
clearPages(); clearPages();
mCurrentPage = 0; mCurrentPage = 0;
MWWorld::LiveCellRef<ESM::Book>* ref = mBook.get<ESM::Book>(); const std::string* text;
if (book.getType() == ESM::REC_BOOK)
text = &book.get<ESM::Book>()->mBase->mText;
else
text = &book.get<ESM4::Book>()->mBase->mText;
bool shrinkTextAtLastTag = book.getType() == ESM::REC_BOOK;
Formatting::BookFormatter formatter; Formatting::BookFormatter formatter;
mPages = formatter.markupToWidget(mLeftPage, ref->mBase->mText); mPages = formatter.markupToWidget(mLeftPage, *text, shrinkTextAtLastTag);
formatter.markupToWidget(mRightPage, ref->mBase->mText); formatter.markupToWidget(mRightPage, *text, shrinkTextAtLastTag);
updatePages(); updatePages();

@ -23,7 +23,7 @@
namespace MWGui::Formatting namespace MWGui::Formatting
{ {
/* BookTextParser */ /* BookTextParser */
BookTextParser::BookTextParser(const std::string& text) BookTextParser::BookTextParser(const std::string& text, bool shrinkTextAtLastTag)
: mIndex(0) : mIndex(0)
, mText(text) , mText(text)
, mIgnoreNewlineTags(true) , mIgnoreNewlineTags(true)
@ -36,20 +36,25 @@ namespace MWGui::Formatting
Misc::StringUtils::replaceAll(mText, "\r", {}); Misc::StringUtils::replaceAll(mText, "\r", {});
// vanilla game does not show any text after the last EOL tag. if (shrinkTextAtLastTag)
const std::string lowerText = Misc::StringUtils::lowerCase(mText);
size_t brIndex = lowerText.rfind("<br>");
size_t pIndex = lowerText.rfind("<p>");
mPlainTextEnd = 0;
if (brIndex != pIndex)
{ {
if (brIndex != std::string::npos && pIndex != std::string::npos) // vanilla game does not show any text after the last EOL tag.
mPlainTextEnd = std::max(brIndex, pIndex); const std::string lowerText = Misc::StringUtils::lowerCase(mText);
else if (brIndex != std::string::npos) size_t brIndex = lowerText.rfind("<br>");
mPlainTextEnd = brIndex; size_t pIndex = lowerText.rfind("<p>");
else mPlainTextEnd = 0;
mPlainTextEnd = pIndex; 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("br", Event_BrTag);
registerTag("p", Event_PTag); registerTag("p", Event_PTag);
@ -73,6 +78,17 @@ namespace MWGui::Formatting
while (mIndex < mText.size()) while (mIndex < mText.size())
{ {
char ch = mText[mIndex]; 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 == '<') if (ch == '<')
{ {
const size_t tagStart = mIndex + 1; const size_t tagStart = mIndex + 1;
@ -98,6 +114,8 @@ namespace MWGui::Formatting
} }
} }
mIgnoreLineEndings = true; mIgnoreLineEndings = true;
if (type == Event_PTag && !mAttributes.empty())
flushBuffer();
} }
else else
flushBuffer(); flushBuffer();
@ -180,9 +198,9 @@ namespace MWGui::Formatting
if (tag.empty()) if (tag.empty())
return; 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) if (quoteEndPos == std::string::npos)
throw std::runtime_error("BookTextParser Error: Missing end quote in tag"); throw std::runtime_error("BookTextParser Error: Missing end quote in tag");
value = tag.substr(1, quoteEndPos - 1); value = tag.substr(1, quoteEndPos - 1);
@ -208,8 +226,8 @@ namespace MWGui::Formatting
} }
/* BookFormatter */ /* BookFormatter */
Paginator::Pages BookFormatter::markupToWidget( Paginator::Pages BookFormatter::markupToWidget(MyGUI::Widget* parent, const std::string& markup,
MyGUI::Widget* parent, const std::string& markup, const int pageWidth, const int pageHeight) const int pageWidth, const int pageHeight, bool shrinkTextAtLastTag)
{ {
Paginator pag(pageWidth, pageHeight); 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); MyGUI::IntCoord(0, 0, pag.getPageWidth(), pag.getPageHeight()), MyGUI::Align::Left | MyGUI::Align::Top);
paper->setNeedMouseFocus(false); paper->setNeedMouseFocus(false);
BookTextParser parser(markup); BookTextParser parser(markup, shrinkTextAtLastTag);
bool brBeforeLastTag = false; bool brBeforeLastTag = false;
bool isPrevImg = false; bool isPrevImg = false;
bool inlineImageInserted = false;
for (;;) for (;;)
{ {
BookTextParser::Events event = parser.next(); 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; continue;
std::string plainText = parser.getReadyText(); std::string plainText = parser.getReadyText();
@ -272,6 +292,12 @@ namespace MWGui::Formatting
if (!plainText.empty() || brBeforeLastTag || isPrevImg) if (!plainText.empty() || brBeforeLastTag || isPrevImg)
{ {
if (inlineImageInserted)
{
pag.setCurrentTop(pag.getCurrentTop() - mTextStyle.mTextSize);
plainText = " " + plainText;
inlineImageInserted = false;
}
TextElement elem(paper, pag, mBlockStyle, mTextStyle, plainText); TextElement elem(paper, pag, mBlockStyle, mTextStyle, plainText);
elem.paginate(); elem.paginate();
} }
@ -286,6 +312,10 @@ namespace MWGui::Formatting
switch (event) switch (event)
{ {
case BookTextParser::Event_PageBreak:
pag << Paginator::Page(pag.getStartTop(), pag.getCurrentTop());
pag.setStartTop(pag.getCurrentTop());
break;
case BookTextParser::Event_ImgTag: case BookTextParser::Event_ImgTag:
{ {
const BookTextParser::Attributes& attr = parser.getAttributes(); const BookTextParser::Attributes& attr = parser.getAttributes();
@ -293,22 +323,38 @@ namespace MWGui::Formatting
auto srcIt = attr.find("src"); auto srcIt = attr.find("src");
if (srcIt == attr.end()) if (srcIt == attr.end())
continue; continue;
auto widthIt = attr.find("width"); int width = 0;
if (widthIt == attr.end()) if (auto widthIt = attr.find("width"); widthIt != attr.end())
continue; width = MyGUI::utility::parseInt(widthIt->second);
auto heightIt = attr.find("height"); int height = 0;
if (heightIt == attr.end()) if (auto heightIt = attr.find("height"); heightIt != attr.end())
continue; height = MyGUI::utility::parseInt(heightIt->second);
const std::string& src = srcIt->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(); 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 <img> tag."; Log(Debug::Warning) << "Warning: Could not find \"" << src << "\" referenced by an <img> tag.";
break; break;
@ -326,6 +372,7 @@ namespace MWGui::Formatting
else else
handleFont(parser.getAttributes()); handleFont(parser.getAttributes());
break; break;
case BookTextParser::Event_PTag:
case BookTextParser::Event_DivTag: case BookTextParser::Event_DivTag:
handleDiv(parser.getAttributes()); handleDiv(parser.getAttributes());
break; break;
@ -343,9 +390,10 @@ namespace MWGui::Formatting
return pag.getPages(); 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() void BookFormatter::resetFontProperties()

@ -46,10 +46,11 @@ namespace MWGui
Event_PTag, Event_PTag,
Event_ImgTag, Event_ImgTag,
Event_DivTag, Event_DivTag,
Event_FontTag Event_FontTag,
Event_PageBreak,
}; };
BookTextParser(const std::string& text); BookTextParser(const std::string& text, bool shrinkTextAtLastTag);
Events next(); Events next();
@ -120,9 +121,9 @@ namespace MWGui
class BookFormatter class BookFormatter
{ {
public: public:
Paginator::Pages markupToWidget( Paginator::Pages markupToWidget(MyGUI::Widget* parent, const std::string& markup, const int pageWidth,
MyGUI::Widget* parent, const std::string& markup, const int pageWidth, const int pageHeight); const int pageHeight, bool shrinkTextAtLastTag);
Paginator::Pages markupToWidget(MyGUI::Widget* parent, const std::string& markup); Paginator::Pages markupToWidget(MyGUI::Widget* parent, const std::string& markup, bool shrinkTextAtLastTag);
private: private:
void resetFontProperties(); void resetFontProperties();

@ -3,6 +3,7 @@
#include <MyGUI_ScrollView.h> #include <MyGUI_ScrollView.h>
#include <components/esm3/loadbook.hpp> #include <components/esm3/loadbook.hpp>
#include <components/esm4/loadbook.hpp>
#include <components/widgets/imagebutton.hpp> #include <components/widgets/imagebutton.hpp>
#include "../mwbase/environment.hpp" #include "../mwbase/environment.hpp"
@ -42,17 +43,22 @@ namespace MWGui
void ScrollWindow::setPtr(const MWWorld::Ptr& scroll) 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"); throw std::runtime_error("Invalid argument in ScrollWindow::setPtr");
mScroll = scroll; mScroll = scroll;
MWWorld::Ptr player = MWMechanics::getPlayer(); MWWorld::Ptr player = MWMechanics::getPlayer();
bool showTakeButton = scroll.getContainerStore() != &player.getClass().getContainerStore(player); bool showTakeButton = scroll.getContainerStore() != &player.getClass().getContainerStore(player);
MWWorld::LiveCellRef<ESM::Book>* ref = mScroll.get<ESM::Book>(); const std::string* text;
if (scroll.getType() == ESM::REC_BOOK)
text = &scroll.get<ESM::Book>()->mBase->mText;
else
text = &scroll.get<ESM4::Book>()->mBase->mText;
bool shrinkTextAtLastTag = scroll.getType() == ESM::REC_BOOK;
Formatting::BookFormatter formatter; 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(); MyGUI::IntSize size = mTextView->getChildAt(0)->getSize();
// Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the // Canvas size must be expressed with VScroll disabled, otherwise MyGUI would expand the scroll area when the

@ -32,6 +32,8 @@ Example:
UI events 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()``) 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). 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 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 World events
------------ ------------

@ -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. 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 .. include:: tables/interfaces.rst

@ -17,9 +17,16 @@ local function ESM4DoorActivation(door, actor)
return false -- disable activation handling in C++ mwmechanics code return false -- disable activation handling in C++ mwmechanics code
end end
local function ESM4BookActivation(book, actor)
if actor.type == types.Player then
actor:sendEvent('AddUiMode', { mode = 'Book', target = book })
end
end
local handlersPerObject = {} local handlersPerObject = {}
local handlersPerType = {} local handlersPerType = {}
handlersPerType[types.ESM4Book] = { ESM4BookActivation }
handlersPerType[types.ESM4Door] = { ESM4DoorActivation } handlersPerType[types.ESM4Door] = { ESM4DoorActivation }
local function onActivate(obj, actor) local function onActivate(obj, actor)

@ -240,5 +240,7 @@ return {
}, },
eventHandlers = { eventHandlers = {
UiModeChanged = onUiModeChangedEvent, UiModeChanged = onUiModeChangedEvent,
AddUiMode = function(options) addMode(options.mode, options) end,
SetUiMode = function(options) setMode(options.mode, options) end,
}, },
} }

Loading…
Cancel
Save