mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-02-06 00:40:04 +00:00
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.
This commit is contained in:
parent
899c302b14
commit
e892c62b10
@ -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;
|
||||
|
||||
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);
|
||||
|
@ -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);
|
||||
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);
|
||||
}
|
||||
|
@ -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
|
||||
|
87
components/esm3/infoorder.cpp
Normal file
87
components/esm3/infoorder.cpp
Normal file
@ -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);
|
||||
}
|
||||
}
|
114
components/esm3/infoorder.hpp
Normal file
114
components/esm3/infoorder.hpp
Normal 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
|
Loading…
x
Reference in New Issue
Block a user