#include "refcollection.hpp" #include #include #include #include #include #include #include #include #include "cell.hpp" #include "record.hpp" #include "ref.hpp" #include "universalid.hpp" #include "../doc/messages.hpp" namespace CSMWorld { template <> void Collection::removeRows(int index, int count) { mRecords.erase(mRecords.begin() + index, mRecords.begin() + index + count); // index map is updated in RefCollection::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 RefCollection::insertRecord() } } void CSMWorld::RefCollection::load(ESM::ESMReader& reader, int cellIndex, bool base, std::map& cache, CSMDoc::Messages& messages) { Record cell = mCells.getRecord(cellIndex); Cell& cell2 = base ? cell.mBase : cell.mModified; ESM::MovedCellRef mref; bool isDeleted = false; bool isMoved = false; while (true) { CellRef ref; ref.mNew = false; if (!ESM::Cell::getNextRef(reader, ref, isDeleted, mref, isMoved)) break; // Keep mOriginalCell empty when in modified (as an indicator that the // original cell will always be equal the current cell). ref.mOriginalCell = base ? cell2.mId : ESM::RefId(); if (cell.get().isExterior()) { // Autocalculate the cell index from coordinates first std::pair index = ref.getCellIndex(); ref.mCell = ESM::RefId::stringRefId(ESM::RefId::esm3ExteriorCell(index.first, index.second).toString()); // Handle non-base moved references if (!base && isMoved) { // Moved references must have a link back to their original cell // See discussion: https://forum.openmw.org/viewtopic.php?f=6&t=577&start=30 ref.mOriginalCell = cell2.mId; // Some mods may move references outside of the bounds, which often happens they are deleted. // This results in nonsensical autocalculated cell IDs, so we must use the record target cell. // Log a warning if the record target cell is different if (index.first != mref.mTarget[0] || index.second != mref.mTarget[1]) { ESM::RefId indexCell = ref.mCell; ref.mCell = ESM::RefId::stringRefId( ESM::RefId::esm3ExteriorCell(mref.mTarget[0], mref.mTarget[1]).toString()); CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, mCells.getId(cellIndex)); messages.add(id, "The position of the moved reference " + ref.mRefID.toDebugString() + " (cell " + indexCell.toDebugString() + ")" " does not match the target cell (" + ref.mCell.toDebugString() + ")", std::string(), CSMDoc::Message::Severity_Warning); } } } else ref.mCell = cell2.mId; if (ref.mRefNum.mContentFile != -1 && !base) { ref.mRefNum.mContentFile = ref.mRefNum.mIndex >> 24; ref.mRefNum.mIndex &= 0x00ffffff; } unsigned int refNum = (ref.mRefNum.mIndex & 0x00ffffff) | (ref.mRefNum.hasContentFile() ? ref.mRefNum.mContentFile : 0xff) << 24; std::map::iterator iter = cache.find(refNum); if (isMoved) { if (iter == cache.end()) { CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, mCells.getId(cellIndex)); messages.add(id, "Attempt to move a non-existent reference - RefNum index " + std::to_string(ref.mRefNum.mIndex) + ", refID " + ref.mRefID.toDebugString() + ", content file index " + std::to_string(ref.mRefNum.mContentFile), /*hint*/ "", CSMDoc::Message::Severity_Warning); continue; } int index = getIntIndex(iter->second); // ensure we have the same record id for setRecord() ref.mId = getRecord(index).get().mId; ref.mIdNum = extractIdNum(ref.mId.getRefIdString()); auto record = std::make_unique>(); // TODO: check whether a base record be moved record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; (base ? record->mBase : record->mModified) = std::move(ref); // overwrite original record setRecord(index, std::move(record)); continue; // NOTE: assumed moved references are not deleted at the same time } if (isDeleted) { if (iter == cache.end()) { CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, mCells.getId(cellIndex)); messages.add(id, "Attempt to delete a non-existent reference - RefNum index " + std::to_string(ref.mRefNum.mIndex) + ", refID " + ref.mRefID.getRefIdString() + ", content file index " + std::to_string(ref.mRefNum.mContentFile), /*hint*/ "", CSMDoc::Message::Severity_Warning); continue; } int index = getIntIndex(iter->second); if (base) { removeRows(index, 1); cache.erase(iter); } else { auto record = std::make_unique>(getRecord(index)); record->mState = RecordBase::State_Deleted; setRecord(index, std::move(record)); } continue; } if (iter == cache.end()) { // new reference ref.mIdNum = mNextId; // FIXME: fragile ref.mId = ESM::RefId::stringRefId(getNewId()); cache.emplace(refNum, ref.mIdNum); auto record = std::make_unique>(); record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_ModifiedOnly; (base ? record->mBase : record->mModified) = std::move(ref); appendRecord(std::move(record)); } else { // old reference -> merge int index = getIntIndex(iter->second); #if 0 // ref.mRefNum.mIndex : the key // iter->second : previously cached idNum for the key // index : position of the record for that idNum // getRecord(index).get() : record in the index position assert(iter->second != getRecord(index).get().mIdNum); // sanity check // check if the plugin used the same RefNum index for a different record if (ref.mRefID != getRecord(index).get().mRefID) { CSMWorld::UniversalId id(CSMWorld::UniversalId::Type_Cell, mCells.getId(cellIndex)); messages.add(id, "RefNum renamed from RefID \"" + getRecord(index).get().mRefID + "\" to \"" + ref.mRefID + "\" (RefNum index " + std::to_string(ref.mRefNum.mIndex) + ")", /*hint*/"", CSMDoc::Message::Severity_Info); } #endif ref.mId = getRecord(index).get().mId; ref.mIdNum = extractIdNum(ref.mId.getRefIdString()); auto record = std::make_unique>(getRecord(index)); record->mState = base ? RecordBase::State_BaseOnly : RecordBase::State_Modified; (base ? record->mBase : record->mModified) = std::move(ref); setRecord(index, std::move(record)); } } } std::string CSMWorld::RefCollection::getNewId() { return "ref#" + std::to_string(mNextId++); } unsigned int CSMWorld::RefCollection::extractIdNum(std::string_view id) const { std::string::size_type separator = id.find_last_of('#'); if (separator == std::string::npos) throw std::runtime_error("invalid ref ID: " + std::string(id)); return Misc::StringUtils::toNumeric(id.substr(separator + 1), 0); } int CSMWorld::RefCollection::getIntIndex(unsigned int id) const { int index = searchId(id); if (index == -1) throw std::runtime_error("invalid RefNum: " + std::to_string(id)); return index; } int CSMWorld::RefCollection::searchId(unsigned int id) const { std::map::const_iterator iter = mRefIndex.find(id); if (iter == mRefIndex.end()) return -1; return iter->second; } void CSMWorld::RefCollection::removeRows(int index, int count) { Collection::removeRows(index, count); // erase records only std::map::iterator iter = mRefIndex.begin(); while (iter != mRefIndex.end()) { if (iter->second >= index) { if (iter->second >= index + count) { iter->second -= count; ++iter; } else mRefIndex.erase(iter++); } else ++iter; } } void CSMWorld::RefCollection::appendBlankRecord(const ESM::RefId& id, UniversalId::Type type) { auto record = std::make_unique>(); record->mState = Record::State_ModifiedOnly; record->mModified.blank(); record->get().mId = id; record->get().mIdNum = extractIdNum(id.getRefIdString()); Collection::appendRecord(std::move(record)); } void CSMWorld::RefCollection::cloneRecord( const ESM::RefId& origin, const ESM::RefId& destination, const UniversalId::Type type) { auto copy = std::make_unique>(); copy->mModified = getRecord(origin).get(); copy->mState = RecordBase::State_ModifiedOnly; copy->get().mId = destination; copy->get().mIdNum = extractIdNum(destination.getRefIdString()); insertRecord(std::move(copy), getAppendIndex(destination, type)); // call RefCollection::insertRecord() } int CSMWorld::RefCollection::searchId(const ESM::RefId& id) const { return searchId(extractIdNum(id.getRefIdString())); } void CSMWorld::RefCollection::appendRecord(std::unique_ptr record, UniversalId::Type type) { int index = getAppendIndex(/*id*/ ESM::RefId(), type); // for CellRef records id is ignored mRefIndex.insert(std::make_pair(static_cast*>(record.get())->get().mIdNum, index)); Collection::insertRecord(std::move(record), index, type); // add records only } void CSMWorld::RefCollection::insertRecord(std::unique_ptr record, int index, UniversalId::Type type) { int size = getAppendIndex(/*id*/ ESM::RefId(), type); // for CellRef records id is ignored unsigned int idNum = static_cast*>(record.get())->get().mIdNum; Collection::insertRecord(std::move(record), index, type); // add records only if (index < size - 1) { for (std::map::iterator iter(mRefIndex.begin()); iter != mRefIndex.end(); ++iter) { if (iter->second >= index) ++(iter->second); } } mRefIndex.insert(std::make_pair(idNum, index)); }