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:
commit
2ff4a5a11a
@ -123,6 +123,7 @@ void CSMDoc::Loader::load()
|
||||
}
|
||||
else
|
||||
{
|
||||
document->getData().finishLoading();
|
||||
done = true;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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")))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -318,7 +318,7 @@ namespace MWWorld
|
||||
{
|
||||
if (dialogue)
|
||||
{
|
||||
dialogue->readInfo(esm, esm.getIndex() != 0);
|
||||
dialogue->readInfo(esm);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -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());
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
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
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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).
|
||||
|
Loading…
x
Reference in New Issue
Block a user