From e892c62b10a947e1ddddba30e7c3eafb8ebedd13 Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 8 Mar 2023 02:17:15 +0100 Subject: [PATCH] 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. --- apps/opencs/model/doc/loader.cpp | 1 + apps/opencs/model/world/collection.hpp | 35 ++ apps/opencs/model/world/data.cpp | 66 +- apps/opencs/model/world/data.hpp | 7 +- apps/opencs/model/world/infocollection.cpp | 132 ++-- apps/opencs/model/world/infocollection.hpp | 32 +- apps/opencs/view/world/infocreator.cpp | 4 +- .../model/world/testinfocollection.cpp | 564 +++++++++++++++++- components/CMakeLists.txt | 1 + components/esm3/infoorder.cpp | 87 +++ components/esm3/infoorder.hpp | 114 ++++ 11 files changed, 947 insertions(+), 96 deletions(-) create mode 100644 components/esm3/infoorder.cpp create mode 100644 components/esm3/infoorder.hpp diff --git a/apps/opencs/model/doc/loader.cpp b/apps/opencs/model/doc/loader.cpp index d3a3490f73..46dea447fe 100644 --- a/apps/opencs/model/doc/loader.cpp +++ b/apps/opencs/model/doc/loader.cpp @@ -123,6 +123,7 @@ void CSMDoc::Loader::load() } else { + document->getData().finishLoading(); done = true; } diff --git a/apps/opencs/model/world/collection.hpp b/apps/opencs/model/world/collection.hpp index 0ec7142d2a..e177291a7c 100644 --- a/apps/opencs/model/world/collection.hpp +++ b/apps/opencs/model/world/collection.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -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 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 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 auto getRecordId(const T& record) { @@ -85,6 +104,8 @@ namespace CSMWorld protected: const std::vector>>& getRecords() const; + void reorderRowsImp(const std::vector& indexOrder); + bool reorderRowsImp(int baseIndex, const std::vector& 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 + void Collection::reorderRowsImp(const std::vector& indexOrder) + { + assert(indexOrder.size() == mRecords.size()); + assert(std::unordered_set(indexOrder.begin(), indexOrder.end()).size() == indexOrder.size()); + std::vector>> orderedRecords; + for (const int index : indexOrder) + { + mIndex.at(mRecords[index]->get().mId) = static_cast(orderedRecords.size()); + orderedRecords.push_back(std::move(mRecords[index])); + } + mRecords = std::move(orderedRecords); + } + template bool Collection::reorderRowsImp(int baseIndex, const std::vector& newOrder) { diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index a6268c1a48..372b865009 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -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; - - std::vector erasedRecords; - - for (const ESM::RefId& id : topicInfos->second) + void removeDialogueInfos( + const ESM::RefId& dialogueId, InfoOrderByTopic& infoOrders, InfoCollection& infoCollection) { - const CSMWorld::Record& record = infoCollection.getRecord(id); + const auto topicInfoOrder = infoOrders.find(dialogueId); - if (record.mState == CSMWorld::RecordBase::State_ModifiedOnly) + if (topicInfoOrder == infoOrders.end()) + return; + + std::vector erasedRecords; + + for (const OrderedInfo& info : topicInfoOrder->second.getOrderedInfo()) { - erasedRecords.push_back(infoCollection.searchId(record.get().mId)); - continue; + const Record& 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); + deletedRecord->mState = RecordBase::State_Deleted; + infoCollection.setRecord(infoCollection.searchId(info.mId), std::move(deletedRecord)); } - auto deletedRecord = std::make_unique>(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); diff --git a/apps/opencs/model/world/data.hpp b/apps/opencs/model/world/data.hpp index 42c5309723..16d7a28f53 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -15,6 +15,7 @@ #include #include +#include #include #include #include @@ -135,8 +136,8 @@ namespace CSMWorld std::vector> 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 getIds(bool listDeleted = true) const; diff --git a/apps/opencs/model/world/infocollection.cpp b/apps/opencs/model/world/infocollection.cpp index 5e5a017243..a038b1046b 100644 --- a/apps/opencs/model/world/infocollection.cpp +++ b/apps/opencs/model/world/infocollection.cpp @@ -1,91 +1,141 @@ #include "infocollection.hpp" +#include #include +#include #include #include -#include -#include +#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>(); - record2->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; - (base ? record2->mBase : record2->mModified) = record; + auto record = std::make_unique>(); + 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>(getRecord(index)); + auto record = std::make_unique>(getRecord(index)); if (base) - record2->mBase = record; + record->mBase = value; else - record2->setModified(record); + record->setModified(value); - setRecord(index, std::move(record2)); - - 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) - { - removeRows(index, 1); - } - else - { - auto record = std::make_unique>(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); + if (base) + { + infoOrders.at(dialogue.mId).removeInfo(id); + removeRows(index, 1); + return; + } + + auto record = std::make_unique>(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 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 : 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>& rhs) { return lhs < rhs->get().mTopicId; }; + const auto it = std::upper_bound(getRecords().begin(), getRecords().end(), getInfoTopicId(id), lessByTopicId); + return static_cast(it - getRecords().begin()); +} + +bool CSMWorld::InfoCollection::reorderRows(int baseIndex, const std::vector& newOrder) +{ + const int lastIndex = baseIndex + static_cast(newOrder.size()) - 1; + + if (lastIndex >= getSize()) + return false; + + if (getRecord(baseIndex).get().mTopicId != getRecord(lastIndex).get().mTopicId) + return false; + + return reorderRowsImp(baseIndex, newOrder); +} diff --git a/apps/opencs/model/world/infocollection.hpp b/apps/opencs/model/world/infocollection.hpp index 696b032440..3853954a08 100644 --- a/apps/opencs/model/world/infocollection.hpp +++ b/apps/opencs/model/world/infocollection.hpp @@ -1,6 +1,7 @@ #ifndef CSM_WOLRD_INFOCOLLECTION_H #define CSM_WOLRD_INFOCOLLECTION_H +#include #include #include #include @@ -12,22 +13,47 @@ namespace ESM { struct Dialogue; class ESMReader; + + template + class InfoOrder; } namespace CSMWorld { - using InfosByTopic = std::unordered_map>; using InfosRecordPtrByTopic = std::unordered_map*>>; + 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; + using InfoOrderByTopic = std::map>; + class InfoCollection : public Collection { 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& newOrder) override; }; } diff --git a/apps/opencs/view/world/infocreator.cpp b/apps/opencs/view/world/infocreator.cpp index c8f92630f3..f98fe5be5e 100644 --- a/apps/opencs/view/world/infocreator.cpp +++ b/apps/opencs/view/world/infocreator.cpp @@ -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 diff --git a/apps/opencs_tests/model/world/testinfocollection.cpp b/apps/opencs_tests/model/world/testinfocollection.cpp index 9901266647..4aa277a22f 100644 --- a/apps/opencs_tests/model/world/testinfocollection.cpp +++ b/apps/opencs_tests/model/world/testinfocollection.cpp @@ -9,18 +9,69 @@ #include #include +#include #include -#include #include +#include namespace CSMWorld { + inline std::ostream& operator<<(std::ostream& stream, const Record* value) + { + return stream << "&Record{.mState=" << value->mState << ", .mId=" << value->get().mId << "}"; + } + namespace { using namespace ::testing; - std::unique_ptr saveDialogueWithInfos( - const ESM::Dialogue& dialogue, std::span infos) + struct DialInfoData + { + ESM::DialInfo mValue; + bool mDeleted = false; + + void save(ESM::ESMWriter& writer) const { mValue.save(writer, mDeleted); } + }; + + template + struct DialogueData + { + ESM::Dialogue mDialogue; + std::vector mInfos; + }; + + DialogueData generateDialogueWithInfos( + std::size_t infoCount, const ESM::RefId& dialogueId = ESM::RefId::stringRefId("dialogue")) + { + DialogueData 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 + std::unique_ptr saveDialogueWithInfos(const ESM::Dialogue& dialogue, Infos&& infos) { auto stream = std::make_unique(); @@ -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 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 infos, - bool base, InfoCollection& infoCollection, InfosByTopic& infosByTopic) + template + 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 + void saveAndLoadDialogueWithInfos( + const DialogueData& 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& 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& 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& 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& 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 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 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 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 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 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 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 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 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 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 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"))))); + } } } diff --git a/components/CMakeLists.txt b/components/CMakeLists.txt index 5304086ee7..1576acc015 100644 --- a/components/CMakeLists.txt +++ b/components/CMakeLists.txt @@ -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 diff --git a/components/esm3/infoorder.cpp b/components/esm3/infoorder.cpp new file mode 100644 index 0000000000..c77913e017 --- /dev/null +++ b/components/esm3/infoorder.cpp @@ -0,0 +1,87 @@ +#include "infoorder.hpp" + +namespace ESM +{ + const std::list* 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::iterator& position = it->second; + + const auto insertOrSplice = [&](std::list::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); + } +} diff --git a/components/esm3/infoorder.hpp b/components/esm3/infoorder.hpp new file mode 100644 index 0000000000..77b80e3d20 --- /dev/null +++ b/components/esm3/infoorder.hpp @@ -0,0 +1,114 @@ +#ifndef OPENMW_COMPONENTS_ESM3_INFOORDER_H +#define OPENMW_COMPONENTS_ESM3_INFOORDER_H + +#include "components/esm/refid.hpp" + +#include +#include +#include +#include +#include + +namespace ESM +{ + template + class InfoOrder + { + public: + const std::list& getOrderedInfo() const { return mOrderedInfo; } + + template + void insertInfo(V&& value, bool deleted) + { + static_assert(std::is_same_v, T>); + + auto it = mInfoPositions.find(value.mId); + + if (it != mInfoPositions.end() && it->second.mPosition->mPrev == value.mPrev) + { + *it->second.mPosition = std::forward(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::const_iterator before) { + if (item.mPosition == mOrderedInfo.end()) + item.mPosition = mOrderedInfo.insert(before, std::forward(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& info) + { + info = mOrderedInfo; + mInfoPositions.clear(); + } + + private: + struct Item + { + typename std::list::iterator mPosition; + bool mDeleted = false; + }; + + std::list mOrderedInfo; + std::unordered_map mInfoPositions; + }; +} + +#endif