Fix loading, inserting and moving topic info records

Topic info records need to have specific order defined via mNext and mPrev
fields (next and previous records). When loading multiple files a record may be
inserted into middle of the topic but neighborhood records may not be aware of
it. Having the order it's possible to move the records within one topic.

Sort the record once after loading all content files but preserve the order for
all other operations. Use std::map to group info ids by topic to make sure the
topics order is stable. Keep order within a topic for info ids on loading new
records. Use this order later for sorting the records.
depth-refraction
elsid 2 years ago
parent 899c302b14
commit e892c62b10
No known key found for this signature in database
GPG Key ID: 4DE04C198CBA7625

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

@ -9,6 +9,7 @@
#include <stdexcept>
#include <string>
#include <string_view>
#include <unordered_set>
#include <vector>
#include <QVariant>
@ -17,6 +18,7 @@
#include "collectionbase.hpp"
#include "columnbase.hpp"
#include "info.hpp"
#include "land.hpp"
#include "landtexture.hpp"
#include "record.hpp"
@ -24,12 +26,29 @@
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>
void setRecordId(const decltype(T::mId)& id, T& record)
{
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>
auto getRecordId(const T& record)
{
@ -85,6 +104,8 @@ namespace CSMWorld
protected:
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);
///< 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).
@ -191,6 +212,20 @@ namespace CSMWorld
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>
bool Collection<ESXRecordT>::reorderRowsImp(int baseIndex, const std::vector<int>& newOrder)
{

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

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

@ -1,91 +1,141 @@
#include "infocollection.hpp"
#include <algorithm>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
#include <components/debug/debuglog.hpp>
#include <components/esm3/loaddial.hpp>
#include "components/debug/debuglog.hpp"
#include "components/esm3/infoorder.hpp"
#include "components/esm3/loaddial.hpp"
#include "components/esm3/loadinfo.hpp"
#include "collection.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)
{
// new record
auto record2 = std::make_unique<Record<Info>>();
record2->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly;
(base ? record2->mBase : record2->mModified) = record;
auto record = std::make_unique<Record<Info>>();
record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly;
(base ? record->mBase : record->mModified) = value;
appendRecord(std::move(record2));
return true;
insertRecord(std::move(record), getSize());
}
else
{
// old record
auto record2 = std::make_unique<Record<Info>>(getRecord(index));
auto record = std::make_unique<Record<Info>>(getRecord(index));
if (base)
record2->mBase = record;
record->mBase = value;
else
record2->setModified(record);
setRecord(index, std::move(record2));
record->setModified(value);
return false;
setRecord(index, std::move(record));
}
}
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;
bool isDeleted = false;
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)
{
int index = searchId(id);
const int index = searchId(id);
if (index == -1)
{
// deleting a record that does not exist
// ignore it for now
/// \todo report the problem to the user
Log(Debug::Warning) << "Trying to delete absent info \"" << info.mId << "\" from topic \"" << dialogue.mId
<< "\"";
return;
}
else if (base)
if (base)
{
infoOrders.at(dialogue.mId).removeInfo(id);
removeRows(index, 1);
return;
}
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))
infosByTopic[dialogue.mId].push_back(info.mId);
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
{
InfosRecordPtrByTopic result;
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;
}
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);
}

@ -1,6 +1,7 @@
#ifndef CSM_WOLRD_INFOCOLLECTION_H
#define CSM_WOLRD_INFOCOLLECTION_H
#include <map>
#include <string>
#include <unordered_map>
#include <vector>
@ -12,22 +13,47 @@ namespace ESM
{
struct Dialogue;
class ESMReader;
template <class T>
class InfoOrder;
}
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>*>>;
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>
{
private:
bool load(const Info& record, bool base);
void load(const Info& value, bool base);
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;
int getAppendIndex(const ESM::RefId& id, UniversalId::Type type = UniversalId::Type_None) const override;
bool reorderRows(int baseIndex, const std::vector<int>& newOrder) override;
};
}

@ -27,7 +27,7 @@ class QUndoStack;
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();
@ -35,7 +35,7 @@ std::string CSVWorld::InfoCreator::getId() const
unique = unique.substr(1, unique.size() - 2);
return id + '#' + unique;
return topic + '#' + unique;
}
void CSVWorld::InfoCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const

