diff --git a/apps/opencs/model/doc/savingstages.cpp b/apps/opencs/model/doc/savingstages.cpp index c247e90aa3..4f184f81c4 100644 --- a/apps/opencs/model/doc/savingstages.cpp +++ b/apps/opencs/model/doc/savingstages.cpp @@ -145,11 +145,11 @@ void CSMDoc::WriteDialogueCollectionStage::perform(int stage, Messages& messages // Test, if we need to save anything associated info records. 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; break; @@ -173,35 +173,30 @@ void CSMDoc::WriteDialogueCollectionStage::perform(int stage, Messages& messages } // 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& record = **iter; + + if (record.isModified() || record.mState == CSMWorld::RecordBase::State_Deleted) { - ESM::DialInfo info = (*iter)->get(); - std::string_view infoIdString = info.mId.getRefIdString(); - info.mId = ESM::RefId::stringRefId(infoIdString.substr(infoIdString.find_last_of('#') + 1)); + ESM::DialInfo info = record.get(); + info.mId = record.get().mOriginalId; info.mPrev = ESM::RefId::sEmpty; - if (iter != range.first) + if (iter != infos.begin()) { - CSMWorld::InfoCollection::RecordConstIterator prev = iter; - --prev; - std::string_view prevIdString = (*prev)->get().mId.getRefIdString(); - info.mPrev = ESM::RefId::stringRefId(prevIdString.substr(prevIdString.find_last_of('#') + 1)); + const auto prev = std::prev(iter); + info.mPrev = (*prev)->get().mOriginalId; } - CSMWorld::InfoCollection::RecordConstIterator next = iter; - ++next; + const auto next = std::next(iter); info.mNext = ESM::RefId::sEmpty; - if (next != range.second) - { - std::string_view nextIdString = (*next)->get().mId.getRefIdString(); - info.mNext = ESM::RefId::stringRefId(nextIdString.substr(nextIdString.find_last_of('#') + 1)); - } + if (next != infos.end()) + info.mNext = (*next)->get().mOriginalId; 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); } } diff --git a/apps/opencs/model/tools/journalcheck.cpp b/apps/opencs/model/tools/journalcheck.cpp index 84973c5a9c..5a477ed5a9 100644 --- a/apps/opencs/model/tools/journalcheck.cpp +++ b/apps/opencs/model/tools/journalcheck.cpp @@ -50,16 +50,12 @@ void CSMTools::JournalCheckStage::perform(int stage, CSMDoc::Messages& messages) int totalInfoCount = 0; std::set questIndices; - CSMWorld::InfoCollection::Range range = mJournalInfos.getTopicRange(journal.mId.getRefIdString()); - - for (CSMWorld::InfoCollection::RecordConstIterator it = range.first; it != range.second; ++it) + for (const CSMWorld::Record* record : mJournalInfos.getTopicInfos(journal.mId)) { - const CSMWorld::Record infoRecord = (*it->get()); - - if (infoRecord.isDeleted()) + if (record->isDeleted()) continue; - const CSMWorld::Info& journalInfo = infoRecord.get(); + const CSMWorld::Info& journalInfo = record->get(); totalInfoCount += 1; @@ -69,7 +65,7 @@ void CSMTools::JournalCheckStage::perform(int stage, CSMDoc::Messages& messages) } // Skip "Base" records (setting!) - if (mIgnoreBaseRecords && infoRecord.mState == CSMWorld::RecordBase::State_BaseOnly) + if (mIgnoreBaseRecords && record->mState == CSMWorld::RecordBase::State_BaseOnly) continue; if (journalInfo.mResponse.empty()) diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index ef1c745b4c..3fe87cde40 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -55,6 +55,41 @@ #include "resourcesmanager.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 erasedRecords; + + for (const ESM::RefId& id : topicInfos->second) + { + const CSMWorld::Record& 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>(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) { mModels.push_back(model); @@ -1254,11 +1289,11 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages) if (mJournals.tryDelete(recordIdString)) { - mJournalInfos.removeDialogueInfos(recordIdString); + removeDialogueInfos(record.mId, mJournalInfosByTopic, mJournalInfos); } else if (mTopics.tryDelete(recordIdString)) { - mTopicInfos.removeDialogueInfos(recordIdString); + removeDialogueInfos(record.mId, mTopicInfosByTopic, mTopicInfos); } else { @@ -1296,9 +1331,9 @@ bool CSMWorld::Data::continueLoading(CSMDoc::Messages& messages) } if (mDialogue->mType == ESM::Dialogue::Journal) - mJournalInfos.load(*mReader, mBase, *mDialogue); + mJournalInfos.load(*mReader, mBase, *mDialogue, mJournalInfosByTopic); else - mTopicInfos.load(*mReader, mBase, *mDialogue); + mTopicInfos.load(*mReader, mBase, *mDialogue, mTopicInfosByTopic); break; } diff --git a/apps/opencs/model/world/data.hpp b/apps/opencs/model/world/data.hpp index b012f36045..6db992cca6 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -135,6 +135,9 @@ namespace CSMWorld std::vector> mReaders; + CSMWorld::InfosByTopic mJournalInfosByTopic; + CSMWorld::InfosByTopic mTopicInfosByTopic; + // not implemented Data(const Data&); Data& operator=(const Data&); diff --git a/apps/opencs/model/world/info.hpp b/apps/opencs/model/world/info.hpp index f1743b5760..dd3741b266 100644 --- a/apps/opencs/model/world/info.hpp +++ b/apps/opencs/model/world/info.hpp @@ -8,6 +8,7 @@ namespace CSMWorld struct Info : public ESM::DialInfo { ESM::RefId mTopicId; + ESM::RefId mOriginalId; }; } diff --git a/apps/opencs/model/world/infocollection.cpp b/apps/opencs/model/world/infocollection.cpp index b25f974bc7..b06f7d05f8 100644 --- a/apps/opencs/model/world/infocollection.cpp +++ b/apps/opencs/model/world/infocollection.cpp @@ -1,80 +1,16 @@ #include "infocollection.hpp" -#include -#include -#include -#include -#include - -#include -#include -#include +#include +#include +#include +#include #include -#include -namespace CSMWorld -{ - template <> - void Collection>::removeRows(int index, int count) - { - mRecords.erase(mRecords.begin() + index, mRecords.begin() + index + count); +#include "collection.hpp" +#include "info.hpp" - // index map is updated in InfoCollection::removeRows() - } - - template <> - void Collection>::insertRecord( - std::unique_ptr record, int index, UniversalId::Type type) - { - int size = static_cast(mRecords.size()); - if (index < 0 || index > size) - throw std::runtime_error("index out of range"); - - std::unique_ptr> record2(static_cast*>(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>::reorderRowsImp(int baseIndex, const std::vector& newOrder) - { - if (!newOrder.empty()) - { - int size = static_cast(newOrder.size()); - - // check that all indices are present - std::vector test(newOrder); - std::sort(test.begin(), test.end()); - if (*test.begin() != 0 || *--test.end() != size - 1) - return false; - - // reorder records - std::vector>> 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) +bool CSMWorld::InfoCollection::load(const Info& record, bool base) { 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; appendRecord(std::move(record2)); + + return true; } else { @@ -98,107 +36,13 @@ void CSMWorld::InfoCollection::load(const Info& record, bool base) record2->setModified(record); 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>>::const_iterator iter - = mInfoIndex.find(Misc::StringUtils::lowerCase(topic)); - - if (iter == mInfoIndex.end()) - return -1; - - // brute force loop - for (std::vector>::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 range = getTopicRange(id.substr(0, separator)); - - if (range.first == range.second) - return Collection>::getAppendIndex(ESM::RefId::stringRefId(id), type); - - return std::distance(getRecords().begin(), range.second); - } - - int index = -1; - - const Info& info = static_cast*>(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& newOrder) -{ - // check if the range is valid - int lastIndex = baseIndex + newOrder.size() - 1; - - if (lastIndex >= getSize()) return false; - - // Check that topics match - if (!(getRecord(baseIndex).get().mTopicId == getRecord(lastIndex).get().mTopicId)) - return false; - - // reorder - if (!Collection>::reorderRowsImp(baseIndex, newOrder)) - return false; - - // adjust index - int size = static_cast(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; bool isDeleted = false; @@ -230,174 +74,19 @@ void CSMWorld::InfoCollection::load(ESM::ESMReader& reader, bool base, const ESM else { info.mTopicId = dialogue.mId; + info.mOriginalId = info.mId; 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::InfoCollection::getTopicInfos(const ESM::RefId& topic) const { - std::string lowerTopic = Misc::StringUtils::lowerCase(topic); - - // find the topic - std::unordered_map>>::const_iterator iter - = mInfoIndex.find(lowerTopic); - - if (iter == mInfoIndex.end()) - return Range(getRecords().end(), getRecords().end()); - - // topic found, find the starting index - int low = INT_MAX; - for (std::vector>::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(std::distance(begin, end)) == iter->second.size()); - - return Range(begin, end); -} - -void CSMWorld::InfoCollection::removeDialogueInfos(const std::string& dialogueId) -{ - std::vector erasedRecords; - - Range range = getTopicRange(dialogueId); // getTopicRange converts dialogueId to lower case first - - for (; range.first != range.second; ++range.first) - { - const Record& 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); - 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>::removeRows(index, count); // erase records only - - for (std::unordered_map>>::iterator iter = mInfoIndex.begin(); - iter != mInfoIndex.end();) - { - for (std::vector>::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>(); - - record2->mState = Record::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 record, UniversalId::Type type) -{ - int index - = getInsertIndex(static_cast*>(record.get())->get().mId.getRefIdString(), type, record.get()); - - insertRecord(std::move(record), index, type); -} - -void CSMWorld::InfoCollection::insertRecord(std::unique_ptr record, int index, UniversalId::Type type) -{ - int size = static_cast(getRecords().size()); - - std::string id = static_cast*>(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>::insertRecord(std::move(record), index, type); // add records only - - // adjust index - if (index < size - 1) - { - for (std::unordered_map>>::iterator iter - = mInfoIndex.begin(); - iter != mInfoIndex.end(); ++iter) - { - for (std::vector>::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>>::iterator, bool> res - = mInfoIndex.insert( - std::make_pair(lowerId.substr(0, separator), std::vector>())); // empty vector - - // insert info and index - res.first->second.push_back(std::make_pair(lowerId.substr(separator + 1), index)); + std::vector*> result; + for (const std::unique_ptr>& record : getRecords()) + if (record->mBase.mTopicId == topic) + result.push_back(record.get()); + return result; } diff --git a/apps/opencs/model/world/infocollection.hpp b/apps/opencs/model/world/infocollection.hpp index 04c72201b4..c1b1edcabd 100644 --- a/apps/opencs/model/world/infocollection.hpp +++ b/apps/opencs/model/world/infocollection.hpp @@ -1,19 +1,13 @@ #ifndef CSM_WOLRD_INFOCOLLECTION_H #define CSM_WOLRD_INFOCOLLECTION_H -#include #include -#include #include -#include -#include #include -#include -#include - #include "collection.hpp" #include "info.hpp" +#include "record.hpp" namespace ESM { @@ -23,77 +17,17 @@ namespace ESM namespace CSMWorld { - template <> - void Collection>::removeRows(int index, int count); - - template <> - void Collection>::insertRecord( - std::unique_ptr record, int index, UniversalId::Type type); - - template <> - bool Collection>::reorderRowsImp(int baseIndex, const std::vector& newOrder); + using InfosByTopic = std::unordered_map>; class InfoCollection : public Collection> { - public: - typedef std::vector>>::const_iterator RecordConstIterator; - typedef std::pair Range; - private: - // The general strategy is to keep the records in Collection kept in order (within - // 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>> 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 + bool load(const Info& record, bool base); public: - int getInsertIndex(const std::string& id, UniversalId::Type type = UniversalId::Type_None, - 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. + void load(ESM::ESMReader& reader, bool base, const ESM::Dialogue& dialogue, InfosByTopic& infosByTopic); - int getAppendIndex(const ESM::RefId& id, UniversalId::Type type) const override - { - return getInsertIndex(id.getRefIdString(), type); - } - - bool reorderRows(int baseIndex, const std::vector& 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 record, UniversalId::Type type = UniversalId::Type_None) override; - - void insertRecord( - std::unique_ptr record, int index, UniversalId::Type type = UniversalId::Type_None) override; + std::vector*> getTopicInfos(const ESM::RefId& topic) const; }; }