#include "esmstore.hpp" #include #include #include #include #include #include #include #include #include #include "../mwmechanics/spelllist.hpp" namespace { struct Ref { ESM::RefNum mRefNum; std::size_t mRefID; Ref(ESM::RefNum refNum, std::size_t refID) : mRefNum(refNum), mRefID(refID) {} }; constexpr std::size_t deletedRefID = std::numeric_limits::max(); void readRefs(const ESM::Cell& cell, std::vector& refs, std::vector& refIDs, std::vector& readers) { // TODO: we have many similar copies of this code. for (size_t i = 0; i < cell.mContextList.size(); i++) { size_t index = cell.mContextList[i].index; if (readers.size() <= index) readers.resize(index + 1); cell.restore(readers[index], i); ESM::CellRef ref; ref.mRefNum.unset(); bool deleted = false; while(cell.getNextRef(readers[index], ref, deleted)) { if(deleted) refs.emplace_back(ref.mRefNum, deletedRefID); else if (std::find(cell.mMovedRefs.begin(), cell.mMovedRefs.end(), ref.mRefNum) == cell.mMovedRefs.end()) { refs.emplace_back(ref.mRefNum, refIDs.size()); refIDs.push_back(std::move(ref.mRefID)); } } } for(const auto& [value, deleted] : cell.mLeasedRefs) { if(deleted) refs.emplace_back(value.mRefNum, deletedRefID); else { refs.emplace_back(value.mRefNum, refIDs.size()); refIDs.push_back(value.mRefID); } } } std::vector getNPCsToReplace(const MWWorld::Store& factions, const MWWorld::Store& classes, const std::unordered_map& npcs) { // Cache first class from store - we will use it if current class is not found std::string defaultCls; auto it = classes.begin(); if (it != classes.end()) defaultCls = it->mId; else throw std::runtime_error("List of NPC classes is empty!"); // Validate NPCs for non-existing class and faction. // We will replace invalid entries by fixed ones std::vector npcsToReplace; for (const auto& npcIter : npcs) { ESM::NPC npc = npcIter.second; bool changed = false; const std::string npcFaction = npc.mFaction; if (!npcFaction.empty()) { const ESM::Faction *fact = factions.search(npcFaction); if (!fact) { Log(Debug::Verbose) << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent faction '" << npc.mFaction << "', ignoring it."; npc.mFaction.clear(); npc.mNpdt.mRank = 0; changed = true; } } std::string npcClass = npc.mClass; if (!npcClass.empty()) { const ESM::Class *cls = classes.search(npcClass); if (!cls) { Log(Debug::Verbose) << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent class '" << npc.mClass << "', using '" << defaultCls << "' class as replacement."; npc.mClass = defaultCls; changed = true; } } if (changed) npcsToReplace.push_back(npc); } return npcsToReplace; } // Custom enchanted items can reference scripts that no longer exist, this doesn't necessarily mean the base item no longer exists however. // So instead of removing the item altogether, we're only removing the script. template void removeMissingScripts(const MWWorld::Store& scripts, MapT& items) { for(auto& [id, item] : items) { if(!item.mScript.empty() && !scripts.search(item.mScript)) { item.mScript.clear(); Log(Debug::Verbose) << "Item '" << id << "' (" << item.mName << ") has nonexistent script '" << item.mScript << "', ignoring it."; } } } } namespace MWWorld { static bool isCacheableRecord(int id) { if (id == ESM::REC_ACTI || id == ESM::REC_ALCH || id == ESM::REC_APPA || id == ESM::REC_ARMO || id == ESM::REC_BOOK || id == ESM::REC_CLOT || id == ESM::REC_CONT || id == ESM::REC_CREA || id == ESM::REC_DOOR || id == ESM::REC_INGR || id == ESM::REC_LEVC || id == ESM::REC_LEVI || id == ESM::REC_LIGH || id == ESM::REC_LOCK || id == ESM::REC_MISC || id == ESM::REC_NPC_ || id == ESM::REC_PROB || id == ESM::REC_REPA || id == ESM::REC_STAT || id == ESM::REC_WEAP || id == ESM::REC_BODY) { return true; } return false; } void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener) { listener->setProgressRange(1000); ESM::Dialogue *dialogue = nullptr; // Land texture loading needs to use a separate internal store for each plugin. // We set the number of plugins here so we can properly verify if valid plugin // indices are being passed to the LandTexture Store retrieval methods. mLandTextures.resize(esm.getIndex()+1); // Loop through all records while(esm.hasMoreRecs()) { ESM::NAME n = esm.getRecName(); esm.getRecHeader(); // Look up the record type. std::map::iterator it = mStores.find(n.toInt()); if (it == mStores.end()) { if (n.toInt() == ESM::REC_INFO) { if (dialogue) { dialogue->readInfo(esm, esm.getIndex() != 0); } else { Log(Debug::Error) << "Error: info record without dialog"; esm.skipRecord(); } } else if (n.toInt() == ESM::REC_MGEF) { mMagicEffects.load (esm); } else if (n.toInt() == ESM::REC_SKIL) { mSkills.load (esm); } else if (n.toInt() == ESM::REC_FILT || n.toInt() == ESM::REC_DBGP) { // ignore project file only records esm.skipRecord(); } else if (n.toInt() == ESM::REC_LUAL) { ESM::LuaScriptsCfg cfg; cfg.load(esm); // TODO: update refnums in cfg.mScripts[].mInitializationData according to load order mLuaContent.push_back(std::move(cfg)); } else { throw std::runtime_error("Unknown record: " + n.toString()); } } else { RecordId id = it->second->load(esm); if (id.mIsDeleted) { it->second->eraseStatic(id.mId); continue; } if (n.toInt() == ESM::REC_DIAL) { dialogue = const_cast(mDialogs.find(id.mId)); } else { dialogue = nullptr; } } listener->setProgress(static_cast(esm.getFileOffset() / (float)esm.getFileSize() * 1000)); } } ESM::LuaScriptsCfg ESMStore::getLuaScriptsCfg() const { ESM::LuaScriptsCfg cfg; for (const LuaContent& c : mLuaContent) { if (std::holds_alternative(c)) { // *.omwscripts are intentionally reloaded every time when `getLuaScriptsCfg` is called. // It is important for the `reloadlua` console command. try { auto file = std::ifstream(std::get(c)); std::string fileContent(std::istreambuf_iterator(file), {}); LuaUtil::parseOMWScripts(cfg, fileContent); } catch (std::exception& e) { Log(Debug::Error) << e.what(); } } else { const ESM::LuaScriptsCfg& addition = std::get(c); cfg.mScripts.insert(cfg.mScripts.end(), addition.mScripts.begin(), addition.mScripts.end()); } } return cfg; } void ESMStore::setUp(bool validateRecords) { mIds.clear(); std::map::iterator storeIt = mStores.begin(); for (; storeIt != mStores.end(); ++storeIt) { storeIt->second->setUp(); if (isCacheableRecord(storeIt->first)) { std::vector identifiers; storeIt->second->listIdentifier(identifiers); for (std::vector::const_iterator record = identifiers.begin(); record != identifiers.end(); ++record) mIds[*record] = storeIt->first; } } if (mStaticIds.empty()) mStaticIds = mIds; mSkills.setUp(); mMagicEffects.setUp(); mAttributes.setUp(); mDialogs.setUp(); if (validateRecords) { validate(); countAllCellRefs(); } } void ESMStore::countAllCellRefs() { // TODO: We currently need to read entire files here again. // We should consider consolidating or deferring this reading. if(!mRefCount.empty()) return; std::vector refs; std::vector refIDs; std::vector readers; for(auto it = mCells.intBegin(); it != mCells.intEnd(); ++it) readRefs(*it, refs, refIDs, readers); for(auto it = mCells.extBegin(); it != mCells.extEnd(); ++it) readRefs(*it, refs, refIDs, readers); const auto lessByRefNum = [] (const Ref& l, const Ref& r) { return l.mRefNum < r.mRefNum; }; std::stable_sort(refs.begin(), refs.end(), lessByRefNum); const auto equalByRefNum = [] (const Ref& l, const Ref& r) { return l.mRefNum == r.mRefNum; }; const auto incrementRefCount = [&] (const Ref& value) { if (value.mRefID != deletedRefID) { std::string& refId = refIDs[value.mRefID]; // We manually lower case IDs here for the time being to improve performance. Misc::StringUtils::lowerCaseInPlace(refId); ++mRefCount[std::move(refId)]; } }; Misc::forEachUnique(refs.rbegin(), refs.rend(), equalByRefNum, incrementRefCount); } int ESMStore::getRefCount(const std::string& id) const { const std::string lowerId = Misc::StringUtils::lowerCase(id); auto it = mRefCount.find(lowerId); if(it == mRefCount.end()) return 0; return it->second; } void ESMStore::validate() { std::vector npcsToReplace = getNPCsToReplace(mFactions, mClasses, mNpcs.mStatic); for (const ESM::NPC &npc : npcsToReplace) { mNpcs.eraseStatic(npc.mId); mNpcs.insertStatic(npc); } // Validate spell effects for invalid arguments std::vector spellsToReplace; for (ESM::Spell spell : mSpells) { if (spell.mEffects.mList.empty()) continue; bool changed = false; auto iter = spell.mEffects.mList.begin(); while (iter != spell.mEffects.mList.end()) { const ESM::MagicEffect* mgef = mMagicEffects.search(iter->mEffectID); if (!mgef) { Log(Debug::Verbose) << "Spell '" << spell.mId << "' has an invalid effect (index " << iter->mEffectID << ") present. Dropping the effect."; iter = spell.mEffects.mList.erase(iter); changed = true; continue; } if (mgef->mData.mFlags & ESM::MagicEffect::TargetSkill) { if (iter->mAttribute != -1) { iter->mAttribute = -1; Log(Debug::Verbose) << ESM::MagicEffect::effectIdToString(iter->mEffectID) << " effect of spell '" << spell.mId << "' has an attribute argument present. Dropping the argument."; changed = true; } } else if (mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute) { if (iter->mSkill != -1) { iter->mSkill = -1; Log(Debug::Verbose) << ESM::MagicEffect::effectIdToString(iter->mEffectID) << " effect of spell '" << spell.mId << "' has a skill argument present. Dropping the argument."; changed = true; } } else if (iter->mSkill != -1 || iter->mAttribute != -1) { iter->mSkill = -1; iter->mAttribute = -1; Log(Debug::Verbose) << ESM::MagicEffect::effectIdToString(iter->mEffectID) << " effect of spell '" << spell.mId << "' has argument(s) present. Dropping the argument(s)."; changed = true; } ++iter; } if (changed) spellsToReplace.emplace_back(spell); } for (const ESM::Spell &spell : spellsToReplace) { mSpells.eraseStatic(spell.mId); mSpells.insertStatic(spell); } } void ESMStore::validateDynamic() { std::vector npcsToReplace = getNPCsToReplace(mFactions, mClasses, mNpcs.mDynamic); for (const ESM::NPC &npc : npcsToReplace) mNpcs.insert(npc); removeMissingScripts(mScripts, mArmors.mDynamic); removeMissingScripts(mScripts, mBooks.mDynamic); removeMissingScripts(mScripts, mClothes.mDynamic); removeMissingScripts(mScripts, mWeapons.mDynamic); removeMissingObjects(mCreatureLists); removeMissingObjects(mItemLists); } // Leveled lists can be modified by scripts. This removes items that no longer exist (presumably because the plugin was removed) from modified lists template void ESMStore::removeMissingObjects(Store& store) { for(auto& entry : store.mDynamic) { auto first = std::remove_if(entry.second.mList.begin(), entry.second.mList.end(), [&] (const auto& item) { if(!find(item.mId)) { Log(Debug::Verbose) << "Leveled list '" << entry.first << "' has nonexistent object '" << item.mId << "', ignoring it."; return true; } return false; }); entry.second.mList.erase(first, entry.second.mList.end()); } } int ESMStore::countSavedGameRecords() const { return 1 // DYNA (dynamic name counter) +mPotions.getDynamicSize() +mArmors.getDynamicSize() +mBooks.getDynamicSize() +mClasses.getDynamicSize() +mClothes.getDynamicSize() +mEnchants.getDynamicSize() +mNpcs.getDynamicSize() +mSpells.getDynamicSize() +mWeapons.getDynamicSize() +mCreatureLists.getDynamicSize() +mItemLists.getDynamicSize() +mCreatures.getDynamicSize() +mContainers.getDynamicSize(); } void ESMStore::write (ESM::ESMWriter& writer, Loading::Listener& progress) const { writer.startRecord(ESM::REC_DYNA); writer.startSubRecord("COUN"); writer.writeT(mDynamicCount); writer.endRecord("COUN"); writer.endRecord(ESM::REC_DYNA); mPotions.write (writer, progress); mArmors.write (writer, progress); mBooks.write (writer, progress); mClasses.write (writer, progress); mClothes.write (writer, progress); mEnchants.write (writer, progress); mSpells.write (writer, progress); mWeapons.write (writer, progress); mNpcs.write (writer, progress); mItemLists.write (writer, progress); mCreatureLists.write (writer, progress); mCreatures.write (writer, progress); mContainers.write (writer, progress); } bool ESMStore::readRecord (ESM::ESMReader& reader, uint32_t type) { switch (type) { case ESM::REC_ALCH: case ESM::REC_ARMO: case ESM::REC_BOOK: case ESM::REC_CLAS: case ESM::REC_CLOT: case ESM::REC_ENCH: case ESM::REC_SPEL: case ESM::REC_WEAP: case ESM::REC_LEVI: case ESM::REC_LEVC: mStores[type]->read (reader); return true; case ESM::REC_NPC_: case ESM::REC_CREA: case ESM::REC_CONT: mStores[type]->read (reader, true); return true; case ESM::REC_DYNA: reader.getSubNameIs("COUN"); reader.getHT(mDynamicCount); return true; default: return false; } } void ESMStore::checkPlayer() { setUp(); const ESM::NPC *player = mNpcs.find ("player"); if (!mRaces.find (player->mRace) || !mClasses.find (player->mClass)) throw std::runtime_error ("Invalid player record (race or class unavailable"); } std::pair, bool> ESMStore::getSpellList(const std::string& id) const { auto result = mSpellListCache.find(id); std::shared_ptr ptr; if (result != mSpellListCache.end()) ptr = result->second.lock(); if (!ptr) { int type = find(id); ptr = std::make_shared(id, type); if (result != mSpellListCache.end()) result->second = ptr; else mSpellListCache.insert({id, ptr}); return {ptr, false}; } return {ptr, true}; } } // end namespace