@ -9,18 +9,69 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <span>
#include <sstream>
#include <vector>
namespace CSMWorld
{
inline std::ostream& operator<<(std::ostream& stream, const Record<Info>* value)
{
return stream << "&Record{.mState=" << value->mState << ", .mId=" << value->get().mId << "}";
}
namespace
{
using namespace ::testing;
std::unique_ptr<std::stringstream> saveDialogueWithInfos(
const ESM::Dialogue& dialogue, std::span<const ESM::DialInfo> infos)
struct DialInfoData
{
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>();
@ -32,7 +83,7 @@ namespace CSMWorld
dialogue.save(writer);
writer.endRecord(ESM::REC_DIAL);
for (const ESM::DialInfo& info : infos)
for (const auto& info : infos)
{
writer.startRecord(ESM::REC_INFO);
info.save(writer);
@ -43,7 +94,7 @@ namespace CSMWorld
}
void loadDialogueWithInfos(bool base, std::unique_ptr<std::stringstream> stream, InfoCollection& infoCollection,
InfosByTopic& infosByTopic)
InfoOrderByTopic& infoOrder)
{
ESM::ESMReader reader;
reader.open(std::move(stream), "test");
@ -59,64 +110,535 @@ namespace CSMWorld
{
ASSERT_EQ(reader.getRecName().toInt(), ESM::REC_INFO);
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,
bool base, InfoCollection& infoCollection, InfosByTopic& infosByTopic)
template <class Infos>
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)
{
ESM::Dialogue dialogue;
dialogue.blank();
dialogue.mId = ESM::RefId::stringRefId("dialogue1");
dialogue.mId = ESM::RefId::stringRefId("dialogue");
ESM::DialInfo info;
info.blank();
info.mId = ESM::RefId::stringRefId("info1");
info.mId = ESM::RefId::stringRefId("info0");
const bool base = true;
InfosByTopic infosByTopic;
InfoOrderByTopic infoOrder;
InfoCollection collection;
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infosByTopic);
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);
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);
ASSERT_EQ(record.mState, RecordBase::State_BaseOnly);
EXPECT_EQ(record.mBase.mTopicId, dialogue.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)
{
ESM::Dialogue dialogue;
dialogue.blank();
dialogue.mId = ESM::RefId::stringRefId("dialogue1");
dialogue.mId = ESM::RefId::stringRefId("dialogue");
ESM::DialInfo info;
info.blank();
info.mId = ESM::RefId::stringRefId("info1");
info.mId = ESM::RefId::stringRefId("info0");
const bool base = true;
InfosByTopic infosByTopic;
InfoOrderByTopic infoOrder;
InfoCollection collection;
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infosByTopic);
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);
ESM::DialInfo updatedInfo = info;
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);
ASSERT_EQ(record.mState, RecordBase::State_BaseOnly);
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")))));
}
}
}

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

@ -0,0 +1,87 @@
#include "infoorder.hpp"
namespace ESM
{
const std::list<OrderedInfo>* InfoOrder::findInfosByTopic(const ESM::RefId& refId) const
{
const auto it = mOrderByTopic.find(refId);
if (it == mOrderByTopic.end())
return nullptr;
return &it->second;
}
void InfoOrder::insertInfo(const OrderedInfo& info)
{
auto it = mInfoPositions.find(info.mId);
if (it != mInfoPositions.end() && it->second->mPrev == info.mPrev)
{
it->second->mNext = info.mNext;
return;
}
auto& infos = mOrderByTopic[info.mTopicId];
if (it == mInfoPositions.end())
it = mInfoPositions.emplace(info.mId, infos.end()).first;
std::list<OrderedInfo>::iterator& position = it->second;
const auto insertOrSplice = [&](std::list<OrderedInfo>::const_iterator before) {
if (position == infos.end())
position = infos.insert(before, info);
else
infos.splice(before, infos, position);
};
if (info.mPrev.empty())
{
insertOrSplice(infos.begin());
return;
}
const auto prevIt = mInfoPositions.find(info.mPrev);
if (prevIt != mInfoPositions.end())
{
insertOrSplice(std::next(prevIt->second));
return;
}
const auto nextIt = mInfoPositions.find(info.mNext);
if (nextIt != mInfoPositions.end())
{
insertOrSplice(nextIt->second);
return;
}
insertOrSplice(infos.end());
}
void InfoOrder::removeInfo(const ESM::RefId& infoRefId)
{
const auto it = mInfoPositions.find(infoRefId);
if (it == mInfoPositions.end())
return;
const auto topicIt = mOrderByTopic.find(it->second->mTopicId);
if (topicIt != mOrderByTopic.end())
topicIt->second.erase(it->second);
mInfoPositions.erase(it);
}
void InfoOrder::removeTopic(const ESM::RefId& topicRefId)
{
const auto it = mOrderByTopic.find(topicRefId);
if (it == mOrderByTopic.end())
return;
for (const OrderedInfo& info : it->second)
mInfoPositions.erase(info.mId);
mOrderByTopic.erase(it);
}
}

@ -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
Loading…
Cancel
Save