1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-02-06 16:45:36 +00:00

Simplify InfoCollection

There was additional logic to create topic infos index by topic id to make
getTopicInfos and removeDialogueInfos functions faster. In practice it makes
loading slower.

Move infos index by topic to CSMWorld::Data and use only on loading.
This commit is contained in:
elsid 2023-02-15 20:57:01 +01:00
parent de24cdc12c
commit b6a2fd8fc1
No known key found for this signature in database
GPG key ID: 4DE04C198CBA7625
7 changed files with 90 additions and 437 deletions

View file

@ -145,11 +145,11 @@ void CSMDoc::WriteDialogueCollectionStage::perform(int stage, Messages& messages
// Test, if we need to save anything associated info records. // Test, if we need to save anything associated info records.
bool infoModified = false; bool infoModified = false;
CSMWorld::InfoCollection::Range range = mInfos.getTopicRange(topic.get().mId.getRefIdString()); const auto infos = mInfos.getTopicInfos(topic.get().mId);
for (CSMWorld::InfoCollection::RecordConstIterator iter(range.first); iter != range.second; ++iter) for (const auto& record : infos)
{ {
if ((*iter)->isModified() || (*iter)->mState == CSMWorld::RecordBase::State_Deleted) if (record->isModified() || record->mState == CSMWorld::RecordBase::State_Deleted)
{ {
infoModified = true; infoModified = true;
break; break;
@ -173,35 +173,30 @@ void CSMDoc::WriteDialogueCollectionStage::perform(int stage, Messages& messages
} }
// write modified selected info records // write modified selected info records
for (CSMWorld::InfoCollection::RecordConstIterator iter(range.first); iter != range.second; ++iter) for (auto iter = infos.begin(); iter != infos.end(); ++iter)
{ {
if ((*iter)->isModified() || (*iter)->mState == CSMWorld::RecordBase::State_Deleted) const CSMWorld::Record<CSMWorld::Info>& record = **iter;
if (record.isModified() || record.mState == CSMWorld::RecordBase::State_Deleted)
{ {
ESM::DialInfo info = (*iter)->get(); ESM::DialInfo info = record.get();
std::string_view infoIdString = info.mId.getRefIdString(); info.mId = record.get().mOriginalId;
info.mId = ESM::RefId::stringRefId(infoIdString.substr(infoIdString.find_last_of('#') + 1));
info.mPrev = ESM::RefId::sEmpty; info.mPrev = ESM::RefId::sEmpty;
if (iter != range.first) if (iter != infos.begin())
{ {
CSMWorld::InfoCollection::RecordConstIterator prev = iter; const auto prev = std::prev(iter);
--prev; info.mPrev = (*prev)->get().mOriginalId;
std::string_view prevIdString = (*prev)->get().mId.getRefIdString();
info.mPrev = ESM::RefId::stringRefId(prevIdString.substr(prevIdString.find_last_of('#') + 1));
} }
CSMWorld::InfoCollection::RecordConstIterator next = iter; const auto next = std::next(iter);
++next;
info.mNext = ESM::RefId::sEmpty; info.mNext = ESM::RefId::sEmpty;
if (next != range.second) if (next != infos.end())
{ info.mNext = (*next)->get().mOriginalId;
std::string_view nextIdString = (*next)->get().mId.getRefIdString();
info.mNext = ESM::RefId::stringRefId(nextIdString.substr(nextIdString.find_last_of('#') + 1));
}
writer.startRecord(info.sRecordId); writer.startRecord(info.sRecordId);
info.save(writer, (*iter)->mState == CSMWorld::RecordBase::State_Deleted); info.save(writer, record.mState == CSMWorld::RecordBase::State_Deleted);
writer.endRecord(info.sRecordId); writer.endRecord(info.sRecordId);
} }
} }

View file

@ -50,16 +50,12 @@ void CSMTools::JournalCheckStage::perform(int stage, CSMDoc::Messages& messages)
int totalInfoCount = 0; int totalInfoCount = 0;
std::set<int> questIndices; std::set<int> questIndices;
CSMWorld::InfoCollection::Range range = mJournalInfos.getTopicRange(journal.mId.getRefIdString()); for (const CSMWorld::Record<CSMWorld::Info>* record : mJournalInfos.getTopicInfos(journal.mId))
for (CSMWorld::InfoCollection::RecordConstIterator it = range.first; it != range.second; ++it)
{ {
const CSMWorld::Record<CSMWorld::Info> infoRecord = (*it->get()); if (record->isDeleted())
if (infoRecord.isDeleted())
continue; continue;
const CSMWorld::Info& journalInfo = infoRecord.get(); const CSMWorld::Info& journalInfo = record->get();
totalInfoCount += 1; totalInfoCount += 1;
@ -69,7 +65,7 @@ void CSMTools::JournalCheckStage::perform(int stage, CSMDoc::Messages& messages)
} }
// Skip "Base" records (setting!) // Skip "Base" records (setting!)
if (mIgnoreBaseRecords && infoRecord.mState == CSMWorld::RecordBase::State_BaseOnly) if (mIgnoreBaseRecords && record->mState == CSMWorld::RecordBase::State_BaseOnly)
continue; continue;
if (journalInfo.mResponse.empty()) if (journalInfo.mResponse.empty())

View file

@ -55,6 +55,41 @@
#include "resourcesmanager.hpp" #include "resourcesmanager.hpp"
#include "resourcetable.hpp" #include "resourcetable.hpp"
namespace
{
void removeDialogueInfos(const ESM::RefId& dialogueId, const CSMWorld::InfosByTopic& infosByTopic,
CSMWorld::InfoCollection& infoCollection)
{
const auto topicInfos = infosByTopic.find(dialogueId);
if (topicInfos == infosByTopic.end())
return;
std::vector<int> erasedRecords;
for (const ESM::RefId& id : topicInfos->second)
{
const CSMWorld::Record<CSMWorld::Info>& record = infoCollection.getRecord(id);
if (record.mState == CSMWorld::RecordBase::State_ModifiedOnly)
{
erasedRecords.push_back(infoCollection.searchId(record.get().mId));
continue;
}
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();
}
}
}
void CSMWorld::Data::addModel(QAbstractItemModel* model, UniversalId::Type type, bool update) void CSMWorld::Data::addModel(QAbstractItemModel* model, UniversalId::Type type, bool update)
{ {
mModels.push_back(model); mModels.push_back(model);
@ -1254,11 +1289,11 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages)
if (mJournals.tryDelete(recordIdString)) if (mJournals.tryDelete(recordIdString))
{ {
mJournalInfos.removeDialogueInfos(recordIdString); removeDialogueInfos(record.mId, mJournalInfosByTopic, mJournalInfos);
} }
else if (mTopics.tryDelete(recordIdString)) else if (mTopics.tryDelete(recordIdString))
{ {
mTopicInfos.removeDialogueInfos(recordIdString); removeDialogueInfos(record.mId, mTopicInfosByTopic, mTopicInfos);
} }
else else
{ {
@ -1296,9 +1331,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); mJournalInfos.load(*mReader, mBase, *mDialogue, mJournalInfosByTopic);
else else
mTopicInfos.load(*mReader, mBase, *mDialogue); mTopicInfos.load(*mReader, mBase, *mDialogue, mTopicInfosByTopic);
break; break;
} }

View file

@ -135,6 +135,9 @@ namespace CSMWorld
std::vector<std::shared_ptr<ESM::ESMReader>> mReaders; std::vector<std::shared_ptr<ESM::ESMReader>> mReaders;
CSMWorld::InfosByTopic mJournalInfosByTopic;
CSMWorld::InfosByTopic mTopicInfosByTopic;
// not implemented // not implemented
Data(const Data&); Data(const Data&);
Data& operator=(const Data&); Data& operator=(const Data&);

View file

@ -8,6 +8,7 @@ namespace CSMWorld
struct Info : public ESM::DialInfo struct Info : public ESM::DialInfo
{ {
ESM::RefId mTopicId; ESM::RefId mTopicId;
ESM::RefId mOriginalId;
}; };
} }

View file

@ -1,80 +1,16 @@
#include "infocollection.hpp" #include "infocollection.hpp"
#include <algorithm> #include <memory>
#include <cassert> #include <string>
#include <iterator> #include <utility>
#include <limits.h>
#include <stdexcept>
#include <apps/opencs/model/world/collection.hpp>
#include <apps/opencs/model/world/info.hpp>
#include <components/misc/strings/lower.hpp>
#include <components/debug/debuglog.hpp>
#include <components/esm3/loaddial.hpp> #include <components/esm3/loaddial.hpp>
#include <components/misc/strings/algorithm.hpp>
namespace CSMWorld #include "collection.hpp"
{ #include "info.hpp"
template <>
void Collection<Info, IdAccessor<Info>>::removeRows(int index, int count)
{
mRecords.erase(mRecords.begin() + index, mRecords.begin() + index + count);
// index map is updated in InfoCollection::removeRows() bool CSMWorld::InfoCollection::load(const Info& record, bool base)
}
template <>
void Collection<Info, IdAccessor<Info>>::insertRecord(
std::unique_ptr<RecordBase> record, int index, UniversalId::Type type)
{
int size = static_cast<int>(mRecords.size());
if (index < 0 || index > size)
throw std::runtime_error("index out of range");
std::unique_ptr<Record<Info>> record2(static_cast<Record<Info>*>(record.release()));
if (index == size)
mRecords.push_back(std::move(record2));
else
mRecords.insert(mRecords.begin() + index, std::move(record2));
// index map is updated in InfoCollection::insertRecord()
}
template <>
bool Collection<Info, IdAccessor<Info>>::reorderRowsImp(int baseIndex, const std::vector<int>& newOrder)
{
if (!newOrder.empty())
{
int size = static_cast<int>(newOrder.size());
// check that all indices are present
std::vector<int> test(newOrder);
std::sort(test.begin(), test.end());
if (*test.begin() != 0 || *--test.end() != size - 1)
return false;
// reorder records
std::vector<std::unique_ptr<Record<Info>>> buffer(size);
// FIXME: BUG: undo does not remove modified flag
for (int i = 0; i < size; ++i)
{
buffer[newOrder[i]] = std::move(mRecords[baseIndex + i]);
if (buffer[newOrder[i]])
buffer[newOrder[i]]->setModified(buffer[newOrder[i]]->get());
}
std::move(buffer.begin(), buffer.end(), mRecords.begin() + baseIndex);
// index map is updated in InfoCollection::reorderRows()
}
return true;
}
}
void CSMWorld::InfoCollection::load(const Info& record, bool base)
{ {
int index = searchId(record.mId.getRefIdString()); int index = searchId(record.mId.getRefIdString());
@ -86,6 +22,8 @@ void CSMWorld::InfoCollection::load(const Info& record, bool base)
(base ? record2->mBase : record2->mModified) = record; (base ? record2->mBase : record2->mModified) = record;
appendRecord(std::move(record2)); appendRecord(std::move(record2));
return true;
} }
else else
{ {
@ -98,107 +36,13 @@ void CSMWorld::InfoCollection::load(const Info& record, bool base)
record2->setModified(record); record2->setModified(record);
setRecord(index, std::move(record2)); setRecord(index, std::move(record2));
}
}
int CSMWorld::InfoCollection::getInfoIndex(std::string_view id, std::string_view topic) const
{
// find the topic first
std::unordered_map<std::string, std::vector<std::pair<std::string, int>>>::const_iterator iter
= mInfoIndex.find(Misc::StringUtils::lowerCase(topic));
if (iter == mInfoIndex.end())
return -1;
// brute force loop
for (std::vector<std::pair<std::string, int>>::const_iterator it = iter->second.begin(); it != iter->second.end();
++it)
{
if (Misc::StringUtils::ciEqual(it->first, id))
return it->second;
}
return -1;
}
// Calling insertRecord() using index from getInsertIndex() needs to take into account of
// prev/next records; an example is deleting a record then undo
int CSMWorld::InfoCollection::getInsertIndex(const std::string& id, UniversalId::Type type, RecordBase* record) const
{
if (record == nullptr)
{
std::string::size_type separator = id.find_last_of('#');
if (separator == std::string::npos)
throw std::runtime_error("invalid info ID: " + id);
std::pair<RecordConstIterator, RecordConstIterator> range = getTopicRange(id.substr(0, separator));
if (range.first == range.second)
return Collection<Info, IdAccessor<Info>>::getAppendIndex(ESM::RefId::stringRefId(id), type);
return std::distance(getRecords().begin(), range.second);
}
int index = -1;
const Info& info = static_cast<Record<Info>*>(record)->get();
const std::string& topic = info.mTopicId.getRefIdString();
// if the record has a prev, find its index value
if (!info.mPrev.empty())
{
index = getInfoIndex(info.mPrev.getRefIdString(), topic);
if (index != -1)
++index; // if prev exists, set current index to one above prev
}
// if prev doesn't exist or not found and the record has a next, find its index value
if (index == -1 && !info.mNext.empty())
{
// if next exists, use its index as the current index
index = getInfoIndex(info.mNext.getRefIdString(), topic);
}
// if next doesn't exist or not found (i.e. neither exist yet) then start a new one
if (index == -1)
{
Range range = getTopicRange(topic); // getTopicRange converts topic to lower case first
index = std::distance(getRecords().begin(), range.second);
}
return index;
}
bool CSMWorld::InfoCollection::reorderRows(int baseIndex, const std::vector<int>& newOrder)
{
// check if the range is valid
int lastIndex = baseIndex + newOrder.size() - 1;
if (lastIndex >= getSize())
return false; return false;
}
// Check that topics match
if (!(getRecord(baseIndex).get().mTopicId == getRecord(lastIndex).get().mTopicId))
return false;
// reorder
if (!Collection<Info, IdAccessor<Info>>::reorderRowsImp(baseIndex, newOrder))
return false;
// adjust index
int size = static_cast<int>(newOrder.size());
for (auto& [hash, infos] : mInfoIndex)
for (auto& [a, b] : infos)
if (b >= baseIndex && b < baseIndex + size)
b = newOrder.at(b - baseIndex) + baseIndex;
return true;
} }
void CSMWorld::InfoCollection::load(ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue) void CSMWorld::InfoCollection::load(
ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue, InfosByTopic& infosByTopic)
{ {
Info info; Info info;
bool isDeleted = false; bool isDeleted = false;
@ -230,174 +74,19 @@ void CSMWorld::InfoCollection::load(ESM::ESMReader& reader, bool base, const ESM
else else
{ {
info.mTopicId = dialogue.mId; info.mTopicId = dialogue.mId;
info.mOriginalId = info.mId;
info.mId = ESM::RefId::stringRefId(id); info.mId = ESM::RefId::stringRefId(id);
load(info, base);
if (load(info, base))
infosByTopic[dialogue.mId].push_back(info.mId);
} }
} }
CSMWorld::InfoCollection::Range CSMWorld::InfoCollection::getTopicRange(const std::string& topic) const std::vector<CSMWorld::Record<CSMWorld::Info>*> CSMWorld::InfoCollection::getTopicInfos(const ESM::RefId& topic) const
{ {
std::string lowerTopic = Misc::StringUtils::lowerCase(topic); std::vector<CSMWorld::Record<CSMWorld::Info>*> result;
for (const std::unique_ptr<Record<Info>>& record : getRecords())
// find the topic if (record->mBase.mTopicId == topic)
std::unordered_map<std::string, std::vector<std::pair<std::string, int>>>::const_iterator iter result.push_back(record.get());
= mInfoIndex.find(lowerTopic); return result;
if (iter == mInfoIndex.end())
return Range(getRecords().end(), getRecords().end());
// topic found, find the starting index
int low = INT_MAX;
for (std::vector<std::pair<std::string, int>>::const_iterator it = iter->second.begin(); it != iter->second.end();
++it)
{
low = std::min(low, it->second);
}
RecordConstIterator begin = getRecords().begin() + low;
// Find end (one past the range)
RecordConstIterator end = begin + iter->second.size();
assert(static_cast<size_t>(std::distance(begin, end)) == iter->second.size());
return Range(begin, end);
}
void CSMWorld::InfoCollection::removeDialogueInfos(const std::string& dialogueId)
{
std::vector<int> erasedRecords;
Range range = getTopicRange(dialogueId); // getTopicRange converts dialogueId to lower case first
for (; range.first != range.second; ++range.first)
{
const Record<Info>& record = **range.first;
if ((ESM::RefId::stringRefId(dialogueId) == record.get().mTopicId))
{
if (record.mState == RecordBase::State_ModifiedOnly)
{
erasedRecords.push_back(range.first - getRecords().begin());
}
else
{
auto record2 = std::make_unique<Record<Info>>(record);
record2->mState = RecordBase::State_Deleted;
setRecord(range.first - getRecords().begin(), std::move(record2));
}
}
else
{
break;
}
}
while (!erasedRecords.empty())
{
removeRows(erasedRecords.back(), 1);
erasedRecords.pop_back();
}
}
// FIXME: removing a record should adjust prev/next and mark those records as modified
// accordingly (also consider undo)
void CSMWorld::InfoCollection::removeRows(int index, int count)
{
Collection<Info, IdAccessor<Info>>::removeRows(index, count); // erase records only
for (std::unordered_map<std::string, std::vector<std::pair<std::string, int>>>::iterator iter = mInfoIndex.begin();
iter != mInfoIndex.end();)
{
for (std::vector<std::pair<std::string, int>>::iterator it = iter->second.begin(); it != iter->second.end();)
{
if (it->second >= index)
{
if (it->second >= index + count)
{
it->second -= count;
++it;
}
else
it = iter->second.erase(it);
}
else
++it;
}
// check for an empty vector
if (iter->second.empty())
mInfoIndex.erase(iter++);
else
++iter;
}
}
void CSMWorld::InfoCollection::appendBlankRecord(const ESM::RefId& id, UniversalId::Type type)
{
auto record2 = std::make_unique<Record<Info>>();
record2->mState = Record<Info>::State_ModifiedOnly;
record2->mModified.blank();
record2->get().mId = id;
insertRecord(std::move(record2), getInsertIndex(id.getRefIdString(), type, nullptr),
type); // call InfoCollection::insertRecord()
}
int CSMWorld::InfoCollection::searchId(std::string_view id) const
{
std::string::size_type separator = id.find_last_of('#');
if (separator == std::string::npos)
throw std::runtime_error("invalid info ID: " + std::string(id));
return getInfoIndex(id.substr(separator + 1), id.substr(0, separator));
}
void CSMWorld::InfoCollection::appendRecord(std::unique_ptr<RecordBase> record, UniversalId::Type type)
{
int index
= getInsertIndex(static_cast<Record<Info>*>(record.get())->get().mId.getRefIdString(), type, record.get());
insertRecord(std::move(record), index, type);
}
void CSMWorld::InfoCollection::insertRecord(std::unique_ptr<RecordBase> record, int index, UniversalId::Type type)
{
int size = static_cast<int>(getRecords().size());
std::string id = static_cast<Record<Info>*>(record.get())->get().mId.getRefIdString();
std::string::size_type separator = id.find_last_of('#');
if (separator == std::string::npos)
throw std::runtime_error("invalid info ID: " + id);
Collection<Info, IdAccessor<Info>>::insertRecord(std::move(record), index, type); // add records only
// adjust index
if (index < size - 1)
{
for (std::unordered_map<std::string, std::vector<std::pair<std::string, int>>>::iterator iter
= mInfoIndex.begin();
iter != mInfoIndex.end(); ++iter)
{
for (std::vector<std::pair<std::string, int>>::iterator it = iter->second.begin(); it != iter->second.end();
++it)
{
if (it->second >= index)
++(it->second);
}
}
}
// get iterator for existing topic or a new topic
std::string lowerId = Misc::StringUtils::lowerCase(id);
std::pair<std::unordered_map<std::string, std::vector<std::pair<std::string, int>>>::iterator, bool> res
= mInfoIndex.insert(
std::make_pair(lowerId.substr(0, separator), std::vector<std::pair<std::string, int>>())); // empty vector
// insert info and index
res.first->second.push_back(std::make_pair(lowerId.substr(separator + 1), index));
} }

View file

@ -1,19 +1,13 @@
#ifndef CSM_WOLRD_INFOCOLLECTION_H #ifndef CSM_WOLRD_INFOCOLLECTION_H
#define CSM_WOLRD_INFOCOLLECTION_H #define CSM_WOLRD_INFOCOLLECTION_H
#include <memory>
#include <string> #include <string>
#include <string_view>
#include <unordered_map> #include <unordered_map>
#include <utility>
#include <variant>
#include <vector> #include <vector>
#include <apps/opencs/model/world/record.hpp>
#include <apps/opencs/model/world/universalid.hpp>
#include "collection.hpp" #include "collection.hpp"
#include "info.hpp" #include "info.hpp"
#include "record.hpp"
namespace ESM namespace ESM
{ {
@ -23,77 +17,17 @@ namespace ESM
namespace CSMWorld namespace CSMWorld
{ {
template <> using InfosByTopic = std::unordered_map<ESM::RefId, std::vector<ESM::RefId>>;
void Collection<Info, IdAccessor<Info>>::removeRows(int index, int count);
template <>
void Collection<Info, IdAccessor<Info>>::insertRecord(
std::unique_ptr<RecordBase> record, int index, UniversalId::Type type);
template <>
bool Collection<Info, IdAccessor<Info>>::reorderRowsImp(int baseIndex, const std::vector<int>& newOrder);
class InfoCollection : public Collection<Info, IdAccessor<Info>> class InfoCollection : public Collection<Info, IdAccessor<Info>>
{ {
public:
typedef std::vector<std::unique_ptr<Record<Info>>>::const_iterator RecordConstIterator;
typedef std::pair<RecordConstIterator, RecordConstIterator> Range;
private: private:
// The general strategy is to keep the records in Collection kept in order (within bool load(const Info& record, bool base);
// a topic group) while the index lookup maps are not ordered. It is assumed that
// each topic has a small number of infos, which allows the use of vectors for
// iterating through them without too much penalty.
//
// NOTE: topic string as well as id string are stored in lower case.
std::unordered_map<std::string, std::vector<std::pair<std::string, int>>> mInfoIndex;
void load(const Info& record, bool base);
int getInfoIndex(std::string_view id, std::string_view topic) const;
///< Return index for record \a id or -1 (if not present; deleted records are considered)
///
/// \param id info ID without topic prefix
//
/// \attention id and topic are assumed to be in lower case
public: public:
int getInsertIndex(const std::string& id, UniversalId::Type type = UniversalId::Type_None, void load(ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue, InfosByTopic& infosByTopic);
RecordBase* record = nullptr) const override;
///< \param type Will be ignored, unless the collection supports multiple record types
///
/// Works like getAppendIndex unless an overloaded method uses the record pointer
/// to get additional info about the record that results in an alternative index.
int getAppendIndex(const ESM::RefId& id, UniversalId::Type type) const override std::vector<Record<Info>*> getTopicInfos(const ESM::RefId& topic) const;
{
return getInsertIndex(id.getRefIdString(), type);
}
bool reorderRows(int baseIndex, const std::vector<int>& newOrder) override;
///< 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).
///
/// \return Success?
void load(ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue);
Range getTopicRange(const std::string& topic) const;
///< Return iterators that point to the beginning and past the end of the range for
/// the given topic.
void removeDialogueInfos(const std::string& dialogueId);
void removeRows(int index, int count) override;
void appendBlankRecord(const ESM::RefId& id, UniversalId::Type type = UniversalId::Type_None) override;
int searchId(std::string_view id) const override;
void appendRecord(std::unique_ptr<RecordBase> record, UniversalId::Type type = UniversalId::Type_None) override;
void insertRecord(
std::unique_ptr<RecordBase> record, int index, UniversalId::Type type = UniversalId::Type_None) override;
}; };
} }