You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
478 lines
16 KiB
C++
478 lines
16 KiB
C++
#include "esmstore.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <set>
|
|
|
|
#include <boost/filesystem/operations.hpp>
|
|
|
|
#include <components/debug/debuglog.hpp>
|
|
#include <components/loadinglistener/loadinglistener.hpp>
|
|
#include <components/esm/esmreader.hpp>
|
|
#include <components/esm/esmwriter.hpp>
|
|
#include <components/misc/algorithm.hpp>
|
|
|
|
#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<std::size_t>::max();
|
|
|
|
void readRefs(const ESM::Cell& cell, std::vector<Ref>& refs, std::vector<std::string>& refIDs, std::vector<ESM::ESMReader>& readers)
|
|
{
|
|
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.mContentFile = ESM::RefNum::RefNum_NoContentFile;
|
|
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<ESM::NPC> getNPCsToReplace(const MWWorld::Store<ESM::Faction>& factions, const MWWorld::Store<ESM::Class>& classes, const std::map<std::string, ESM::NPC>& 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<ESM::NPC> 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;
|
|
}
|
|
}
|
|
|
|
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 to avoid continual resizes during loading,
|
|
// and so we can properly verify if valid plugin indices are being passed to the
|
|
// LandTexture Store retrieval methods.
|
|
mLandTextures.resize(esm.getGlobalReaderList()->size());
|
|
|
|
/// \todo Move this to somewhere else. ESMReader?
|
|
// Cache parent esX files by tracking their indices in the global list of
|
|
// all files/readers used by the engine. This will greaty accelerate
|
|
// refnumber mangling, as required for handling moved references.
|
|
const std::vector<ESM::Header::MasterData> &masters = esm.getGameFiles();
|
|
std::vector<ESM::ESMReader> *allPlugins = esm.getGlobalReaderList();
|
|
for (size_t j = 0; j < masters.size(); j++) {
|
|
const ESM::Header::MasterData &mast = masters[j];
|
|
std::string fname = mast.name;
|
|
int index = ~0;
|
|
for (int i = 0; i < esm.getIndex(); i++) {
|
|
const std::string candidate = allPlugins->at(i).getContext().filename;
|
|
std::string fnamecandidate = boost::filesystem::path(candidate).filename().string();
|
|
if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) {
|
|
index = i;
|
|
break;
|
|
}
|
|
}
|
|
if (index == (int)~0) {
|
|
// Tried to load a parent file that has not been loaded yet. This is bad,
|
|
// the launcher should have taken care of this.
|
|
std::string fstring = "File " + esm.getName() + " asks for parent file " + masters[j].name
|
|
+ ", but it has not been loaded yet. Please check your load order.";
|
|
esm.fail(fstring);
|
|
}
|
|
esm.addParentFileIndex(index);
|
|
}
|
|
|
|
// Loop through all records
|
|
while(esm.hasMoreRecs())
|
|
{
|
|
ESM::NAME n = esm.getRecName();
|
|
esm.getRecHeader();
|
|
|
|
// Look up the record type.
|
|
std::map<int, StoreBase *>::iterator it = mStores.find(n.intval);
|
|
|
|
if (it == mStores.end()) {
|
|
if (n.intval == 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.intval == ESM::REC_MGEF) {
|
|
mMagicEffects.load (esm);
|
|
} else if (n.intval == ESM::REC_SKIL) {
|
|
mSkills.load (esm);
|
|
}
|
|
else if (n.intval==ESM::REC_FILT || n.intval == ESM::REC_DBGP)
|
|
{
|
|
// ignore project file only records
|
|
esm.skipRecord();
|
|
}
|
|
else {
|
|
std::stringstream error;
|
|
error << "Unknown record: " << n.toString();
|
|
throw std::runtime_error(error.str());
|
|
}
|
|
} else {
|
|
RecordId id = it->second->load(esm);
|
|
if (id.mIsDeleted)
|
|
{
|
|
it->second->eraseStatic(id.mId);
|
|
continue;
|
|
}
|
|
|
|
if (n.intval==ESM::REC_DIAL) {
|
|
dialogue = const_cast<ESM::Dialogue*>(mDialogs.find(id.mId));
|
|
} else {
|
|
dialogue = nullptr;
|
|
}
|
|
}
|
|
listener->setProgress(static_cast<size_t>(esm.getFileOffset() / (float)esm.getFileSize() * 1000));
|
|
}
|
|
}
|
|
|
|
void ESMStore::setUp(bool validateRecords)
|
|
{
|
|
mIds.clear();
|
|
|
|
std::map<int, StoreBase *>::iterator storeIt = mStores.begin();
|
|
for (; storeIt != mStores.end(); ++storeIt) {
|
|
storeIt->second->setUp();
|
|
|
|
if (isCacheableRecord(storeIt->first))
|
|
{
|
|
std::vector<std::string> identifiers;
|
|
storeIt->second->listIdentifier(identifiers);
|
|
|
|
for (std::vector<std::string>::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();
|
|
countRecords();
|
|
}
|
|
}
|
|
|
|
void ESMStore::countRecords()
|
|
{
|
|
if(!mRefCount.empty())
|
|
return;
|
|
std::vector<Ref> refs;
|
|
std::vector<std::string> refIDs;
|
|
std::vector<ESM::ESMReader> 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];
|
|
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<ESM::NPC> 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<ESM::Spell> 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<ESM::NPC> npcsToReplace = getNPCsToReplace(mFactions, mClasses, mNpcs.mDynamic);
|
|
|
|
for (const ESM::NPC &npc : npcsToReplace)
|
|
mNpcs.insert(npc);
|
|
}
|
|
|
|
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<std::shared_ptr<MWMechanics::SpellList>, bool> ESMStore::getSpellList(const std::string& originalId) const
|
|
{
|
|
const std::string id = Misc::StringUtils::lowerCase(originalId);
|
|
auto result = mSpellListCache.find(id);
|
|
std::shared_ptr<MWMechanics::SpellList> ptr;
|
|
if (result != mSpellListCache.end())
|
|
ptr = result->second.lock();
|
|
if (!ptr)
|
|
{
|
|
int type = find(id);
|
|
ptr = std::make_shared<MWMechanics::SpellList>(id, type);
|
|
if (result != mSpellListCache.end())
|
|
result->second = ptr;
|
|
else
|
|
mSpellListCache.insert({id, ptr});
|
|
return {ptr, false};
|
|
}
|
|
return {ptr, true};
|
|
}
|
|
} // end namespace
|