mirror of
https://github.com/OpenMW/openmw.git
synced 2025-12-06 20:34:35 +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
18 changed files with 893 additions and 172 deletions
|
|
@ -123,6 +123,7 @@ void CSMDoc::Loader::load()
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
document->getData().finishLoading();
|
||||||
done = true;
|
done = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -190,17 +190,16 @@ void CSMDoc::WriteDialogueCollectionStage::perform(int stage, Messages& messages
|
||||||
ESM::DialInfo info = record.get();
|
ESM::DialInfo info = record.get();
|
||||||
info.mId = record.get().mOriginalId;
|
info.mId = record.get().mOriginalId;
|
||||||
|
|
||||||
|
if (iter == infos.begin())
|
||||||
info.mPrev = ESM::RefId();
|
info.mPrev = ESM::RefId();
|
||||||
if (iter != infos.begin())
|
else
|
||||||
{
|
info.mPrev = (*std::prev(iter))->get().mOriginalId;
|
||||||
const auto prev = std::prev(iter);
|
|
||||||
info.mPrev = (*prev)->get().mOriginalId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto next = std::next(iter);
|
const auto next = std::next(iter);
|
||||||
|
|
||||||
|
if (next == infos.end())
|
||||||
info.mNext = ESM::RefId();
|
info.mNext = ESM::RefId();
|
||||||
if (next != infos.end())
|
else
|
||||||
info.mNext = (*next)->get().mOriginalId;
|
info.mNext = (*next)->get().mOriginalId;
|
||||||
|
|
||||||
writer.startRecord(info.sRecordId);
|
writer.startRecord(info.sRecordId);
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <QVariant>
|
#include <QVariant>
|
||||||
|
|
@ -17,6 +18,7 @@
|
||||||
|
|
||||||
#include "collectionbase.hpp"
|
#include "collectionbase.hpp"
|
||||||
#include "columnbase.hpp"
|
#include "columnbase.hpp"
|
||||||
|
#include "info.hpp"
|
||||||
#include "land.hpp"
|
#include "land.hpp"
|
||||||
#include "landtexture.hpp"
|
#include "landtexture.hpp"
|
||||||
#include "record.hpp"
|
#include "record.hpp"
|
||||||
|
|
@ -24,12 +26,29 @@
|
||||||
|
|
||||||
namespace CSMWorld
|
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>
|
template <typename T>
|
||||||
void setRecordId(const decltype(T::mId)& id, T& record)
|
void setRecordId(const decltype(T::mId)& id, T& record)
|
||||||
{
|
{
|
||||||
record.mId = id;
|
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>
|
template <typename T>
|
||||||
auto getRecordId(const T& record)
|
auto getRecordId(const T& record)
|
||||||
{
|
{
|
||||||
|
|
@ -85,6 +104,8 @@ namespace CSMWorld
|
||||||
protected:
|
protected:
|
||||||
const std::vector<std::unique_ptr<Record<ESXRecordT>>>& getRecords() const;
|
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);
|
bool reorderRowsImp(int baseIndex, const std::vector<int>& newOrder);
|
||||||
///< Reorder the rows [baseIndex, baseIndex+newOrder.size()) according to the indices
|
///< 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).
|
/// given in \a newOrder (baseIndex+newOrder[0] specifies the new index of row baseIndex).
|
||||||
|
|
@ -191,6 +212,20 @@ namespace CSMWorld
|
||||||
return mRecords;
|
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>
|
template <typename ESXRecordT>
|
||||||
bool Collection<ESXRecordT>::reorderRowsImp(int baseIndex, const std::vector<int>& newOrder)
|
bool Collection<ESXRecordT>::reorderRowsImp(int baseIndex, const std::vector<int>& newOrder)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
#include <components/esm/esmcommon.hpp>
|
#include <components/esm/esmcommon.hpp>
|
||||||
#include <components/esm3/cellref.hpp>
|
#include <components/esm3/cellref.hpp>
|
||||||
#include <components/esm3/esmreader.hpp>
|
#include <components/esm3/esmreader.hpp>
|
||||||
|
#include <components/esm3/infoorder.hpp>
|
||||||
#include <components/esm3/loadcell.hpp>
|
#include <components/esm3/loadcell.hpp>
|
||||||
#include <components/esm3/loaddoor.hpp>
|
#include <components/esm3/loaddoor.hpp>
|
||||||
#include <components/esm3/loadglob.hpp>
|
#include <components/esm3/loadglob.hpp>
|
||||||
|
|
@ -55,31 +56,33 @@
|
||||||
#include "resourcesmanager.hpp"
|
#include "resourcesmanager.hpp"
|
||||||
#include "resourcetable.hpp"
|
#include "resourcetable.hpp"
|
||||||
|
|
||||||
namespace
|
namespace CSMWorld
|
||||||
{
|
{
|
||||||
void removeDialogueInfos(const ESM::RefId& dialogueId, const CSMWorld::InfosByTopic& infosByTopic,
|
namespace
|
||||||
CSMWorld::InfoCollection& infoCollection)
|
|
||||||
{
|
{
|
||||||
const auto topicInfos = infosByTopic.find(dialogueId);
|
void removeDialogueInfos(
|
||||||
|
const ESM::RefId& dialogueId, InfoOrderByTopic& infoOrders, InfoCollection& infoCollection)
|
||||||
|
{
|
||||||
|
const auto topicInfoOrder = infoOrders.find(dialogueId);
|
||||||
|
|
||||||
if (topicInfos == infosByTopic.end())
|
if (topicInfoOrder == infoOrders.end())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
std::vector<int> erasedRecords;
|
std::vector<int> erasedRecords;
|
||||||
|
|
||||||
for (const ESM::RefId& id : topicInfos->second)
|
for (const OrderedInfo& info : topicInfoOrder->second.getOrderedInfo())
|
||||||
{
|
{
|
||||||
const CSMWorld::Record<CSMWorld::Info>& record = infoCollection.getRecord(id);
|
const Record<Info>& record = infoCollection.getRecord(info.mId);
|
||||||
|
|
||||||
if (record.mState == CSMWorld::RecordBase::State_ModifiedOnly)
|
if (record.mState == RecordBase::State_ModifiedOnly)
|
||||||
{
|
{
|
||||||
erasedRecords.push_back(infoCollection.searchId(record.get().mId));
|
erasedRecords.push_back(infoCollection.searchId(info.mId));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto deletedRecord = std::make_unique<CSMWorld::Record<CSMWorld::Info>>(record);
|
auto deletedRecord = std::make_unique<Record<Info>>(record);
|
||||||
deletedRecord->mState = CSMWorld::RecordBase::State_Deleted;
|
deletedRecord->mState = RecordBase::State_Deleted;
|
||||||
infoCollection.setRecord(infoCollection.searchId(record.get().mId), std::move(deletedRecord));
|
infoCollection.setRecord(infoCollection.searchId(info.mId), std::move(deletedRecord));
|
||||||
}
|
}
|
||||||
|
|
||||||
while (!erasedRecords.empty())
|
while (!erasedRecords.empty())
|
||||||
|
|
@ -87,6 +90,9 @@ namespace
|
||||||
infoCollection.removeRows(erasedRecords.back(), 1);
|
infoCollection.removeRows(erasedRecords.back(), 1);
|
||||||
erasedRecords.pop_back();
|
erasedRecords.pop_back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
infoOrders.erase(topicInfoOrder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1289,11 +1295,11 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages)
|
||||||
|
|
||||||
if (mJournals.tryDelete(record.mId))
|
if (mJournals.tryDelete(record.mId))
|
||||||
{
|
{
|
||||||
removeDialogueInfos(record.mId, mJournalInfosByTopic, mJournalInfos);
|
removeDialogueInfos(record.mId, mJournalInfoOrder, mJournalInfos);
|
||||||
}
|
}
|
||||||
else if (mTopics.tryDelete(record.mId))
|
else if (mTopics.tryDelete(record.mId))
|
||||||
{
|
{
|
||||||
removeDialogueInfos(record.mId, mTopicInfosByTopic, mTopicInfos);
|
removeDialogueInfos(record.mId, mTopicInfoOrder, mTopicInfos);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -1331,9 +1337,9 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mDialogue->mType == ESM::Dialogue::Journal)
|
if (mDialogue->mType == ESM::Dialogue::Journal)
|
||||||
mJournalInfos.load(*mReader, mBase, *mDialogue, mJournalInfosByTopic);
|
mJournalInfos.load(*mReader, mBase, *mDialogue, mJournalInfoOrder);
|
||||||
else
|
else
|
||||||
mTopicInfos.load(*mReader, mBase, *mDialogue, mTopicInfosByTopic);
|
mTopicInfos.load(*mReader, mBase, *mDialogue, mTopicInfoOrder);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -1376,6 +1382,12 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CSMWorld::Data::finishLoading()
|
||||||
|
{
|
||||||
|
mTopicInfos.sort(mTopicInfoOrder);
|
||||||
|
mJournalInfos.sort(mJournalInfoOrder);
|
||||||
|
}
|
||||||
|
|
||||||
bool CSMWorld::Data::hasId(const std::string& id) const
|
bool CSMWorld::Data::hasId(const std::string& id) const
|
||||||
{
|
{
|
||||||
const ESM::RefId refId = ESM::RefId::stringRefId(id);
|
const ESM::RefId refId = ESM::RefId::stringRefId(id);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
#include <components/esm3/debugprofile.hpp>
|
#include <components/esm3/debugprofile.hpp>
|
||||||
#include <components/esm3/filter.hpp>
|
#include <components/esm3/filter.hpp>
|
||||||
|
#include <components/esm3/infoorder.hpp>
|
||||||
#include <components/esm3/loadbody.hpp>
|
#include <components/esm3/loadbody.hpp>
|
||||||
#include <components/esm3/loadbsgn.hpp>
|
#include <components/esm3/loadbsgn.hpp>
|
||||||
#include <components/esm3/loadclas.hpp>
|
#include <components/esm3/loadclas.hpp>
|
||||||
|
|
@ -135,8 +136,8 @@ namespace CSMWorld
|
||||||
|
|
||||||
std::vector<std::shared_ptr<ESM::ESMReader>> mReaders;
|
std::vector<std::shared_ptr<ESM::ESMReader>> mReaders;
|
||||||
|
|
||||||
CSMWorld::InfosByTopic mJournalInfosByTopic;
|
InfoOrderByTopic mJournalInfoOrder;
|
||||||
CSMWorld::InfosByTopic mTopicInfosByTopic;
|
InfoOrderByTopic mTopicInfoOrder;
|
||||||
|
|
||||||
// not implemented
|
// not implemented
|
||||||
Data(const Data&);
|
Data(const Data&);
|
||||||
|
|
@ -307,6 +308,8 @@ namespace CSMWorld
|
||||||
bool continueLoading(CSMDoc::Messages& messages);
|
bool continueLoading(CSMDoc::Messages& messages);
|
||||||
///< \return Finished?
|
///< \return Finished?
|
||||||
|
|
||||||
|
void finishLoading();
|
||||||
|
|
||||||
bool hasId(const std::string& id) const;
|
bool hasId(const std::string& id) const;
|
||||||
|
|
||||||
std::vector<ESM::RefId> getIds(bool listDeleted = true) 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)
|
void CSMWorld::IdTable::reorderRows(int baseIndex, const std::vector<int>& newOrder)
|
||||||
{
|
{
|
||||||
if (!newOrder.empty())
|
if (newOrder.empty())
|
||||||
if (mIdCollection->reorderRows(baseIndex, newOrder))
|
return;
|
||||||
emit dataChanged(index(baseIndex, 0),
|
if (!mIdCollection->reorderRows(baseIndex, newOrder))
|
||||||
index(baseIndex + static_cast<int>(newOrder.size()) - 1, mIdCollection->getColumns() - 1));
|
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
|
std::pair<CSMWorld::UniversalId, std::string> CSMWorld::IdTable::view(int row) const
|
||||||
|
|
|
||||||
|
|
@ -1,91 +1,141 @@
|
||||||
#include "infocollection.hpp"
|
#include "infocollection.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include <components/debug/debuglog.hpp>
|
#include "components/debug/debuglog.hpp"
|
||||||
#include <components/esm3/loaddial.hpp>
|
#include "components/esm3/infoorder.hpp"
|
||||||
|
#include "components/esm3/loaddial.hpp"
|
||||||
|
#include "components/esm3/loadinfo.hpp"
|
||||||
|
|
||||||
#include "collection.hpp"
|
#include "collection.hpp"
|
||||||
#include "info.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)
|
if (index == -1)
|
||||||
{
|
{
|
||||||
// new record
|
// new record
|
||||||
auto record2 = std::make_unique<Record<Info>>();
|
auto record = std::make_unique<Record<Info>>();
|
||||||
record2->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly;
|
record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly;
|
||||||
(base ? record2->mBase : record2->mModified) = record;
|
(base ? record->mBase : record->mModified) = value;
|
||||||
|
|
||||||
appendRecord(std::move(record2));
|
insertRecord(std::move(record), getSize());
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// old record
|
// old record
|
||||||
auto record2 = std::make_unique<Record<Info>>(getRecord(index));
|
auto record = std::make_unique<Record<Info>>(getRecord(index));
|
||||||
|
|
||||||
if (base)
|
if (base)
|
||||||
record2->mBase = record;
|
record->mBase = value;
|
||||||
else
|
else
|
||||||
record2->setModified(record);
|
record->setModified(value);
|
||||||
|
|
||||||
setRecord(index, std::move(record2));
|
setRecord(index, std::move(record));
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSMWorld::InfoCollection::load(
|
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;
|
Info info;
|
||||||
bool isDeleted = false;
|
bool isDeleted = false;
|
||||||
|
|
||||||
info.load(reader, isDeleted);
|
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)
|
if (isDeleted)
|
||||||
{
|
{
|
||||||
int index = searchId(id);
|
const int index = searchId(id);
|
||||||
|
|
||||||
if (index == -1)
|
if (index == -1)
|
||||||
{
|
{
|
||||||
// deleting a record that does not exist
|
Log(Debug::Warning) << "Trying to delete absent info \"" << info.mId << "\" from topic \"" << dialogue.mId
|
||||||
// ignore it for now
|
<< "\"";
|
||||||
/// \todo report the problem to the user
|
return;
|
||||||
}
|
}
|
||||||
else if (base)
|
|
||||||
|
if (base)
|
||||||
{
|
{
|
||||||
|
infoOrders.at(dialogue.mId).removeInfo(id);
|
||||||
removeRows(index, 1);
|
removeRows(index, 1);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
auto record = std::make_unique<Record<Info>>(getRecord(index));
|
auto record = std::make_unique<Record<Info>>(getRecord(index));
|
||||||
record->mState = RecordBase::State_Deleted;
|
record->mState = RecordBase::State_Deleted;
|
||||||
setRecord(index, std::move(record));
|
setRecord(index, std::move(record));
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
info.mTopicId = dialogue.mId;
|
info.mTopicId = dialogue.mId;
|
||||||
info.mOriginalId = info.mId;
|
info.mOriginalId = info.mId;
|
||||||
info.mId = id;
|
info.mId = id;
|
||||||
|
|
||||||
if (load(info, base))
|
load(info, base);
|
||||||
infosByTopic[dialogue.mId].push_back(info.mId);
|
|
||||||
}
|
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
|
CSMWorld::InfosRecordPtrByTopic CSMWorld::InfoCollection::getInfosByTopic() const
|
||||||
{
|
{
|
||||||
InfosRecordPtrByTopic result;
|
InfosRecordPtrByTopic result;
|
||||||
for (const std::unique_ptr<Record<Info>>& record : getRecords())
|
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;
|
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
|
#ifndef CSM_WOLRD_INFOCOLLECTION_H
|
||||||
#define CSM_WOLRD_INFOCOLLECTION_H
|
#define CSM_WOLRD_INFOCOLLECTION_H
|
||||||
|
|
||||||
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
@ -12,22 +13,47 @@ namespace ESM
|
||||||
{
|
{
|
||||||
struct Dialogue;
|
struct Dialogue;
|
||||||
class ESMReader;
|
class ESMReader;
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
class InfoOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace CSMWorld
|
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>*>>;
|
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>
|
class InfoCollection : public Collection<Info>
|
||||||
{
|
{
|
||||||
private:
|
private:
|
||||||
bool load(const Info& record, bool base);
|
void load(const Info& value, bool base);
|
||||||
|
|
||||||
public:
|
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;
|
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 <QVBoxLayout>
|
||||||
|
|
||||||
#include <apps/opencs/model/world/data.hpp>
|
#include <apps/opencs/model/world/data.hpp>
|
||||||
|
|
||||||
|
#include <components/debug/debuglog.hpp>
|
||||||
#include <components/files/qtconversion.hpp>
|
#include <components/files/qtconversion.hpp>
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
|
@ -70,6 +72,7 @@ CSVDoc::LoadingDocument::LoadingDocument(CSMDoc::Document* document)
|
||||||
// error message
|
// error message
|
||||||
mError = new QLabel(this);
|
mError = new QLabel(this);
|
||||||
mError->setWordWrap(true);
|
mError->setWordWrap(true);
|
||||||
|
mError->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||||
|
|
||||||
mLayout->addWidget(mError);
|
mLayout->addWidget(mError);
|
||||||
|
|
||||||
|
|
@ -120,6 +123,7 @@ void CSVDoc::LoadingDocument::abort(const std::string& error)
|
||||||
{
|
{
|
||||||
mAborted = true;
|
mAborted = true;
|
||||||
mError->setText(QString::fromUtf8(("<font color=red>Loading failed: " + error + "</font>").c_str()));
|
mError->setText(QString::fromUtf8(("<font color=red>Loading failed: " + error + "</font>").c_str()));
|
||||||
|
Log(Debug::Error) << "Loading failed: " << error;
|
||||||
mButtons->setStandardButtons(QDialogButtonBox::Close);
|
mButtons->setStandardButtons(QDialogButtonBox::Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ class QUndoStack;
|
||||||
|
|
||||||
std::string CSVWorld::InfoCreator::getId() const
|
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();
|
std::string unique = QUuid::createUuid().toByteArray().data();
|
||||||
|
|
||||||
|
|
@ -35,7 +35,7 @@ std::string CSVWorld::InfoCreator::getId() const
|
||||||
|
|
||||||
unique = unique.substr(1, unique.size() - 2);
|
unique = unique.substr(1, unique.size() - 2);
|
||||||
|
|
||||||
return id + '#' + unique;
|
return topic + '#' + unique;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CSVWorld::InfoCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const
|
void CSVWorld::InfoCreator::configureCreateCommand(CSMWorld::CreateCommand& command) const
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,69 @@
|
||||||
#include <gmock/gmock.h>
|
#include <gmock/gmock.h>
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <span>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace CSMWorld
|
namespace CSMWorld
|
||||||
{
|
{
|
||||||
|
inline std::ostream& operator<<(std::ostream& stream, const Record<Info>* value)
|
||||||
|
{
|
||||||
|
return stream << "&Record{.mState=" << value->mState << ", .mId=" << value->get().mId << "}";
|
||||||
|
}
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
using namespace ::testing;
|
using namespace ::testing;
|
||||||
|
|
||||||
std::unique_ptr<std::stringstream> saveDialogueWithInfos(
|
struct DialInfoData
|
||||||
const ESM::Dialogue& dialogue, std::span<const ESM::DialInfo> infos)
|
{
|
||||||
|
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>();
|
auto stream = std::make_unique<std::stringstream>();
|
||||||
|
|
||||||
|
|
@ -32,7 +83,7 @@ namespace CSMWorld
|
||||||
dialogue.save(writer);
|
dialogue.save(writer);
|
||||||
writer.endRecord(ESM::REC_DIAL);
|
writer.endRecord(ESM::REC_DIAL);
|
||||||
|
|
||||||
for (const ESM::DialInfo& info : infos)
|
for (const auto& info : infos)
|
||||||
{
|
{
|
||||||
writer.startRecord(ESM::REC_INFO);
|
writer.startRecord(ESM::REC_INFO);
|
||||||
info.save(writer);
|
info.save(writer);
|
||||||
|
|
@ -43,7 +94,7 @@ namespace CSMWorld
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadDialogueWithInfos(bool base, std::unique_ptr<std::stringstream> stream, InfoCollection& infoCollection,
|
void loadDialogueWithInfos(bool base, std::unique_ptr<std::stringstream> stream, InfoCollection& infoCollection,
|
||||||
InfosByTopic& infosByTopic)
|
InfoOrderByTopic& infoOrder)
|
||||||
{
|
{
|
||||||
ESM::ESMReader reader;
|
ESM::ESMReader reader;
|
||||||
reader.open(std::move(stream), "test");
|
reader.open(std::move(stream), "test");
|
||||||
|
|
@ -59,64 +110,535 @@ namespace CSMWorld
|
||||||
{
|
{
|
||||||
ASSERT_EQ(reader.getRecName().toInt(), ESM::REC_INFO);
|
ASSERT_EQ(reader.getRecName().toInt(), ESM::REC_INFO);
|
||||||
reader.getRecHeader();
|
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,
|
template <class Infos>
|
||||||
bool base, InfoCollection& infoCollection, InfosByTopic& infosByTopic)
|
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)
|
TEST(CSMWorldInfoCollectionTest, loadShouldAddRecord)
|
||||||
{
|
{
|
||||||
ESM::Dialogue dialogue;
|
ESM::Dialogue dialogue;
|
||||||
dialogue.blank();
|
dialogue.blank();
|
||||||
dialogue.mId = ESM::RefId::stringRefId("dialogue1");
|
dialogue.mId = ESM::RefId::stringRefId("dialogue");
|
||||||
|
|
||||||
ESM::DialInfo info;
|
ESM::DialInfo info;
|
||||||
info.blank();
|
info.blank();
|
||||||
info.mId = ESM::RefId::stringRefId("info1");
|
info.mId = ESM::RefId::stringRefId("info0");
|
||||||
|
|
||||||
const bool base = true;
|
const bool base = true;
|
||||||
InfosByTopic infosByTopic;
|
InfoOrderByTopic infoOrder;
|
||||||
InfoCollection collection;
|
InfoCollection collection;
|
||||||
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infosByTopic);
|
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);
|
||||||
|
|
||||||
EXPECT_EQ(collection.getSize(), 1);
|
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);
|
const Record<Info>& record = collection.getRecord(0);
|
||||||
ASSERT_EQ(record.mState, RecordBase::State_BaseOnly);
|
ASSERT_EQ(record.mState, RecordBase::State_BaseOnly);
|
||||||
EXPECT_EQ(record.mBase.mTopicId, dialogue.mId);
|
EXPECT_EQ(record.mBase.mTopicId, dialogue.mId);
|
||||||
EXPECT_EQ(record.mBase.mOriginalId, info.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)
|
TEST(CSMWorldInfoCollectionTest, loadShouldUpdateRecord)
|
||||||
{
|
{
|
||||||
ESM::Dialogue dialogue;
|
ESM::Dialogue dialogue;
|
||||||
dialogue.blank();
|
dialogue.blank();
|
||||||
dialogue.mId = ESM::RefId::stringRefId("dialogue1");
|
dialogue.mId = ESM::RefId::stringRefId("dialogue");
|
||||||
|
|
||||||
ESM::DialInfo info;
|
ESM::DialInfo info;
|
||||||
info.blank();
|
info.blank();
|
||||||
info.mId = ESM::RefId::stringRefId("info1");
|
info.mId = ESM::RefId::stringRefId("info0");
|
||||||
|
|
||||||
const bool base = true;
|
const bool base = true;
|
||||||
InfosByTopic infosByTopic;
|
InfoOrderByTopic infoOrder;
|
||||||
InfoCollection collection;
|
InfoCollection collection;
|
||||||
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infosByTopic);
|
saveAndLoadDialogueWithInfos(dialogue, std::array{ info }, base, collection, infoOrder);
|
||||||
|
|
||||||
ESM::DialInfo updatedInfo = info;
|
ESM::DialInfo updatedInfo = info;
|
||||||
updatedInfo.mActor = ESM::RefId::stringRefId("newActor");
|
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);
|
const Record<Info>& record = collection.getRecord(0);
|
||||||
ASSERT_EQ(record.mState, RecordBase::State_BaseOnly);
|
ASSERT_EQ(record.mState, RecordBase::State_BaseOnly);
|
||||||
EXPECT_EQ(record.mBase.mActor, ESM::RefId::stringRefId("newActor"));
|
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)
|
if (dialogue)
|
||||||
{
|
{
|
||||||
dialogue->readInfo(esm, esm.getIndex() != 0);
|
dialogue->readInfo(esm);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1070,7 +1070,7 @@ namespace MWWorld
|
||||||
// DialInfos marked as deleted are kept during the loading phase, so that the linked list
|
// 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.
|
// structure is kept intact for inserting further INFOs. Delete them now that loading is done.
|
||||||
for (auto& [_, dial] : mStatic)
|
for (auto& [_, dial] : mStatic)
|
||||||
dial.clearDeletedInfos();
|
dial.setUp();
|
||||||
|
|
||||||
mShared.clear();
|
mShared.clear();
|
||||||
mShared.reserve(mStatic.size());
|
mShared.reserve(mStatic.size());
|
||||||
|
|
|
||||||
|
|
@ -671,7 +671,7 @@ namespace
|
||||||
EXPECT_THAT(dialogue->mInfo, ElementsAre(HasIdEqualTo("info0"), HasIdEqualTo("info1"), HasIdEqualTo("info2")));
|
EXPECT_THAT(dialogue->mInfo, ElementsAre(HasIdEqualTo("info0"), HasIdEqualTo("info1"), HasIdEqualTo("info2")));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(MWWorldStoreTest, shouldLoadDialogueWithInfosAsIsWhenReversed)
|
TEST(MWWorldStoreTest, shouldLoadDialogueWithInfosAndOrderWhenReversed)
|
||||||
{
|
{
|
||||||
DialogueData data = generateDialogueWithInfos(3);
|
DialogueData data = generateDialogueWithInfos(3);
|
||||||
|
|
||||||
|
|
@ -683,7 +683,7 @@ namespace
|
||||||
|
|
||||||
const ESM::Dialogue* dialogue = esmStore.get<ESM::Dialogue>().search(ESM::RefId::stringRefId("dialogue"));
|
const ESM::Dialogue* dialogue = esmStore.get<ESM::Dialogue>().search(ESM::RefId::stringRefId("dialogue"));
|
||||||
ASSERT_NE(dialogue, nullptr);
|
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)
|
TEST(MWWorldStoreTest, shouldLoadDialogueWithInfosInsertingNewRecordBasedOnPrev)
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ add_component_dir (esm3
|
||||||
inventorystate containerstate npcstate creaturestate dialoguestate statstate npcstats creaturestats
|
inventorystate containerstate npcstate creaturestate dialoguestate statstate npcstats creaturestats
|
||||||
weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate debugprofile
|
weatherstate quickkeys fogstate spellstate activespells creaturelevliststate doorstate projectilestate debugprofile
|
||||||
aisequence magiceffects custommarkerstate stolenitems transport animationstate controlsstate mappings readerscache
|
aisequence magiceffects custommarkerstate stolenitems transport animationstate controlsstate mappings readerscache
|
||||||
|
infoorder
|
||||||
)
|
)
|
||||||
|
|
||||||
add_component_dir (esm3terrain
|
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();
|
mInfo.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Dialogue::readInfo(ESMReader& esm, bool merge)
|
void Dialogue::readInfo(ESMReader& esm)
|
||||||
{
|
{
|
||||||
DialInfo info;
|
DialInfo info;
|
||||||
bool isDeleted = false;
|
bool isDeleted = false;
|
||||||
info.load(esm, isDeleted);
|
info.load(esm, isDeleted);
|
||||||
|
mInfoOrder.insertInfo(std::move(info), 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);
|
void Dialogue::setUp()
|
||||||
|
|
||||||
if (lookup != mLookup.end())
|
|
||||||
{
|
{
|
||||||
auto it = lookup->second.first;
|
mInfoOrder.removeDeleted();
|
||||||
if (it->mPrev == info.mPrev)
|
mInfoOrder.extractOrderedInfo(mInfo);
|
||||||
{
|
|
||||||
*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);
|
|
||||||
}
|
|
||||||
|
|
||||||
void Dialogue::clearDeletedInfos()
|
|
||||||
{
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
#include "components/esm/defs.hpp"
|
#include "components/esm/defs.hpp"
|
||||||
#include "components/esm/refid.hpp"
|
#include "components/esm/refid.hpp"
|
||||||
|
#include "components/esm3/infoorder.hpp"
|
||||||
|
|
||||||
#include "loadinfo.hpp"
|
#include "loadinfo.hpp"
|
||||||
|
|
||||||
|
|
@ -23,6 +24,8 @@ namespace ESM
|
||||||
|
|
||||||
struct Dialogue
|
struct Dialogue
|
||||||
{
|
{
|
||||||
|
using InfoContainer = std::list<DialInfo>;
|
||||||
|
|
||||||
constexpr static RecNameInts sRecordId = REC_DIAL;
|
constexpr static RecNameInts sRecordId = REC_DIAL;
|
||||||
/// Return a string descriptor for this record type. Currently used for debugging / error logs only.
|
/// Return a string descriptor for this record type. Currently used for debugging / error logs only.
|
||||||
static std::string_view getRecordType() { return "Dialogue"; }
|
static std::string_view getRecordType() { return "Dialogue"; }
|
||||||
|
|
@ -39,17 +42,12 @@ namespace ESM
|
||||||
|
|
||||||
RefId mId;
|
RefId mId;
|
||||||
signed char mType;
|
signed char mType;
|
||||||
|
InfoContainer mInfo;
|
||||||
typedef std::list<DialInfo> InfoContainer;
|
InfoOrder<DialInfo> mInfoOrder;
|
||||||
|
|
||||||
// Parameters: Info ID, (Info iterator, Deleted flag)
|
// Parameters: Info ID, (Info iterator, Deleted flag)
|
||||||
typedef std::map<ESM::RefId, std::pair<InfoContainer::iterator, bool>> LookupMap;
|
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);
|
void load(ESMReader& esm, bool& isDeleted);
|
||||||
///< Loads all sub-records of Dialogue record
|
///< Loads all sub-records of Dialogue record
|
||||||
void loadId(ESMReader& esm);
|
void loadId(ESMReader& esm);
|
||||||
|
|
@ -60,11 +58,10 @@ namespace ESM
|
||||||
void save(ESMWriter& esm, bool isDeleted = false) const;
|
void save(ESMWriter& esm, bool isDeleted = false) const;
|
||||||
|
|
||||||
/// Remove all INFOs that are deleted
|
/// Remove all INFOs that are deleted
|
||||||
void clearDeletedInfos();
|
void setUp();
|
||||||
|
|
||||||
/// Read the next info record
|
/// 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);
|
||||||
void readInfo(ESMReader& esm, bool merge);
|
|
||||||
|
|
||||||
void blank();
|
void blank();
|
||||||
///< Set record to default state (does not touch the ID and does not change the type).
|
///< Set record to default state (does not touch the ID and does not change the type).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue