#include "infocollection.hpp" #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); // 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) { int index = searchId(record.mId.getRefIdString()); if (index == -1) { // new record auto record2 = std::make_unique>(); record2->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; (base ? record2->mBase : record2->mModified) = record; appendRecord(std::move(record2)); } else { // old record auto record2 = std::make_unique>(getRecord(index)); if (base) record2->mBase = record; else 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) { Info info; bool isDeleted = false; info.load(reader, isDeleted); std::string id = dialogue.mId.getRefIdString() + "#" + info.mId.getRefIdString(); if (isDeleted) { int index = searchId(id); if (index == -1) { // deleting a record that does not exist // ignore it for now /// \todo report the problem to the user } else if (base) { removeRows(index, 1); } else { auto record = std::make_unique>(getRecord(index)); record->mState = RecordBase::State_Deleted; setRecord(index, std::move(record)); } } else { info.mTopicId = dialogue.mId; info.mId = ESM::RefId::stringRefId(id); load(info, base); } } CSMWorld::InfoCollection::Range CSMWorld::InfoCollection::getTopicRange(const std::string& 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)); }