1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-12-06 19:34:32 +00:00

Merge branch 'cs_fix_info_collection' into 'master'

Fix loading, inserting and moving topic info records

See merge request OpenMW/openmw!2806
This commit is contained in:
psi29a 2023-03-17 14:40:05 +00:00
commit 2ff4a5a11a
18 changed files with 893 additions and 172 deletions

View file

@ -123,6 +123,7 @@ void CSMDoc::Loader::load()
} }
else else
{ {
document->getData().finishLoading();
done = true; done = true;
} }

View file

@ -190,17 +190,16 @@ void CSMDoc::WriteDialogueCollectionStage::perform(int stage, Messages& messages
ESM::DialInfo info = record.get(); ESM::DialInfo info = record.get();
info.mId = record.get().mOriginalId; info.mId = record.get().mOriginalId;
info.mPrev = ESM::RefId(); if (iter == infos.begin())
if (iter != infos.begin()) info.mPrev = ESM::RefId();
{ else
const auto prev = std::prev(iter); info.mPrev = (*std::prev(iter))->get().mOriginalId;
info.mPrev = (*prev)->get().mOriginalId;
}
const auto next = std::next(iter); const auto next = std::next(iter);
info.mNext = ESM::RefId(); if (next == infos.end())
if (next != infos.end()) info.mNext = ESM::RefId();
else
info.mNext = (*next)->get().mOriginalId; info.mNext = (*next)->get().mOriginalId;
writer.startRecord(info.sRecordId); writer.startRecord(info.sRecordId);

View file

@ -9,6 +9,7 @@
#include <stdexcept> #include <stdexcept>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <unordered_set>
#include <vector> #include <vector>
#include <QVariant> #include <QVariant>
@ -17,6 +18,7 @@
#include "collectionbase.hpp" #include "collectionbase.hpp"
#include "columnbase.hpp" #include "columnbase.hpp"
#include "info.hpp"
#include "land.hpp" #include "land.hpp"
#include "landtexture.hpp" #include "landtexture.hpp"
#include "record.hpp" #include "record.hpp"
@ -24,12 +26,29 @@
namespace CSMWorld namespace CSMWorld
{ {
inline std::pair<std::string_view, std::string_view> parseInfoRefId(const ESM::RefId& infoId)
{
const auto separator = infoId.getRefIdString().find('#');
if (separator == std::string::npos)
throw std::runtime_error("Invalid info id: " + infoId.getRefIdString());
const std::string_view view(infoId.getRefIdString());
return { view.substr(0, separator), view.substr(separator + 1) };
}
template <typename T> template <typename T>
void setRecordId(const decltype(T::mId)& id, T& record) void setRecordId(const decltype(T::mId)& id, T& record)
{ {
record.mId = id; record.mId = id;
} }
inline void setRecordId(const ESM::RefId& id, Info& record)
{
record.mId = id;
const auto [topicId, originalId] = parseInfoRefId(id);
record.mTopicId = ESM::RefId::stringRefId(topicId);
record.mOriginalId = ESM::RefId::stringRefId(originalId);
}
template <typename T> template <typename T>
auto getRecordId(const T& record) auto getRecordId(const T& record)
{ {
@ -85,6 +104,8 @@ namespace CSMWorld
protected: protected:
const std::vector<std::unique_ptr<Record<ESXRecordT>>>& getRecords() const; const std::vector<std::unique_ptr<Record<ESXRecordT>>>& getRecords() const;
void reorderRowsImp(const std::vector<int>& indexOrder);
bool reorderRowsImp(int baseIndex, const std::vector<int>& newOrder); bool reorderRowsImp(int baseIndex, const std::vector<int>& newOrder);
///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices ///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices
/// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex). /// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex).
@ -191,6 +212,20 @@ namespace CSMWorld
return mRecords; return mRecords;
} }
template <typename ESXRecordT>
void Collection<ESXRecordT>::reorderRowsImp(const std::vector<int>& indexOrder)
{
assert(indexOrder.size() == mRecords.size());
assert(std::unordered_set(indexOrder.begin(), indexOrder.end()).size() == indexOrder.size());
std::vector<std::unique_ptr<Record<ESXRecordT>>> orderedRecords;
for (const int index : indexOrder)
{
mIndex.at(mRecords[index]->get().mId) = static_cast<int>(orderedRecords.size());
orderedRecords.push_back(std::move(mRecords[index]));
}
mRecords = std::move(orderedRecords);
}
template <typename ESXRecordT> template <typename ESXRecordT>
bool Collection<ESXRecordT>::reorderRowsImp(int baseIndex, const std::vector<int>& newOrder) bool Collection<ESXRecordT>::reorderRowsImp(int baseIndex, const std::vector<int>& newOrder)
{ {

View file

@ -31,6 +31,7 @@
#include <components/esm/esmcommon.hpp> #include <components/esm/esmcommon.hpp>
#include <components/esm3/cellref.hpp> #include <components/esm3/cellref.hpp>
#include <components/esm3/esmreader.hpp> #include <components/esm3/esmreader.hpp>
#include <components/esm3/infoorder.hpp>
#include <components/esm3/loadcell.hpp> #include <components/esm3/loadcell.hpp>
#include <components/esm3/loaddoor.hpp> #include <components/esm3/loaddoor.hpp>
#include <components/esm3/loadglob.hpp> #include <components/esm3/loadglob.hpp>
@ -55,37 +56,42 @@
#include "resourcesmanager.hpp" #include "resourcesmanager.hpp"
#include "resourcetable.hpp" #include "resourcetable.hpp"
namespace namespace CSMWorld
{ {
void removeDialogueInfos(const ESM::RefId& dialogueId, const CSMWorld::InfosByTopic& infosByTopic, namespace
CSMWorld::InfoCollection& infoCollection)
{ {
const auto topicInfos = infosByTopic.find(dialogueId); void removeDialogueInfos(
const ESM::RefId& dialogueId, InfoOrderByTopic& infoOrders, InfoCollection& infoCollection)
if (topicInfos == infosByTopic.end())
return;
std::vector<int> erasedRecords;
for (const ESM::RefId& id : topicInfos->second)
{ {
const CSMWorld::Record<CSMWorld::Info>& record = infoCollection.getRecord(id); const auto topicInfoOrder = infoOrders.find(dialogueId);
if (record.mState == CSMWorld::RecordBase::State_ModifiedOnly) if (topicInfoOrder == infoOrders.end())
return;
std::vector<int> erasedRecords;
for (const OrderedInfo& info : topicInfoOrder->second.getOrderedInfo())
{ {
erasedRecords.push_back(infoCollection.searchId(record.get().mId)); const Record<Info>& record = infoCollection.getRecord(info.mId);
continue;
if (record.mState == RecordBase::State_ModifiedOnly)
{
erasedRecords.push_back(infoCollection.searchId(info.mId));
continue;
}
auto deletedRecord = std::make_unique<Record<Info>>(record);
deletedRecord->mState = RecordBase::State_Deleted;
infoCollection.setRecord(infoCollection.searchId(info.mId), std::move(deletedRecord));
} }
auto deletedRecord = std::make_unique<CSMWorld::Record<CSMWorld::Info>>(record); while (!erasedRecords.empty())
deletedRecord->mState = CSMWorld::RecordBase::State_Deleted; {
infoCollection.setRecord(infoCollection.searchId(record.get().mId), std::move(deletedRecord)); infoCollection.removeRows(erasedRecords.back(), 1);
} erasedRecords.pop_back();
}
while (!erasedRecords.empty()) infoOrders.erase(topicInfoOrder);
{
infoCollection.removeRows(erasedRecords.back(), 1);
erasedRecords.pop_back();
} }
} }
} }
@ -1289,11 +1295,11 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages)
if (mJournals.tryDelete(record.mId)) if (mJournals.tryDelete(record.mId))
{ {
removeDialogueInfos(record.mId, mJournalInfosByTopic, mJournalInfos); removeDialogueInfos(record.mId, mJournalInfoOrder, mJournalInfos);
} }
else if (mTopics.tryDelete(record.mId)) else if (mTopics.tryDelete(record.mId))
{ {
removeDialogueInfos(record.mId, mTopicInfosByTopic, mTopicInfos); removeDialogueInfos(record.mId, mTopicInfoOrder, mTopicInfos);
} }
else else
{ {
@ -1331,9 +1337,9 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages)
} }
if (mDialogue->mType == ESM::Dialogue::Journal) if (mDialogue->mType == ESM::Dialogue::Journal)
mJournalInfos.load(*mReader, mBase, *mDialogue, mJournalInfosByTopic); mJournalInfos.load(*mReader, mBase, *mDialogue, mJournalInfoOrder);
else else
mTopicInfos.load(*mReader, mBase, *mDialogue, mTopicInfosByTopic); mTopicInfos.load(*mReader, mBase, *mDialogue, mTopicInfoOrder);
break; break;
} }
@ -1376,6 +1382,12 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages)
return false; return false;
} }
void CSMWorld::Data::finishLoading()
{
mTopicInfos.sort(mTopicInfoOrder);
mJournalInfos.sort(mJournalInfoOrder);
}
bool CSMWorld::Data::hasId(const std::string& id) const bool CSMWorld::Data::hasId(const std::string& id) const
{ {
const ESM::RefId refId = ESM::RefId::stringRefId(id); const ESM::RefId refId = ESM::RefId::stringRefId(id);

View file

@ -15,6 +15,7 @@
#include <components/esm3/debugprofile.hpp> #include <components/esm3/debugprofile.hpp>
#include <components/esm3/filter.hpp> #include <components/esm3/filter.hpp>
#include <components/esm3/infoorder.hpp>
#include <components/esm3/loadbody.hpp> #include <components/esm3/loadbody.hpp>
#include <components/esm3/loadbsgn.hpp> #include <components/esm3/loadbsgn.hpp>
#include <components/esm3/loadclas.hpp> #include <components/esm3/loadclas.hpp>
@ -135,8 +136,8 @@ namespace CSMWorld
std::vector<std::shared_ptr<ESM::ESMReader>> mReaders; std::vector<std::shared_ptr<ESM::ESMReader>> mReaders;
CSMWorld::InfosByTopic mJournalInfosByTopic; InfoOrderByTopic mJournalInfoOrder;
CSMWorld::InfosByTopic mTopicInfosByTopic; InfoOrderByTopic mTopicInfoOrder;
// not implemented // not implemented
Data(const Data&); Data(const Data&);
@ -307,6 +308,8 @@ namespace CSMWorld
bool continueLoading(CSMDoc::Messages& messages); bool continueLoading(CSMDoc::Messages& messages);
///< \return Finished? ///< \return Finished?
void finishLoading();
bool hasId(const std::string& id) const; bool hasId(const std::string& id) const;
std::vector<ESM::RefId> getIds(bool listDeleted = true) const; std::vector<ESM::RefId> getIds(bool listDeleted = true) const;

View file

@ -298,10 +298,12 @@ int CSMWorld::IdTable::findColumnIndex(Columns::ColumnId id) const
void CSMWorld::IdTable::reorderRows(int baseIndex, const std::vector<int>& newOrder) void CSMWorld::IdTable::reorderRows(int baseIndex, const std::vector<int>& newOrder)
{ {
if (!newOrder.empty()) if (newOrder.empty())
if (mIdCollection->reorderRows(baseIndex, newOrder)) return;
emit dataChanged(index(baseIndex, 0), if (!mIdCollection->reorderRows(baseIndex, newOrder))
index(baseIndex + static_cast<int>(newOrder.size()) - 1, mIdCollection->getColumns() - 1)); return;
emit dataChanged(
index(baseIndex, 0), index(baseIndex + static_cast<int>(newOrder.size()) - 1, mIdCollection->getColumns() - 1));
} }
std::pair<CSMWorld::UniversalId, std::string> CSMWorld::IdTable::view(int row) const std::pair<CSMWorld::UniversalId, std::string> CSMWorld::IdTable::view(int row) const

View file

@ -1,91 +1,141 @@
#include "infocollection.hpp" #include "infocollection.hpp"
#include <algorithm>
#include <memory> #include <memory>
#include <stdexcept>
#include <string> #include <string>
#include <utility> #include <utility>
#include <components/debug/debuglog.hpp> #include "components/debug/debuglog.hpp"
#include <components/esm3/loaddial.hpp> #include "components/esm3/infoorder.hpp"
#include "components/esm3/loaddial.hpp"
#include "components/esm3/loadinfo.hpp"
#include "collection.hpp" #include "collection.hpp"
#include "info.hpp" #include "info.hpp"
bool CSMWorld::InfoCollection::load(const Info& record, bool base) namespace CSMWorld
{ {
const int index = searchId(record.mId); namespace
{
ESM::RefId makeCompositeRefId(const ESM::RefId& topicId, const ESM::RefId& infoId)
{
return ESM::RefId::stringRefId(topicId.getRefIdString() + '#' + infoId.getRefIdString());
}
std::string_view getInfoTopicId(const ESM::RefId& infoId)
{
return parseInfoRefId(infoId).first;
}
}
}
void CSMWorld::InfoCollection::load(const Info& value, bool base)
{
const int index = searchId(value.mId);
if (index == -1) if (index == -1)
{ {
// new record // new record
auto record2 = std::make_unique<Record<Info>>(); auto record = std::make_unique<Record<Info>>();
record2->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly;
(base ? record2->mBase : record2->mModified) = record; (base ? record->mBase : record->mModified) = value;
appendRecord(std::move(record2)); insertRecord(std::move(record), getSize());
return true;
} }
else else
{ {
// old record // old record
auto record2 = std::make_unique<Record<Info>>(getRecord(index)); auto record = std::make_unique<Record<Info>>(getRecord(index));
if (base) if (base)
record2->mBase = record; record->mBase = value;
else else
record2->setModified(record); record->setModified(value);
setRecord(index, std::move(record2)); setRecord(index, std::move(record));
return false;
} }
} }
void CSMWorld::InfoCollection::load( void CSMWorld::InfoCollection::load(
ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue, InfosByTopic& infosByTopic) ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue, InfoOrderByTopic& infoOrders)
{ {
Info info; Info info;
bool isDeleted = false; bool isDeleted = false;
info.load(reader, isDeleted); info.load(reader, isDeleted);
const ESM::RefId id = ESM::RefId::stringRefId(dialogue.mId.getRefIdString() + "#" + info.mId.getRefIdString());
const ESM::RefId id = makeCompositeRefId(dialogue.mId, info.mId);
if (isDeleted) if (isDeleted)
{ {
int index = searchId(id); const int index = searchId(id);
if (index == -1) if (index == -1)
{ {
// deleting a record that does not exist Log(Debug::Warning) << "Trying to delete absent info \"" << info.mId << "\" from topic \"" << dialogue.mId
// ignore it for now << "\"";
/// \todo report the problem to the user return;
} }
else if (base)
{
removeRows(index, 1);
}
else
{
auto record = std::make_unique<Record<Info>>(getRecord(index));
record->mState = RecordBase::State_Deleted;
setRecord(index, std::move(record));
}
}
else
{
info.mTopicId = dialogue.mId;
info.mOriginalId = info.mId;
info.mId = id;
if (load(info, base)) if (base)
infosByTopic[dialogue.mId].push_back(info.mId); {
infoOrders.at(dialogue.mId).removeInfo(id);
removeRows(index, 1);
return;
}
auto record = std::make_unique<Record<Info>>(getRecord(index));
record->mState = RecordBase::State_Deleted;
setRecord(index, std::move(record));
return;
} }
info.mTopicId = dialogue.mId;
info.mOriginalId = info.mId;
info.mId = id;
load(info, base);
infoOrders[dialogue.mId].insertInfo(OrderedInfo(info), isDeleted);
}
void CSMWorld::InfoCollection::sort(const InfoOrderByTopic& infoOrders)
{
std::vector<int> order;
order.reserve(getSize());
for (const auto& [topicId, infoOrder] : infoOrders)
for (const OrderedInfo& info : infoOrder.getOrderedInfo())
order.push_back(getIndex(makeCompositeRefId(topicId, info.mId)));
reorderRowsImp(order);
} }
CSMWorld::InfosRecordPtrByTopic CSMWorld::InfoCollection::getInfosByTopic() const CSMWorld::InfosRecordPtrByTopic CSMWorld::InfoCollection::getInfosByTopic() const
{ {
InfosRecordPtrByTopic result; InfosRecordPtrByTopic result;
for (const std::unique_ptr<Record<Info>>& record : getRecords()) for (const std::unique_ptr<Record<Info>>& record : getRecords())
result[record->mBase.mTopicId].push_back(record.get()); result[record->get().mTopicId].push_back(record.get());
return result; return result;
} }
int CSMWorld::InfoCollection::getAppendIndex(const ESM::RefId& id, UniversalId::Type /*type*/) const
{
const auto lessByTopicId
= [](std::string_view lhs, const std::unique_ptr<Record<Info>>& rhs) { return lhs < rhs->get().mTopicId; };
const auto it = std::upper_bound(getRecords().begin(), getRecords().end(), getInfoTopicId(id), lessByTopicId);
return static_cast<int>(it - getRecords().begin());
}
bool CSMWorld::InfoCollection::reorderRows(int baseIndex, const std::vector<int>& newOrder)
{
const int lastIndex = baseIndex + static_cast<int>(newOrder.size()) - 1;
if (lastIndex >= getSize())
return false;
if (getRecord(baseIndex).get().mTopicId != getRecord(lastIndex).get().mTopicId)
return false;
return reorderRowsImp(baseIndex, newOrder);
}

View file

@ -1,6 +1,7 @@
#ifndef CSM_WOLRD_INFOCOLLECTION_H #ifndef CSM_WOLRD_INFOCOLLECTION_H
#define CSM_WOLRD_INFOCOLLECTION_H #define CSM_WOLRD_INFOCOLLECTION_H
#include <map>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
@ -12,22 +13,47 @@ namespace ESM
{ {
struct Dialogue; struct Dialogue;
class ESMReader; class ESMReader;
template <class T>
class InfoOrder;
} }
namespace CSMWorld namespace CSMWorld
{ {
using InfosByTopic = std::unordered_map<ESM::RefId, std::vector<ESM::RefId>>;
using InfosRecordPtrByTopic = std::unordered_map<ESM::RefId, std::vector<const Record<Info>*>>; using InfosRecordPtrByTopic = std::unordered_map<ESM::RefId, std::vector<const Record<Info>*>>;
struct OrderedInfo
{
ESM::RefId mId;
ESM::RefId mNext;
ESM::RefId mPrev;
explicit OrderedInfo(const Info& info)
: mId(info.mOriginalId)
, mNext(info.mNext)
, mPrev(info.mPrev)
{
}
};
using InfoOrder = ESM::InfoOrder<OrderedInfo>;
using InfoOrderByTopic = std::map<ESM::RefId, ESM::InfoOrder<OrderedInfo>>;
class InfoCollection : public Collection<Info> class InfoCollection : public Collection<Info>
{ {
private: private:
bool load(const Info& record, bool base); void load(const Info& value, bool base);
public: public:
void load(ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue, InfosByTopic& infosByTopic); void load(ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue, InfoOrderByTopic& infoOrder);
void sort(const InfoOrderByTopic& infoOrders);
InfosRecordPtrByTopic getInfosByTopic() const; InfosRecordPtrByTopic getInfosByTopic() const;
int getAppendIndex(const ESM::RefId& id, UniversalId::Type type = UniversalId::Type_None) const override;
bool reorderRows(int baseIndex, const std::vector<int>& newOrder) override;
}; };
} }

View file

@ -9,6 +9,8 @@
#include <QVBoxLayout> #include <QVBoxLayout>
#include <apps/opencs/model/world/data.hpp> #include <apps/opencs/model/world/data.hpp>
#include <components/debug/debuglog.hpp>
#include <components/files/qtconversion.hpp> #include <components/files/qtconversion.hpp>
#include <filesystem> #include <filesystem>
@ -70,6 +72,7 @@ CSVDoc::LoadingDocument::LoadingDocument(CSMDoc::Document* document)
// error message // error message
mError = new QLabel(this); mError = new QLabel(this);
mError->setWordWrap(true); mError->setWordWrap(true);
mError->setTextInteractionFlags(Qt::TextSelectableByMouse);
mLayout->addWidget(mError); mLayout->addWidget(mError);
@ -120,6 +123,7 @@ void CSVDoc::LoadingDocument::abort(const std::string& error)
{ {
mAborted = true; mAborted = true;
mError->setText(QString::fromUtf8(("<font color=red>Loading failed: " + error + "</font>").c_str())); mError->setText(QString::fromUtf8(("<font color=red>Loading failed: " + error + "</font>").c_str()));
Log(Debug::Error) << "Loading failed: " << error;
mButtons->setStandardButtons(QDialogButtonBox::Close); mButtons->setStandardButtons(QDialogButtonBox::Close);
} }

View file

@ -27,7 +27,7 @@ class QUndoStack;
std::string CSVWorld::InfoCreator::getId() const std::string CSVWorld::InfoCreator::getId() const
{ {
std::string id = Misc::StringUtils::lowerCase(mTopic->text().toUtf8().constData()); const std::string topic = mTopic->text().toStdString();
std::string unique = QUuid::createUuid().toByteArray().data(); std::string unique = QUuid::createUuid().toByteArray().data();
@ -35,7 +35,7 @@ std::string CSVWorld::InfoCreator::getId() const
unique = unique.substr(1, unique.size() - 2); unique = unique.substr(1, unique.size() - 2);
return id + '#' + unique; return topic + '#' + unique;
} }
void CSVWorld::InfoCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const void CSVWorld::InfoCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const

View file

@ -9,18 +9,69 @@
#include <gmock/gmock.h> #include <gmock/gmock.h>
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <algorithm>
#include <array> #include <array>
#include <span>
#include <sstream> #include <sstream>
#include <vector>
namespace CSMWorld namespace CSMWorld
{ {
inline std::ostream& operator<<(std::ostream& stream, const Record<Info>* value)
{
return stream << "&Record{.mState=" << value->mState << ", .mId=" << value->get().mId << "}";
}
namespace namespace
{ {
using namespace ::testing; using namespace ::testing;
std::unique_ptr<std::stringstream> saveDialogueWithInfos( struct DialInfoData
const ESM::Dialogue& dialogue, std::span<const ESM::DialInfo> infos) {
ESM::DialInfo mValue;
bool mDeleted = false;
void save(ESM::ESMWriter& writer) const { mValue.save(writer, mDeleted); }
};
template <class T>
struct DialogueData
{
ESM::Dialogue mDialogue;
std::vector<T> mInfos;
};
DialogueData<ESM::DialInfo> generateDialogueWithInfos(
std::size_t infoCount, const ESM::RefId& dialogueId = ESM::RefId::stringRefId("dialogue"))
{
DialogueData<ESM::DialInfo> result;
result.mDialogue.blank();
result.mDialogue.mId = dialogueId;
for (std::size_t i = 0; i < infoCount; ++i)
{
ESM::DialInfo& info = result.mInfos.emplace_back();
info.blank();
info.mId = ESM::RefId::stringRefId("info" + std::to_string(i));
}
if (infoCount >= 2)
{
result.mInfos[0].mNext = result.mInfos[1].mId;
result.mInfos[infoCount - 1].mPrev = result.mInfos[infoCount - 2].mId;
}
for (std::size_t i = 1; i < infoCount - 1; ++i)
{
result.mInfos[i].mPrev = result.mInfos[i - 1].mId;
result.mInfos[i].mNext = result.mInfos[i + 1].mId;
}
return result;
}
template <class Infos>
std::unique_ptr<std::stringstream> saveDialogueWithInfos(const ESM::Dialogue& dialogue, Infos&& infos)
{ {
auto stream = std::make_unique<std::stringstream>(); auto stream = std::make_unique<std::stringstream>();
@ -32,7 +83,7 @@ namespace CSMWorld
dialogue.save(writer); dialogue.save(writer);
writer.endRecord(ESM::REC_DIAL); writer.endRecord(ESM::REC_DIAL);
for (const ESM::DialInfo& info : infos) for (const auto& info : infos)
{ {
writer.startRecord(ESM::REC_INFO); writer.startRecord(ESM::REC_INFO);
info.save(writer); info.save(writer);
@ -43,7 +94,7 @@ namespace CSMWorld
} }
void loadDialogueWithInfos(bool base, std::unique_ptr<std::stringstream> stream, InfoCollection& infoCollection, void loadDialogueWithInfos(bool base, std::unique_ptr<std::stringstream> stream, InfoCollection& infoCollection,
InfosByTopic& infosByTopic) InfoOrderByTopic& infoOrder)
{ {
ESM::ESMReader reader; ESM::ESMReader reader;
reader.open(std::move(stream), "test"); reader.open(std::move(stream), "test");
@ -59,64 +110,535 @@ namespace CSMWorld
{ {
ASSERT_EQ(reader.getRecName().toInt(), ESM::REC_INFO); ASSERT_EQ(reader.getRecName().toInt(), ESM::REC_INFO);
reader.getRecHeader(); reader.getRecHeader();
infoCollection.load(reader, base, dialogue, infosByTopic); infoCollection.load(reader, base, dialogue, infoOrder);
} }
} }
void saveAndLoadDialogueWithInfos(const ESM::Dialogue& dialogue, std::span<const ESM::DialInfo> infos, template <class Infos>
bool base, InfoCollection& infoCollection, InfosByTopic& infosByTopic) void saveAndLoadDialogueWithInfos(const ESM::Dialogue& dialogue, Infos&& infos, bool base,
InfoCollection& infoCollection, InfoOrderByTopic& infoOrder)
{ {
loadDialogueWithInfos(base, saveDialogueWithInfos(dialogue, infos), infoCollection, infosByTopic); loadDialogueWithInfos(base, saveDialogueWithInfos(dialogue, infos), infoCollection, infoOrder);
}
template <class T>
void saveAndLoadDialogueWithInfos(
const DialogueData<T>& data, bool base, InfoCollection& infoCollection, InfoOrderByTopic& infoOrder)
{
saveAndLoadDialogueWithInfos(data.mDialogue, data.mInfos, base, infoCollection, infoOrder);
} }
TEST(CSMWorldInfoCollectionTest, loadShouldAddRecord) TEST(CSMWorldInfoCollectionTest, loadShouldAddRecord)
{ {
ESM::Dialogue dialogue; ESM::Dialogue dialogue;
dialogue.blank(); dialogue.blank();
dialogue.mId = ESM::RefId::stringRefId("dialogue1"); dialogue.mId = ESM::RefId::stringRefId("dialogue");
ESM::DialInfo info; ESM::DialInfo info;
info.blank(); info.blank();
info.mId = ESM::RefId::stringRefId("info1"); info.mId = ESM::RefId::stringRefId("info0");
const bool base = true; const bool base = true;
InfosByTopic infosByTopic; InfoOrderByTopic infoOrder;
InfoCollection collection; InfoCollection collection;
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infosByTopic); saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);
EXPECT_EQ(collection.getSize(), 1); EXPECT_EQ(collection.getSize(), 1);
ASSERT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue1#info1")), 0); ASSERT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
const Record<Info>& record = collection.getRecord(0); const Record<Info>& record = collection.getRecord(0);
ASSERT_EQ(record.mState, RecordBase::State_BaseOnly); ASSERT_EQ(record.mState, RecordBase::State_BaseOnly);
EXPECT_EQ(record.mBase.mTopicId, dialogue.mId); EXPECT_EQ(record.mBase.mTopicId, dialogue.mId);
EXPECT_EQ(record.mBase.mOriginalId, info.mId); EXPECT_EQ(record.mBase.mOriginalId, info.mId);
EXPECT_EQ(record.mBase.mId, ESM::RefId::stringRefId("dialogue1#info1")); EXPECT_EQ(record.mBase.mId, ESM::RefId::stringRefId("dialogue#info0"));
}
TEST(CSMWorldInfoCollectionTest, loadShouldAddRecordAndMarkModifiedOnlyWhenNotBase)
{
ESM::Dialogue dialogue;
dialogue.blank();
dialogue.mId = ESM::RefId::stringRefId("dialogue");
ESM::DialInfo info;
info.blank();
info.mId = ESM::RefId::stringRefId("info0");
const bool base = false;
InfoOrderByTopic infoOrder;
InfoCollection collection;
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);
EXPECT_EQ(collection.getSize(), 1);
ASSERT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
const Record<Info>& record = collection.getRecord(0);
ASSERT_EQ(record.mState, RecordBase::State_ModifiedOnly);
EXPECT_EQ(record.mModified.mTopicId, dialogue.mId);
EXPECT_EQ(record.mModified.mOriginalId, info.mId);
EXPECT_EQ(record.mModified.mId, ESM::RefId::stringRefId("dialogue#info0"));
} }
TEST(CSMWorldInfoCollectionTest, loadShouldUpdateRecord) TEST(CSMWorldInfoCollectionTest, loadShouldUpdateRecord)
{ {
ESM::Dialogue dialogue; ESM::Dialogue dialogue;
dialogue.blank(); dialogue.blank();
dialogue.mId = ESM::RefId::stringRefId("dialogue1"); dialogue.mId = ESM::RefId::stringRefId("dialogue");
ESM::DialInfo info; ESM::DialInfo info;
info.blank(); info.blank();
info.mId = ESM::RefId::stringRefId("info1"); info.mId = ESM::RefId::stringRefId("info0");
const bool base = true; const bool base = true;
InfosByTopic infosByTopic; InfoOrderByTopic infoOrder;
InfoCollection collection; InfoCollection collection;
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infosByTopic); saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);
ESM::DialInfo updatedInfo = info; ESM::DialInfo updatedInfo = info;
updatedInfo.mActor = ESM::RefId::stringRefId("newActor"); updatedInfo.mActor = ESM::RefId::stringRefId("newActor");
saveAndLoadDialogueWithInfos(dialogue, std::array{ updatedInfo }, base, collection, infosByTopic); saveAndLoadDialogueWithInfos(dialogue, std::array{ updatedInfo }, base, collection, infoOrder);
ASSERT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue1#info1")), 0); ASSERT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
const Record<Info>& record = collection.getRecord(0); const Record<Info>& record = collection.getRecord(0);
ASSERT_EQ(record.mState, RecordBase::State_BaseOnly); ASSERT_EQ(record.mState, RecordBase::State_BaseOnly);
EXPECT_EQ(record.mBase.mActor, ESM::RefId::stringRefId("newActor")); EXPECT_EQ(record.mBase.mActor, ESM::RefId::stringRefId("newActor"));
} }
TEST(CSMWorldInfoCollectionTest, loadShouldUpdateRecordAndMarkModifiedWhenNotBase)
{
ESM::Dialogue dialogue;
dialogue.blank();
dialogue.mId = ESM::RefId::stringRefId("dialogue");
ESM::DialInfo info;
info.blank();
info.mId = ESM::RefId::stringRefId("info0");
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);
ESM::DialInfo updatedInfo = info;
updatedInfo.mActor = ESM::RefId::stringRefId("newActor");
saveAndLoadDialogueWithInfos(dialogue, std::array{ updatedInfo }, false, collection, infoOrder);
ASSERT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
const Record<Info>& record = collection.getRecord(0);
ASSERT_EQ(record.mState, RecordBase::State_Modified);
EXPECT_EQ(record.mModified.mActor, ESM::RefId::stringRefId("newActor"));
}
TEST(CSMWorldInfoCollectionTest, loadShouldSkipAbsentDeletedRecord)
{
ESM::Dialogue dialogue;
dialogue.blank();
dialogue.mId = ESM::RefId::stringRefId("dialogue");
DialInfoData info;
info.mValue.blank();
info.mValue.mId = ESM::RefId::stringRefId("info0");
info.mDeleted = true;
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);
EXPECT_EQ(collection.getSize(), 0);
}
TEST(CSMWorldInfoCollectionTest, loadShouldRemovePresentDeletedBaseRecord)
{
ESM::Dialogue dialogue;
dialogue.blank();
dialogue.mId = ESM::RefId::stringRefId("dialogue");
DialInfoData info;
info.mValue.blank();
info.mValue.mId = ESM::RefId::stringRefId("info0");
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);
info.mDeleted = true;
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);
EXPECT_EQ(collection.getSize(), 0);
}
TEST(CSMWorldInfoCollectionTest, loadShouldMarkAsDeletedNotBaseRecord)
{
ESM::Dialogue dialogue;
dialogue.blank();
dialogue.mId = ESM::RefId::stringRefId("dialogue");
DialInfoData info;
info.mValue.blank();
info.mValue.mId = ESM::RefId::stringRefId("info0");
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);
info.mDeleted = true;
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, false, collection, infoOrder);
EXPECT_EQ(collection.getSize(), 1);
EXPECT_EQ(
collection.getRecord(ESM::RefId::stringRefId("dialogue#info0")).mState, RecordBase::State_Deleted);
}
TEST(CSMWorldInfoCollectionTest, sortShouldOrderRecordsBasedOnPrevAndNext)
{
const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);
collection.sort(infoOrder);
EXPECT_EQ(collection.getSize(), 3);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 1);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 2);
}
TEST(CSMWorldInfoCollectionTest, sortShouldOrderRecordsBasedOnPrevAndNextWhenReversed)
{
DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);
std::reverse(data.mInfos.begin(), data.mInfos.end());
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);
collection.sort(infoOrder);
EXPECT_EQ(collection.getSize(), 3);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 1);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 2);
}
TEST(CSMWorldInfoCollectionTest, sortShouldInsertNewRecordBasedOnPrev)
{
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);
saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);
ESM::DialInfo newInfo;
newInfo.blank();
newInfo.mId = ESM::RefId::stringRefId("newInfo");
newInfo.mPrev = data.mInfos[1].mId;
newInfo.mNext = ESM::RefId::stringRefId("invalid");
saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ newInfo }, base, collection, infoOrder);
collection.sort(infoOrder);
EXPECT_EQ(collection.getSize(), 4);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 1);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#newInfo")), 2);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 3);
}
TEST(CSMWorldInfoCollectionTest, sortShouldInsertNewRecordBasedOnNextWhenPrevIsNotFound)
{
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);
saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);
ESM::DialInfo newInfo;
newInfo.blank();
newInfo.mId = ESM::RefId::stringRefId("newInfo");
newInfo.mPrev = ESM::RefId::stringRefId("invalid");
newInfo.mNext = data.mInfos[2].mId;
saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ newInfo }, base, collection, infoOrder);
collection.sort(infoOrder);
EXPECT_EQ(collection.getSize(), 4);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 1);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#newInfo")), 2);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 3);
}
TEST(CSMWorldInfoCollectionTest, sortShouldInsertNewRecordToFrontWhenPrevIsEmpty)
{
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);
saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);
ESM::DialInfo newInfo;
newInfo.blank();
newInfo.mId = ESM::RefId::stringRefId("newInfo");
newInfo.mNext = ESM::RefId::stringRefId("invalid");
saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ newInfo }, base, collection, infoOrder);
collection.sort(infoOrder);
EXPECT_EQ(collection.getSize(), 4);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#newInfo")), 0);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 1);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 2);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 3);
}
TEST(CSMWorldInfoCollectionTest, sortShouldInsertNewRecordToBackWhenNextIsEmpty)
{
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);
saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);
ESM::DialInfo newInfo;
newInfo.blank();
newInfo.mId = ESM::RefId::stringRefId("newInfo");
newInfo.mPrev = ESM::RefId::stringRefId("invalid");
saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ newInfo }, base, collection, infoOrder);
collection.sort(infoOrder);
EXPECT_EQ(collection.getSize(), 4);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 1);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 2);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#newInfo")), 3);
}
TEST(CSMWorldInfoCollectionTest, sortShouldMoveBackwardUpdatedRecordBasedOnPrev)
{
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);
saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);
ESM::DialInfo updatedInfo = data.mInfos[2];
updatedInfo.mPrev = data.mInfos[0].mId;
updatedInfo.mNext = ESM::RefId::stringRefId("invalid");
saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ updatedInfo }, base, collection, infoOrder);
collection.sort(infoOrder);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 0);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 2);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 1);
}
TEST(CSMWorldInfoCollectionTest, sortShouldMoveForwardUpdatedRecordBasedOnPrev)
{
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);
saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);
ESM::DialInfo updatedInfo = data.mInfos[0];
updatedInfo.mPrev = data.mInfos[1].mId;
updatedInfo.mNext = ESM::RefId::stringRefId("invalid");
saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ updatedInfo }, base, collection, infoOrder);
collection.sort(infoOrder);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 1);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 0);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 2);
}
TEST(CSMWorldInfoCollectionTest, sortShouldMoveToFrontUpdatedRecordWhenPrevIsEmpty)
{
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);
saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);
ESM::DialInfo updatedInfo = data.mInfos[2];
updatedInfo.mPrev = ESM::RefId();
updatedInfo.mNext = ESM::RefId::stringRefId("invalid");
saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ updatedInfo }, base, collection, infoOrder);
collection.sort(infoOrder);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 1);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 2);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 0);
}
TEST(CSMWorldInfoCollectionTest, sortShouldMoveToBackUpdatedRecordWhenNextIsEmpty)
{
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
const DialogueData<ESM::DialInfo> data = generateDialogueWithInfos(3);
saveAndLoadDialogueWithInfos(data, base, collection, infoOrder);
ESM::DialInfo updatedInfo = data.mInfos[0];
updatedInfo.mPrev = ESM::RefId::stringRefId("invalid");
updatedInfo.mNext = ESM::RefId();
saveAndLoadDialogueWithInfos(data.mDialogue, std::array{ updatedInfo }, base, collection, infoOrder);
collection.sort(infoOrder);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info0")), 2);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info1")), 0);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue#info2")), 1);
}
TEST(CSMWorldInfoCollectionTest, sortShouldProvideStableOrderByTopic)
{
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
saveAndLoadDialogueWithInfos(
generateDialogueWithInfos(2, ESM::RefId::stringRefId("dialogue2")), base, collection, infoOrder);
saveAndLoadDialogueWithInfos(
generateDialogueWithInfos(2, ESM::RefId::stringRefId("dialogue0")), base, collection, infoOrder);
saveAndLoadDialogueWithInfos(
generateDialogueWithInfos(2, ESM::RefId::stringRefId("dialogue1")), base, collection, infoOrder);
collection.sort(infoOrder);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info0")), 0);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info1")), 1);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue1#info0")), 2);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue1#info1")), 3);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue2#info0")), 4);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue2#info1")), 5);
}
TEST(CSMWorldInfoCollectionTest, getAppendIndexShouldReturnFirstIndexAfterInfoTopic)
{
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
saveAndLoadDialogueWithInfos(
generateDialogueWithInfos(2, ESM::RefId::stringRefId("dialogue0")), base, collection, infoOrder);
saveAndLoadDialogueWithInfos(
generateDialogueWithInfos(2, ESM::RefId::stringRefId("dialogue1")), base, collection, infoOrder);
collection.sort(infoOrder);
EXPECT_EQ(collection.getAppendIndex(ESM::RefId::stringRefId("dialogue0#info2")), 2);
}
TEST(CSMWorldInfoCollectionTest, reorderRowsShouldFailWhenOutOfBounds)
{
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
saveAndLoadDialogueWithInfos(
generateDialogueWithInfos(2, ESM::RefId::stringRefId("dialogue0")), base, collection, infoOrder);
saveAndLoadDialogueWithInfos(
generateDialogueWithInfos(2, ESM::RefId::stringRefId("dialogue1")), base, collection, infoOrder);
EXPECT_FALSE(collection.reorderRows(5, {}));
}
TEST(CSMWorldInfoCollectionTest, reorderRowsShouldFailWhenAppliedToDifferentTopics)
{
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
saveAndLoadDialogueWithInfos(
generateDialogueWithInfos(2, ESM::RefId::stringRefId("dialogue0")), base, collection, infoOrder);
saveAndLoadDialogueWithInfos(
generateDialogueWithInfos(2, ESM::RefId::stringRefId("dialogue1")), base, collection, infoOrder);
EXPECT_FALSE(collection.reorderRows(0, { 0, 1, 2 }));
}
TEST(CSMWorldInfoCollectionTest, reorderRowsShouldSucceedWhenAppliedToOneTopic)
{
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
saveAndLoadDialogueWithInfos(
generateDialogueWithInfos(3, ESM::RefId::stringRefId("dialogue0")), base, collection, infoOrder);
saveAndLoadDialogueWithInfos(
generateDialogueWithInfos(3, ESM::RefId::stringRefId("dialogue1")), base, collection, infoOrder);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info0")), 0);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info1")), 1);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info2")), 2);
EXPECT_TRUE(collection.reorderRows(1, { 1, 0 }));
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info0")), 0);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info1")), 2);
EXPECT_EQ(collection.searchId(ESM::RefId::stringRefId("dialogue0#info2")), 1);
}
MATCHER_P(RecordPtrIdIs, v, "")
{
return v == arg->get().mId;
}
TEST(CSMWorldInfoCollectionTest, getInfosByTopicShouldReturnRecordsGroupedByTopic)
{
const bool base = true;
InfoOrderByTopic infoOrder;
InfoCollection collection;
saveAndLoadDialogueWithInfos(
generateDialogueWithInfos(2, ESM::RefId::stringRefId("d0")), base, collection, infoOrder);
saveAndLoadDialogueWithInfos(
generateDialogueWithInfos(2, ESM::RefId::stringRefId("d1")), base, collection, infoOrder);
collection.sort(infoOrder);
EXPECT_THAT(collection.getInfosByTopic(),
UnorderedElementsAre(Pair("d0", ElementsAre(RecordPtrIdIs("d0#info0"), RecordPtrIdIs("d0#info1"))),
Pair("d1", ElementsAre(RecordPtrIdIs("d1#info0"), RecordPtrIdIs("d1#info1")))));
}
} }
} }

