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 <components/esm3/loadbook.hpp>
#include <components/esm4/loadbook.hpp>
#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<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;
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();

@ -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("<br>");
size_t pIndex = lowerText.rfind("<p>");
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("<br>");
size_t pIndex = lowerText.rfind("<p>");
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 <img> 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()

@ -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();

@ -3,6 +3,7 @@
#include <MyGUI_ScrollView.h>
#include <components/esm3/loadbook.hpp>
#include <components/esm4/loadbook.hpp>
#include <components/widgets/imagebutton.hpp>
#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<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;
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

@ -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
------------

@ -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

@ -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)

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

Loading…
Cancel
Save