1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-02-04 03:40:14 +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
{
document->getData().finishLoading();
done = true;
}

View File

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

View File

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

View File

@ -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;
std::vector<int> erasedRecords;
for (const ESM::RefId& id : topicInfos->second)
void removeDialogueInfos(
const ESM::RefId& dialogueId, InfoOrderByTopic& infoOrders, InfoCollection& infoCollection)
{
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));
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);

View File

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

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)
{
if (!newOrder.empty())
if (mIdCollection->reorderRows(baseIndex, newOrder))
emit dataChanged(index(baseIndex, 0),
index(baseIndex + static_cast<int>(newOrder.size()) - 1, mIdCollection->getColumns() - 1));
if (newOrder.empty())
return;
if (!mIdCollection->reorderRows(baseIndex, newOrder))
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

View File

@ -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);
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<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);
if (base)
{
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
{
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);
}

View File

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

View File

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

View File

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

View File

@ -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")))));
}
}
}

View File

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

View File

@ -1070,7 +1070,7 @@ namespace MWWorld
// 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.
for (auto& [_, dial] : mStatic)
dial.clearDeletedInfos();
dial.setUp();
mShared.clear();
mShared.reserve(mStatic.size());

View File

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

View File

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

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();
}
void Dialogue::readInfo(ESMReader& esm, bool merge)
void Dialogue::readInfo(ESMReader& esm)
{
DialInfo info;
bool isDeleted = false;
info.load(esm, 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);
mInfoOrder.insertInfo(std::move(info), isDeleted);
}
void Dialogue::clearDeletedInfos()
void Dialogue::setUp()
{
LookupMap::const_iterator current = mLookup.begin();
LookupMap::const_iterator end = mLookup.end();
for (; current != end; ++current)
{
if (current->second.second)
{
mInfo.erase(current->second.first);
}
}
mLookup.clear();
mInfoOrder.removeDeleted();
mInfoOrder.extractOrderedInfo(mInfo);
}
}

View File

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