View file

@ -318,7 +318,7 @@ namespace MWWorld
{ {
if (dialogue) if (dialogue)
{ {
dialogue->readInfo(esm, esm.getIndex() != 0); dialogue->readInfo(esm);
} }
else else
{ {

View file

@ -1070,7 +1070,7 @@ namespace MWWorld
// DialInfos marked as deleted are kept during the loading phase, so that the linked list // DialInfos marked as deleted are kept during the loading phase, so that the linked list
// structure is kept intact for inserting further INFOs. Delete them now that loading is done. // structure is kept intact for inserting further INFOs. Delete them now that loading is done.
for (auto& [_, dial] : mStatic) for (auto& [_, dial] : mStatic)
dial.clearDeletedInfos(); dial.setUp();
mShared.clear(); mShared.clear();
mShared.reserve(mStatic.size()); mShared.reserve(mStatic.size());

View file

@ -671,7 +671,7 @@ namespace
EXPECT_THAT(dialogue->mInfo, ElementsAre(HasIdEqualTo("info0"), HasIdEqualTo("info1"), HasIdEqualTo("info2"))); EXPECT_THAT(dialogue->mInfo, ElementsAre(HasIdEqualTo("info0"), HasIdEqualTo("info1"), HasIdEqualTo("info2")));
} }
TEST(MWWorldStoreTest, shouldLoadDialogueWithInfosAsIsWhenReversed) TEST(MWWorldStoreTest, shouldLoadDialogueWithInfosAndOrderWhenReversed)
{ {
DialogueData data = generateDialogueWithInfos(3); DialogueData data = generateDialogueWithInfos(3);
@ -683,7 +683,7 @@ namespace
const ESM::Dialogue* dialogue = esmStore.get<ESM::Dialogue>().search(ESM::RefId::stringRefId("dialogue")); const ESM::Dialogue* dialogue = esmStore.get<ESM::Dialogue>().search(ESM::RefId::stringRefId("dialogue"));
ASSERT_NE(dialogue, nullptr); ASSERT_NE(dialogue, nullptr);
EXPECT_THAT(dialogue->mInfo, ElementsAre(HasIdEqualTo("info2"), HasIdEqualTo("info1"), HasIdEqualTo("info0"))); EXPECT_THAT(dialogue->mInfo, ElementsAre(HasIdEqualTo("info0"), HasIdEqualTo("info1"), HasIdEqualTo("info2")));
} }
TEST(MWWorldStoreTest, shouldLoadDialogueWithInfosInsertingNewRecordBasedOnPrev) TEST(MWWorldStoreTest, shouldLoadDialogueWithInfosInsertingNewRecordBasedOnPrev)

View file

@ -101,6 +101,7 @@ add_component_dir (esm3
inventorystate containerstate npcstate creaturestate dialoguestate statstate npcstats creaturestats inventorystate containerstate npcstate creaturestate dialoguestate statstate npcstats creaturestats
weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate debugprofile weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate debugprofile
aisequence magiceffects custommarkerstate stolenitems transport animationstate controlsstate mappings readerscache aisequence magiceffects custommarkerstate stolenitems transport animationstate controlsstate mappings readerscache
infoorder
) )
add_component_dir (esm3terrain add_component_dir (esm3terrain

View file

@ -0,0 +1,114 @@
#ifndef OPENMW_COMPONENTS_ESM3_INFOORDER_H
#define OPENMW_COMPONENTS_ESM3_INFOORDER_H
#include "components/esm/refid.hpp"
#include <iterator>
#include <list>
#include <type_traits>
#include <unordered_map>
#include <utility>
namespace ESM
{
template <class T>
class InfoOrder
{
public:
const std::list<T>& getOrderedInfo() const { return mOrderedInfo; }
template <class V>
void insertInfo(V&& value, bool deleted)
{
static_assert(std::is_same_v<std::decay_t<V>, T>);
auto it = mInfoPositions.find(value.mId);
if (it != mInfoPositions.end() && it->second.mPosition->mPrev == value.mPrev)
{
*it->second.mPosition = std::forward<V>(value);
it->second.mDeleted = deleted;
return;
}
if (it == mInfoPositions.end())
it = mInfoPositions.emplace(value.mId, Item{ .mPosition = mOrderedInfo.end(), .mDeleted = deleted })
.first;
Item& item = it->second;
const auto insertOrSplice = [&](typename std::list<T>::const_iterator before) {
if (item.mPosition == mOrderedInfo.end())
item.mPosition = mOrderedInfo.insert(before, std::forward<V>(value));
else
mOrderedInfo.splice(before, mOrderedInfo, item.mPosition);
};
if (value.mPrev.empty())
{
insertOrSplice(mOrderedInfo.begin());
return;
}
const auto prevIt = mInfoPositions.find(value.mPrev);
if (prevIt != mInfoPositions.end())
{
insertOrSplice(std::next(prevIt->second.mPosition));
return;
}
const auto nextIt = mInfoPositions.find(value.mNext);
if (nextIt != mInfoPositions.end())
{
insertOrSplice(nextIt->second.mPosition);
return;
}
insertOrSplice(mOrderedInfo.end());
}
void removeInfo(const RefId& infoRefId)
{
const auto it = mInfoPositions.find(infoRefId);
if (it == mInfoPositions.end())
return;
mOrderedInfo.erase(it->second.mPosition);
mInfoPositions.erase(it);
}
void removeDeleted()
{
for (auto it = mInfoPositions.begin(); it != mInfoPositions.end();)
{
if (!it->second.mDeleted)
{
++it;
continue;
}
mOrderedInfo.erase(it->second.mPosition);
it = mInfoPositions.erase(it);
}
}
void extractOrderedInfo(std::list<T>& info)
{
info = mOrderedInfo;
mInfoPositions.clear();
}
private:
struct Item
{
typename std::list<T>::iterator mPosition;
bool mDeleted = false;
};
std::list<T> mOrderedInfo;
std::unordered_map<RefId, Item> mInfoPositions;
};
}
#endif

View file

@ -70,62 +70,17 @@ namespace ESM
mInfo.clear(); mInfo.clear();
} }
void Dialogue::readInfo(ESMReader& esm, bool merge) void Dialogue::readInfo(ESMReader& esm)
{ {
DialInfo info; DialInfo info;
bool isDeleted = false; bool isDeleted = false;
info.load(esm, isDeleted); info.load(esm, isDeleted);
mInfoOrder.insertInfo(std::move(info), isDeleted);
if (!merge || mInfo.empty())
{
mLookup[info.mId] = std::make_pair(mInfo.insert(mInfo.end(), info), isDeleted);
return;
}
LookupMap::iterator lookup = mLookup.find(info.mId);
if (lookup != mLookup.end())
{
auto it = lookup->second.first;
if (it->mPrev == info.mPrev)
{
*it = info;
lookup->second.second = isDeleted;
return;
}
// Since the new version of this record has a different prev linked list connection, we need to re-insert
// the record
mInfo.erase(it);
mLookup.erase(lookup);
}
if (!info.mPrev.empty())
{
lookup = mLookup.find(info.mPrev);
if (lookup != mLookup.end())
{
auto it = lookup->second.first;
mLookup[info.mId] = std::make_pair(mInfo.insert(++it, info), isDeleted);
}
else
mLookup[info.mId] = std::make_pair(mInfo.insert(mInfo.end(), info), isDeleted);
}
else
mLookup[info.mId] = std::make_pair(mInfo.insert(mInfo.begin(), info), isDeleted);
} }
void Dialogue::clearDeletedInfos() void Dialogue::setUp()
{ {
LookupMap::const_iterator current = mLookup.begin(); mInfoOrder.removeDeleted();
LookupMap::const_iterator end = mLookup.end(); mInfoOrder.extractOrderedInfo(mInfo);
for (; current != end; ++current)
{
if (current->second.second)
{
mInfo.erase(current->second.first);
}
}
mLookup.clear();
} }
} }

