2012-10-01 15:17:04 +00:00
|
|
|
#include "esmstore.hpp"
|
|
|
|
|
2010-05-17 18:59:15 +00:00
|
|
|
#include <set>
|
2010-05-17 15:35:42 +00:00
|
|
|
|
2013-02-14 09:22:00 +00:00
|
|
|
#include <boost/filesystem/operations.hpp>
|
2012-11-25 16:19:29 +00:00
|
|
|
|
2018-08-14 19:05:43 +00:00
|
|
|
#include <components/debug/debuglog.hpp>
|
2013-08-27 13:48:13 +00:00
|
|
|
#include <components/loadinglistener/loadinglistener.hpp>
|
2015-01-25 00:53:20 +00:00
|
|
|
#include <components/esm/esmreader.hpp>
|
2015-07-09 17:22:04 +00:00
|
|
|
#include <components/esm/esmwriter.hpp>
|
2015-01-25 00:53:20 +00:00
|
|
|
|
2012-10-01 15:17:04 +00:00
|
|
|
namespace MWWorld
|
2010-05-17 15:35:42 +00:00
|
|
|
{
|
|
|
|
|
2012-11-05 14:09:14 +00:00
|
|
|
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_ ||
|
2016-01-01 23:49:53 +00:00
|
|
|
id == ESM::REC_PROB || id == ESM::REC_REPA || id == ESM::REC_STAT || id == ESM::REC_WEAP ||
|
|
|
|
id == ESM::REC_BODY)
|
2012-11-05 14:09:14 +00:00
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2013-08-27 13:48:13 +00:00
|
|
|
void ESMStore::load(ESM::ESMReader &esm, Loading::Listener* listener)
|
2010-05-17 15:35:42 +00:00
|
|
|
{
|
2013-08-27 13:48:13 +00:00
|
|
|
listener->setProgressRange(1000);
|
|
|
|
|
2010-08-06 13:19:39 +00:00
|
|
|
ESM::Dialogue *dialogue = 0;
|
|
|
|
|
2015-11-27 20:40:36 +00:00
|
|
|
// 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());
|
|
|
|
|
2013-03-12 07:15:20 +00:00
|
|
|
/// \todo Move this to somewhere else. ESMReader?
|
2012-11-25 16:19:29 +00:00
|
|
|
// Cache parent esX files by tracking their indices in the global list of
|
2013-01-20 18:07:33 +00:00
|
|
|
// all files/readers used by the engine. This will greaty accelerate
|
|
|
|
// refnumber mangling, as required for handling moved references.
|
2013-09-22 04:06:29 +00:00
|
|
|
const std::vector<ESM::Header::MasterData> &masters = esm.getGameFiles();
|
2012-11-25 16:19:29 +00:00
|
|
|
std::vector<ESM::ESMReader> *allPlugins = esm.getGlobalReaderList();
|
|
|
|
for (size_t j = 0; j < masters.size(); j++) {
|
2013-03-12 07:15:20 +00:00
|
|
|
ESM::Header::MasterData &mast = const_cast<ESM::Header::MasterData&>(masters[j]);
|
2012-11-25 16:19:29 +00:00
|
|
|
std::string fname = mast.name;
|
2014-05-26 22:06:34 +00:00
|
|
|
int index = ~0;
|
2013-01-19 22:33:18 +00:00
|
|
|
for (int i = 0; i < esm.getIndex(); i++) {
|
2019-11-13 11:46:49 +00:00
|
|
|
const std::string candidate = allPlugins->at(i).getContext().filename;
|
2012-11-25 16:19:29 +00:00
|
|
|
std::string fnamecandidate = boost::filesystem::path(candidate).filename().string();
|
2014-05-26 15:34:36 +00:00
|
|
|
if (Misc::StringUtils::ciEqual(fname, fnamecandidate)) {
|
2012-11-25 16:19:29 +00:00
|
|
|
index = i;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2013-01-19 22:33:18 +00:00
|
|
|
if (index == (int)~0) {
|
2012-11-25 16:19:29 +00:00
|
|
|
// Tried to load a parent file that has not been loaded yet. This is bad,
|
|
|
|
// the launcher should have taken care of this.
|
2013-12-16 10:39:24 +00:00
|
|
|
std::string fstring = "File " + esm.getName() + " asks for parent file " + masters[j].name
|
2012-11-25 16:19:29 +00:00
|
|
|
+ ", but it has not been loaded yet. Please check your load order.";
|
|
|
|
esm.fail(fstring);
|
|
|
|
}
|
|
|
|
mast.index = index;
|
|
|
|
}
|
|
|
|
|
2010-08-06 13:19:39 +00:00
|
|
|
// Loop through all records
|
|
|
|
while(esm.hasMoreRecs())
|
2010-05-17 15:35:42 +00:00
|
|
|
{
|
2012-10-01 15:17:04 +00:00
|
|
|
ESM::NAME n = esm.getRecName();
|
2010-08-06 13:19:39 +00:00
|
|
|
esm.getRecHeader();
|
2010-05-17 15:35:42 +00:00
|
|
|
|
2010-08-06 13:19:39 +00:00
|
|
|
// Look up the record type.
|
2016-05-07 17:32:51 +00:00
|
|
|
std::map<int, StoreBase *>::iterator it = mStores.find(n.intval);
|
2012-11-05 14:09:14 +00:00
|
|
|
|
|
|
|
if (it == mStores.end()) {
|
2016-05-07 17:32:51 +00:00
|
|
|
if (n.intval == ESM::REC_INFO) {
|
2014-05-30 22:36:37 +00:00
|
|
|
if (dialogue)
|
|
|
|
{
|
|
|
|
dialogue->readInfo(esm, esm.getIndex() != 0);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2018-08-14 19:05:43 +00:00
|
|
|
Log(Debug::Error) << "Error: info record without dialog";
|
2010-08-06 13:19:39 +00:00
|
|
|
esm.skipRecord();
|
|
|
|
}
|
2016-05-07 17:32:51 +00:00
|
|
|
} else if (n.intval == ESM::REC_MGEF) {
|
2012-11-05 14:09:14 +00:00
|
|
|
mMagicEffects.load (esm);
|
2016-05-07 17:32:51 +00:00
|
|
|
} else if (n.intval == ESM::REC_SKIL) {
|
2012-11-05 14:09:14 +00:00
|
|
|
mSkills.load (esm);
|
2014-09-13 18:48:24 +00:00
|
|
|
}
|
2016-05-07 17:32:51 +00:00
|
|
|
else if (n.intval==ESM::REC_FILT || n.intval == ESM::REC_DBGP)
|
2014-09-13 18:48:24 +00:00
|
|
|
{
|
|
|
|
// ignore project file only records
|
|
|
|
esm.skipRecord();
|
|
|
|
}
|
|
|
|
else {
|
2014-05-18 14:29:24 +00:00
|
|
|
std::stringstream error;
|
|
|
|
error << "Unknown record: " << n.toString();
|
|
|
|
throw std::runtime_error(error.str());
|
2010-08-06 13:19:39 +00:00
|
|
|
}
|
2012-11-05 14:09:14 +00:00
|
|
|
} else {
|
2015-07-12 12:20:22 +00:00
|
|
|
RecordId id = it->second->load(esm);
|
|
|
|
if (id.mIsDeleted)
|
2015-07-08 18:26:20 +00:00
|
|
|
{
|
2015-07-12 12:20:22 +00:00
|
|
|
it->second->eraseStatic(id.mId);
|
2015-07-08 18:26:20 +00:00
|
|
|
continue;
|
2014-06-07 17:21:37 +00:00
|
|
|
}
|
|
|
|
|
2016-05-07 17:32:51 +00:00
|
|
|
if (n.intval==ESM::REC_DIAL) {
|
2015-07-12 12:20:22 +00:00
|
|
|
dialogue = const_cast<ESM::Dialogue*>(mDialogs.find(id.mId));
|
2012-11-05 14:09:14 +00:00
|
|
|
} else {
|
2010-08-06 13:19:39 +00:00
|
|
|
dialogue = 0;
|
2012-11-05 14:09:14 +00:00
|
|
|
}
|
2010-08-06 13:19:39 +00:00
|
|
|
}
|
2015-03-08 00:07:29 +00:00
|
|
|
listener->setProgress(static_cast<size_t>(esm.getFileOffset() / (float)esm.getFileSize() * 1000));
|
2010-05-17 15:35:42 +00:00
|
|
|
}
|
|
|
|
}
|
2012-10-01 15:17:04 +00:00
|
|
|
|
2018-06-09 16:47:17 +00:00
|
|
|
void ESMStore::setUp(bool validateRecords)
|
2012-11-05 14:09:14 +00:00
|
|
|
{
|
2015-12-11 14:55:45 +00:00
|
|
|
mIds.clear();
|
|
|
|
|
|
|
|
std::map<int, StoreBase *>::iterator storeIt = mStores.begin();
|
|
|
|
for (; storeIt != mStores.end(); ++storeIt) {
|
2015-12-11 15:59:13 +00:00
|
|
|
storeIt->second->setUp();
|
|
|
|
|
2015-12-11 14:55:45 +00:00
|
|
|
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;
|
|
|
|
}
|
2012-11-05 14:09:14 +00:00
|
|
|
}
|
|
|
|
mSkills.setUp();
|
|
|
|
mMagicEffects.setUp();
|
|
|
|
mAttributes.setUp();
|
2014-10-19 15:45:18 +00:00
|
|
|
mDialogs.setUp();
|
2018-06-09 16:47:17 +00:00
|
|
|
|
|
|
|
if (validateRecords)
|
|
|
|
validate();
|
|
|
|
}
|
|
|
|
|
|
|
|
void ESMStore::validate()
|
|
|
|
{
|
|
|
|
// Cache first class from store - we will use it if current class is not found
|
|
|
|
std::string defaultCls = "";
|
|
|
|
Store<ESM::Class>::iterator it = mClasses.begin();
|
|
|
|
if (it != mClasses.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
|
2019-03-31 15:54:12 +00:00
|
|
|
std::vector<ESM::NPC> npcsToReplace;
|
2018-06-09 16:47:17 +00:00
|
|
|
for (ESM::NPC npc : mNpcs)
|
|
|
|
{
|
|
|
|
bool changed = false;
|
|
|
|
|
|
|
|
const std::string npcFaction = npc.mFaction;
|
|
|
|
if (!npcFaction.empty())
|
|
|
|
{
|
|
|
|
const ESM::Faction *fact = mFactions.search(npcFaction);
|
|
|
|
if (!fact)
|
|
|
|
{
|
2018-08-14 19:05:43 +00:00
|
|
|
Log(Debug::Verbose) << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent faction '" << npc.mFaction << "', ignoring it.";
|
2019-06-18 11:47:10 +00:00
|
|
|
npc.mFaction.clear();
|
|
|
|
npc.mNpdt.mRank = 0;
|
2018-06-09 16:47:17 +00:00
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string npcClass = npc.mClass;
|
|
|
|
if (!npcClass.empty())
|
|
|
|
{
|
|
|
|
const ESM::Class *cls = mClasses.search(npcClass);
|
|
|
|
if (!cls)
|
|
|
|
{
|
2018-08-14 19:05:43 +00:00
|
|
|
Log(Debug::Verbose) << "NPC '" << npc.mId << "' (" << npc.mName << ") has nonexistent class '" << npc.mClass << "', using '" << defaultCls << "' class as replacement.";
|
2018-06-09 16:47:17 +00:00
|
|
|
npc.mClass = defaultCls;
|
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (changed)
|
2019-03-31 15:54:12 +00:00
|
|
|
npcsToReplace.push_back(npc);
|
2018-06-09 16:47:17 +00:00
|
|
|
}
|
|
|
|
|
2019-03-31 15:54:12 +00:00
|
|
|
for (const ESM::NPC &npc : npcsToReplace)
|
2018-06-09 16:47:17 +00:00
|
|
|
{
|
|
|
|
mNpcs.eraseStatic(npc.mId);
|
|
|
|
mNpcs.insertStatic(npc);
|
|
|
|
}
|
2019-03-31 15:54:12 +00:00
|
|
|
|
|
|
|
// 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;
|
2019-05-31 08:23:58 +00:00
|
|
|
auto iter = spell.mEffects.mList.begin();
|
|
|
|
while (iter != spell.mEffects.mList.end())
|
2019-03-31 15:54:12 +00:00
|
|
|
{
|
2019-05-31 08:23:58 +00:00
|
|
|
const ESM::MagicEffect* mgef = mMagicEffects.search(iter->mEffectID);
|
|
|
|
if (!mgef)
|
|
|
|
{
|
2019-10-17 20:45:27 +00:00
|
|
|
Log(Debug::Verbose) << "Spell '" << spell.mId << "' has an invalid effect (index " << iter->mEffectID << ") present. Dropping the effect.";
|
2019-05-31 08:23:58 +00:00
|
|
|
iter = spell.mEffects.mList.erase(iter);
|
|
|
|
changed = true;
|
2019-03-31 15:54:12 +00:00
|
|
|
continue;
|
2019-05-31 08:23:58 +00:00
|
|
|
}
|
2019-03-31 15:54:12 +00:00
|
|
|
|
|
|
|
if (mgef->mData.mFlags & ESM::MagicEffect::TargetSkill)
|
|
|
|
{
|
2019-05-31 08:23:58 +00:00
|
|
|
if (iter->mAttribute != -1)
|
2019-03-31 15:54:12 +00:00
|
|
|
{
|
2019-05-31 08:23:58 +00:00
|
|
|
iter->mAttribute = -1;
|
|
|
|
Log(Debug::Verbose) << ESM::MagicEffect::effectIdToString(iter->mEffectID) <<
|
2019-10-17 20:45:27 +00:00
|
|
|
" effect of spell '" << spell.mId << "' has an attribute argument present. Dropping the argument.";
|
2019-03-31 15:54:12 +00:00
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (mgef->mData.mFlags & ESM::MagicEffect::TargetAttribute)
|
|
|
|
{
|
2019-05-31 08:23:58 +00:00
|
|
|
if (iter->mSkill != -1)
|
2019-03-31 15:54:12 +00:00
|
|
|
{
|
2019-05-31 08:23:58 +00:00
|
|
|
iter->mSkill = -1;
|
|
|
|
Log(Debug::Verbose) << ESM::MagicEffect::effectIdToString(iter->mEffectID) <<
|
2019-10-17 20:45:27 +00:00
|
|
|
" effect of spell '" << spell.mId << "' has a skill argument present. Dropping the argument.";
|
2019-03-31 15:54:12 +00:00
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
}
|
2019-05-31 08:23:58 +00:00
|
|
|
else if (iter->mSkill != -1 || iter->mAttribute != -1)
|
2019-03-31 15:54:12 +00:00
|
|
|
{
|
2019-05-31 08:23:58 +00:00
|
|
|
iter->mSkill = -1;
|
|
|
|
iter->mAttribute = -1;
|
|
|
|
Log(Debug::Verbose) << ESM::MagicEffect::effectIdToString(iter->mEffectID) <<
|
2019-10-17 20:45:27 +00:00
|
|
|
" effect of spell '" << spell.mId << "' has argument(s) present. Dropping the argument(s).";
|
2019-03-31 15:54:12 +00:00
|
|
|
changed = true;
|
|
|
|
}
|
2019-05-31 08:23:58 +00:00
|
|
|
|
|
|
|
++iter;
|
2019-03-31 15:54:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if (changed)
|
|
|
|
spellsToReplace.emplace_back(spell);
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const ESM::Spell &spell : spellsToReplace)
|
|
|
|
{
|
|
|
|
mSpells.eraseStatic(spell.mId);
|
|
|
|
mSpells.insertStatic(spell);
|
|
|
|
}
|
2012-11-05 14:09:14 +00:00
|
|
|
}
|
|
|
|
|
2013-12-07 12:17:28 +00:00
|
|
|
int ESMStore::countSavedGameRecords() const
|
|
|
|
{
|
2014-05-10 22:32:22 +00:00
|
|
|
return 1 // DYNA (dynamic name counter)
|
|
|
|
+mPotions.getDynamicSize()
|
2013-12-07 12:17:28 +00:00
|
|
|
+mArmors.getDynamicSize()
|
|
|
|
+mBooks.getDynamicSize()
|
|
|
|
+mClasses.getDynamicSize()
|
|
|
|
+mClothes.getDynamicSize()
|
|
|
|
+mEnchants.getDynamicSize()
|
|
|
|
+mNpcs.getDynamicSize()
|
|
|
|
+mSpells.getDynamicSize()
|
2014-12-17 00:05:32 +00:00
|
|
|
+mWeapons.getDynamicSize()
|
|
|
|
+mCreatureLists.getDynamicSize()
|
|
|
|
+mItemLists.getDynamicSize();
|
2013-12-07 12:17:28 +00:00
|
|
|
}
|
|
|
|
|
2014-04-28 09:29:57 +00:00
|
|
|
void ESMStore::write (ESM::ESMWriter& writer, Loading::Listener& progress) const
|
2013-12-07 12:17:28 +00:00
|
|
|
{
|
2014-05-10 22:32:22 +00:00
|
|
|
writer.startRecord(ESM::REC_DYNA);
|
|
|
|
writer.startSubRecord("COUN");
|
|
|
|
writer.writeT(mDynamicCount);
|
|
|
|
writer.endRecord("COUN");
|
|
|
|
writer.endRecord(ESM::REC_DYNA);
|
|
|
|
|
2014-04-28 09:29:57 +00:00
|
|
|
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);
|
2014-12-17 00:05:32 +00:00
|
|
|
mItemLists.write (writer, progress);
|
|
|
|
mCreatureLists.write (writer, progress);
|
2013-12-07 12:17:28 +00:00
|
|
|
}
|
|
|
|
|
2015-01-22 18:04:59 +00:00
|
|
|
bool ESMStore::readRecord (ESM::ESMReader& reader, uint32_t type)
|
2013-12-07 12:17:28 +00:00
|
|
|
{
|
|
|
|
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:
|
2014-01-18 14:06:58 +00:00
|
|
|
case ESM::REC_NPC_:
|
2014-12-17 00:05:32 +00:00
|
|
|
case ESM::REC_LEVI:
|
|
|
|
case ESM::REC_LEVC:
|
2013-12-07 12:17:28 +00:00
|
|
|
|
2015-01-08 15:17:13 +00:00
|
|
|
{
|
2015-12-11 14:55:45 +00:00
|
|
|
mStores[type]->read (reader);
|
2015-01-08 15:17:13 +00:00
|
|
|
}
|
2014-01-18 14:06:58 +00:00
|
|
|
|
|
|
|
if (type==ESM::REC_NPC_)
|
|
|
|
{
|
|
|
|
// NPC record will always be last and we know that there can be only one
|
2015-12-11 14:55:45 +00:00
|
|
|
// dynamic NPC record (player) -> We are done here with dynamic record loading
|
2014-01-18 14:06:58 +00:00
|
|
|
setUp();
|
|
|
|
|
|
|
|
const ESM::NPC *player = mNpcs.find ("player");
|
|
|
|
|
|
|
|
if (!mRaces.find (player->mRace) ||
|
|
|
|
!mClasses.find (player->mClass))
|
2014-05-12 19:04:02 +00:00
|
|
|
throw std::runtime_error ("Invalid player record (race or class unavailable");
|
2014-01-18 14:06:58 +00:00
|
|
|
}
|
|
|
|
|
2013-12-07 12:17:28 +00:00
|
|
|
return true;
|
|
|
|
|
2014-05-10 22:32:22 +00:00
|
|
|
case ESM::REC_DYNA:
|
|
|
|
reader.getSubNameIs("COUN");
|
|
|
|
reader.getHT(mDynamicCount);
|
|
|
|
return true;
|
|
|
|
|
2013-12-07 12:17:28 +00:00
|
|
|
default:
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-10-01 15:17:04 +00:00
|
|
|
} // end namespace
|