View file

@ -7,6 +7,7 @@
#include "components/esm/defs.hpp" #include "components/esm/defs.hpp"
#include "components/esm/refid.hpp" #include "components/esm/refid.hpp"
#include "components/esm3/infoorder.hpp"
#include "loadinfo.hpp" #include "loadinfo.hpp"
@ -23,6 +24,8 @@ namespace ESM
struct Dialogue struct Dialogue
{ {
using InfoContainer = std::list<DialInfo>;
constexpr static RecNameInts sRecordId = REC_DIAL; constexpr static RecNameInts sRecordId = REC_DIAL;
/// Return a string descriptor for this record type. Currently used for debugging / error logs only. /// Return a string descriptor for this record type. Currently used for debugging / error logs only.
static std::string_view getRecordType() { return "Dialogue"; } static std::string_view getRecordType() { return "Dialogue"; }
@ -39,17 +42,12 @@ namespace ESM
RefId mId; RefId mId;
signed char mType; signed char mType;
InfoContainer mInfo;
typedef std::list<DialInfo> InfoContainer; InfoOrder<DialInfo> mInfoOrder;
// Parameters: Info ID, (Info iterator, Deleted flag) // Parameters: Info ID, (Info iterator, Deleted flag)
typedef std::map<ESM::RefId, std::pair<InfoContainer::iterator, bool>> LookupMap; typedef std::map<ESM::RefId, std::pair<InfoContainer::iterator, bool>> LookupMap;
InfoContainer mInfo;
// This is only used during the loading phase to speed up DialInfo merging.
LookupMap mLookup;
void load(ESMReader& esm, bool& isDeleted); void load(ESMReader& esm, bool& isDeleted);
///< Loads all sub-records of Dialogue record ///< Loads all sub-records of Dialogue record
void loadId(ESMReader& esm); void loadId(ESMReader& esm);
@ -60,11 +58,10 @@ namespace ESM
void save(ESMWriter& esm, bool isDeleted = false) const; void save(ESMWriter& esm, bool isDeleted = false) const;
/// Remove all INFOs that are deleted /// Remove all INFOs that are deleted
void clearDeletedInfos(); void setUp();
/// Read the next info record /// Read the next info record
/// @param merge Merge with existing list, or just push each record to the end of the list? void readInfo(ESMReader& esm);
void readInfo(ESMReader& esm, bool merge);
void blank(); void blank();
///< Set record to default state (does not touch the ID and does not change the type). ///< Set record to default state (does not touch the ID and does not change the type).