Merge remote-tracking branch 'mark76/multiple_esm_esp' into next

actorid
Marc Zinnschlag 12 years ago
commit f19fbaa293

@ -209,7 +209,10 @@ void loadCell(ESM::Cell &cell, ESM::ESMReader &esm, Arguments& info)
bool save = (info.mode == "clone"); bool save = (info.mode == "clone");
// Skip back to the beginning of the reference list // Skip back to the beginning of the reference list
cell.restore(esm); // FIXME: Changes to the references backend required to support multiple plugins have
// almost certainly broken this following line. I'll leave it as is for now, so that
// the compiler does not complain.
cell.restore(esm, 0);
// Loop through all the references // Loop through all the references
ESM::CellRef ref; ESM::CellRef ref;

@ -210,18 +210,31 @@ void OMW::Engine::setCell (const std::string& cellName)
// Set master file (esm) // Set master file (esm)
// - If the given name does not have an extension, ".esm" is added automatically // - If the given name does not have an extension, ".esm" is added automatically
// - Currently OpenMW only supports one master at the same time.
void OMW::Engine::addMaster (const std::string& master) void OMW::Engine::addMaster (const std::string& master)
{ {
assert (mMaster.empty()); mMaster.push_back(master);
mMaster = master; std::string &str = mMaster.back();
// Append .esm if not already there // Append .esm if not already there
std::string::size_type sep = mMaster.find_last_of ("."); std::string::size_type sep = str.find_last_of (".");
if (sep == std::string::npos)
{
str += ".esm";
}
}
// Add plugin file (esp)
void OMW::Engine::addPlugin (const std::string& plugin)
{
mPlugins.push_back(plugin);
std::string &str = mPlugins.back();
// Append .esp if not already there
std::string::size_type sep = str.find_last_of (".");
if (sep == std::string::npos) if (sep == std::string::npos)
{ {
mMaster += ".esm"; str += ".esp";
} }
} }
@ -326,13 +339,13 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings)
MWGui::CursorReplace replacer; MWGui::CursorReplace replacer;
// Create the world // Create the world
mEnvironment.setWorld (new MWWorld::World (*mOgre, mFileCollections, mMaster, mEnvironment.setWorld( new MWWorld::World (*mOgre, mFileCollections, mMaster, mPlugins,
mResDir, mCfgMgr.getCachePath(), mNewGame, mEncoder, mFallbackMap, mResDir, mCfgMgr.getCachePath(), mNewGame, mEncoder, mFallbackMap,
mActivationDistanceOverride)); mActivationDistanceOverride));
//Load translation data //Load translation data
mTranslationDataStorage.setEncoder(mEncoder); mTranslationDataStorage.setEncoder(mEncoder);
mTranslationDataStorage.loadTranslationData(mFileCollections, mMaster); mTranslationDataStorage.loadTranslationData(mFileCollections, mMaster[0]);
// Create window manager - this manages all the MW-specific GUI windows // Create window manager - this manages all the MW-specific GUI windows
MWScript::registerExtensions (mExtensions); MWScript::registerExtensions (mExtensions);

@ -67,7 +67,8 @@ namespace OMW
boost::filesystem::path mResDir; boost::filesystem::path mResDir;
OEngine::Render::OgreRenderer *mOgre; OEngine::Render::OgreRenderer *mOgre;
std::string mCellName; std::string mCellName;
std::string mMaster; std::vector<std::string> mMaster;
std::vector<std::string> mPlugins;
int mFpsLevel; int mFpsLevel;
bool mDebug; bool mDebug;
bool mVerboseScripts; bool mVerboseScripts;
@ -132,9 +133,12 @@ namespace OMW
/// Set master file (esm) /// Set master file (esm)
/// - If the given name does not have an extension, ".esm" is added automatically /// - If the given name does not have an extension, ".esm" is added automatically
/// - Currently OpenMW only supports one master at the same time.
void addMaster(const std::string& master); void addMaster(const std::string& master);
/// Same as "addMaster", but for plugin files (esp)
/// - If the given name does not have an extension, ".esp" is added automatically
void addPlugin(const std::string& plugin);
/// Enable fps counter /// Enable fps counter
void showFPS(int level); void showFPS(int level);

@ -211,18 +211,21 @@ bool parseOptions (int argc, char** argv, OMW::Engine& engine, Files::Configurat
master.push_back("Morrowind"); master.push_back("Morrowind");
} }
if (master.size() > 1) StringsVector plugin = variables["plugin"].as<StringsVector>();
// Removed check for 255 files, which would be the hard-coded limit in Morrowind.
// I'll keep the following variable in, maybe we can use it for something different.
// Say, a feedback like "loading file x/cnt".
// Commenting this out for now to silence compiler warning.
//int cnt = master.size() + plugin.size();
// Prepare loading master/plugin files (i.e. send filenames to engine)
for (std::vector<std::string>::size_type i = 0; i < master.size(); i++)
{ {
std::cout engine.addMaster(master[i]);
<< "Ignoring all but the first master file (multiple master files not yet supported)."
<< std::endl;
} }
engine.addMaster(master[0]); for (std::vector<std::string>::size_type i = 0; i < plugin.size(); i++)
StringsVector plugin = variables["plugin"].as<StringsVector>();
if (!plugin.empty())
{ {
std::cout << "Ignoring plugin files (plugins not yet supported)." << std::endl; engine.addPlugin(plugin[i]);
} }
// startup-settings // startup-settings

@ -105,7 +105,7 @@ namespace MWBase
virtual const MWWorld::ESMStore& getStore() const = 0; virtual const MWWorld::ESMStore& getStore() const = 0;
virtual ESM::ESMReader& getEsmReader() = 0; virtual std::vector<ESM::ESMReader>& getEsmReader() = 0;
virtual MWWorld::LocalScripts& getLocalScripts() = 0; virtual MWWorld::LocalScripts& getLocalScripts() = 0;

@ -144,7 +144,7 @@ namespace MWRender
std::map<uint16_t, int> indexes; std::map<uint16_t, int> indexes;
initTerrainTextures(&terrainData, cellX, cellY, initTerrainTextures(&terrainData, cellX, cellY,
x * numTextures, y * numTextures, x * numTextures, y * numTextures,
numTextures, indexes); numTextures, indexes, land->mPlugin);
if (mTerrainGroup.getTerrain(terrainX, terrainY) == NULL) if (mTerrainGroup.getTerrain(terrainX, terrainY) == NULL)
{ {
@ -213,8 +213,13 @@ namespace MWRender
void TerrainManager::initTerrainTextures(Terrain::ImportData* terrainData, void TerrainManager::initTerrainTextures(Terrain::ImportData* terrainData,
int cellX, int cellY, int cellX, int cellY,
int fromX, int fromY, int size, int fromX, int fromY, int size,
std::map<uint16_t, int>& indexes) std::map<uint16_t, int>& indexes, size_t plugin)
{ {
// FIXME: In a multiple esm configuration, we have multiple palettes. Since this code
// crosses cell boundaries, we no longer have a unique terrain palette. Instead, we need
// to adopt the following code for a dynamic palette. And this is evil - the current design
// does not work well for this task...
assert(terrainData != NULL && "Must have valid terrain data"); assert(terrainData != NULL && "Must have valid terrain data");
assert(fromX >= 0 && fromY >= 0 && assert(fromX >= 0 && fromY >= 0 &&
"Can't get a terrain texture on terrain outside the current cell"); "Can't get a terrain texture on terrain outside the current cell");
@ -227,12 +232,20 @@ namespace MWRender
// //
//If we don't sort the ltex indexes, the splatting order may differ between //If we don't sort the ltex indexes, the splatting order may differ between
//cells which may lead to inconsistent results when shading between cells //cells which may lead to inconsistent results when shading between cells
int num = MWBase::Environment::get().getWorld()->getStore().get<ESM::LandTexture>().getSize(plugin);
std::set<uint16_t> ltexIndexes; std::set<uint16_t> ltexIndexes;
for ( int y = fromY - 1; y < fromY + size + 1; y++ ) for ( int y = fromY - 1; y < fromY + size + 1; y++ )
{ {
for ( int x = fromX - 1; x < fromX + size + 1; x++ ) for ( int x = fromX - 1; x < fromX + size + 1; x++ )
{ {
ltexIndexes.insert(getLtexIndexAt(cellX, cellY, x, y)); int idx = getLtexIndexAt(cellX, cellY, x, y);
// This is a quick hack to prevent the program from trying to fetch textures
// from a neighboring cell, which might originate from a different plugin,
// and use a separate texture palette. Right now, we simply cast it to the
// default texture (i.e. 0).
if (idx > num)
idx = 0;
ltexIndexes.insert(idx);
} }
} }
@ -244,7 +257,7 @@ namespace MWRender
iter != ltexIndexes.end(); iter != ltexIndexes.end();
++iter ) ++iter )
{ {
const uint16_t ltexIndex = *iter; uint16_t ltexIndex = *iter;
//this is the base texture, so we can ignore this at present //this is the base texture, so we can ignore this at present
if ( ltexIndex == baseTexture ) if ( ltexIndex == baseTexture )
{ {
@ -260,8 +273,11 @@ namespace MWRender
const MWWorld::Store<ESM::LandTexture> &ltexStore = const MWWorld::Store<ESM::LandTexture> &ltexStore =
MWBase::Environment::get().getWorld()->getStore().get<ESM::LandTexture>(); MWBase::Environment::get().getWorld()->getStore().get<ESM::LandTexture>();
assert( (int)ltexStore.getSize() >= (int)ltexIndex - 1 && // NOTE: using the quick hack above, we should no longer end up with textures indices
"LAND.VTEX must be within the bounds of the LTEX array"); // that are out of bounds. However, I haven't updated the test to a multi-palette
// system yet. We probably need more work here, so we skip it for now.
//assert( (int)ltexStore.getSize() >= (int)ltexIndex - 1 &&
//"LAND.VTEX must be within the bounds of the LTEX array");
std::string texture; std::string texture;
if ( ltexIndex == 0 ) if ( ltexIndex == 0 )
@ -270,7 +286,7 @@ namespace MWRender
} }
else else
{ {
texture = ltexStore.search(ltexIndex-1)->mTexture; texture = ltexStore.search(ltexIndex-1, plugin)->mTexture;
//TODO this is needed due to MWs messed up texture handling //TODO this is needed due to MWs messed up texture handling
texture = texture.substr(0, texture.rfind(".")) + ".dds"; texture = texture.substr(0, texture.rfind(".")) + ".dds";
} }

@ -75,7 +75,7 @@ namespace MWRender{
void initTerrainTextures(Ogre::Terrain::ImportData* terrainData, void initTerrainTextures(Ogre::Terrain::ImportData* terrainData,
int cellX, int cellY, int cellX, int cellY,
int fromX, int fromY, int size, int fromX, int fromY, int size,
std::map<uint16_t, int>& indexes); std::map<uint16_t, int>& indexes, size_t plugin);
/** /**
* Creates the blend (splatting maps) for the given terrain from the ltex data. * Creates the blend (splatting maps) for the given terrain from the ltex data.

@ -84,7 +84,7 @@ MWWorld::Ptr MWWorld::Cells::getPtrAndCache (const std::string& name, Ptr::CellS
return ptr; return ptr;
} }
MWWorld::Cells::Cells (const MWWorld::ESMStore& store, ESM::ESMReader& reader) MWWorld::Cells::Cells (const MWWorld::ESMStore& store, std::vector<ESM::ESMReader>& reader)
: mStore (store), mReader (reader), : mStore (store), mReader (reader),
mIdCache (20, std::pair<std::string, Ptr::CellStore *> ("", (Ptr::CellStore*)0)), /// \todo make cache size configurable mIdCache (20, std::pair<std::string, Ptr::CellStore *> ("", (Ptr::CellStore*)0)), /// \todo make cache size configurable
mIdCacheIndex (0) mIdCacheIndex (0)
@ -119,6 +119,7 @@ MWWorld::Ptr::CellStore *MWWorld::Cells::getExterior (int x, int y)
if (result->second.mState!=Ptr::CellStore::State_Loaded) if (result->second.mState!=Ptr::CellStore::State_Loaded)
{ {
// Multiple plugin support for landscape data is much easier than for references. The last plugin wins.
result->second.load (mStore, mReader); result->second.load (mStore, mReader);
fillContainers (result->second); fillContainers (result->second);
} }

@ -2,6 +2,7 @@
#define GAME_MWWORLD_CELLS_H #define GAME_MWWORLD_CELLS_H
#include <map> #include <map>
#include <list>
#include <string> #include <string>
#include "ptr.hpp" #include "ptr.hpp"
@ -19,7 +20,7 @@ namespace MWWorld
class Cells class Cells
{ {
const MWWorld::ESMStore& mStore; const MWWorld::ESMStore& mStore;
ESM::ESMReader& mReader; std::vector<ESM::ESMReader>& mReader;
std::map<std::string, CellStore> mInteriors; std::map<std::string, CellStore> mInteriors;
std::map<std::pair<int, int>, CellStore> mExteriors; std::map<std::pair<int, int>, CellStore> mExteriors;
std::vector<std::pair<std::string, CellStore *> > mIdCache; std::vector<std::pair<std::string, CellStore *> > mIdCache;
@ -36,7 +37,7 @@ namespace MWWorld
public: public:
Cells (const MWWorld::ESMStore& store, ESM::ESMReader& reader); Cells (const MWWorld::ESMStore& store, std::vector<ESM::ESMReader>& reader);
///< \todo pass the dynamic part of the ESMStore isntead (once it is written) of the whole ///< \todo pass the dynamic part of the ESMStore isntead (once it is written) of the whole
/// world /// world

@ -10,13 +10,48 @@
namespace MWWorld namespace MWWorld
{ {
template <typename X>
void CellRefList<X>::load(ESM::CellRef &ref, const MWWorld::ESMStore &esmStore)
{
// Get existing reference, in case we need to overwrite it.
typename std::list<LiveRef>::iterator iter = std::find(mList.begin(), mList.end(), ref.mRefnum);
// Skip this when reference was deleted.
// TODO: Support respawning references, in this case, we need to track it somehow.
if (ref.mDeleted) {
mList.erase(iter);
return;
}
// for throwing exception on unhandled record type
const MWWorld::Store<X> &store = esmStore.get<X>();
const X *ptr = store.search(ref.mRefID);
/// \note no longer redundant - changed to Store<X>::search(), don't throw
/// an exception on miss, try to continue (that's how MW does it, anyway)
if (ptr == NULL) {
std::cout << "Warning: could not resolve cell reference " << ref.mRefID << ", trying to continue anyway" << std::endl;
} else {
if (iter != mList.end())
*iter = LiveRef(ref, ptr);
else
mList.push_back(LiveRef(ref, ptr));
}
}
template<typename X> bool operator==(const LiveCellRef<X>& ref, int pRefnum)
{
return (ref.mRef.mRefnum == pRefnum);
}
CellStore::CellStore (const ESM::Cell *cell) CellStore::CellStore (const ESM::Cell *cell)
: mCell (cell), mState (State_Unloaded) : mCell (cell), mState (State_Unloaded)
{ {
mWaterLevel = cell->mWater; mWaterLevel = cell->mWater;
} }
void CellStore::load (const MWWorld::ESMStore &store, ESM::ESMReader &esm) void CellStore::load (const MWWorld::ESMStore &store, std::vector<ESM::ESMReader> &esm)
{ {
if (mState!=State_Loaded) if (mState!=State_Loaded)
{ {
@ -31,7 +66,7 @@ namespace MWWorld
} }
} }
void CellStore::preload (const MWWorld::ESMStore &store, ESM::ESMReader &esm) void CellStore::preload (const MWWorld::ESMStore &store, std::vector<ESM::ESMReader> &esm)
{ {
if (mState==State_Unloaded) if (mState==State_Unloaded)
{ {
@ -41,57 +76,126 @@ namespace MWWorld
} }
} }
void CellStore::listRefs(const MWWorld::ESMStore &store, ESM::ESMReader &esm) void CellStore::listRefs(const MWWorld::ESMStore &store, std::vector<ESM::ESMReader> &esm)
{ {
assert (mCell); assert (mCell);
if (mCell->mContext.filename.empty()) if (mCell->mContextList.size() == 0)
return; // this is a dynamically generated cell -> skipping. return; // this is a dynamically generated cell -> skipping.
// Reopen the ESM reader and seek to the right position. // Load references from all plugins that do something with this cell.
mCell->restore (esm); for (size_t i = 0; i < mCell->mContextList.size(); i++)
ESM::CellRef ref;
// Get each reference in turn
while (mCell->getNextRef (esm, ref))
{ {
std::string lowerCase = Misc::StringUtils::lowerCase (ref.mRefID); // Reopen the ESM reader and seek to the right position.
int index = mCell->mContextList.at(i).index;
mCell->restore (esm[index], i);
ESM::CellRef ref;
mIds.push_back (lowerCase); // Get each reference in turn
while (mCell->getNextRef (esm[index], ref))
{
std::string lowerCase = Misc::StringUtils::lowerCase (ref.mRefID);
if (ref.mDeleted) {
// Right now, don't do anything. Where is "listRefs" actually used, anyway?
// Skipping for now...
continue;
}
mIds.push_back (lowerCase);
}
} }
std::sort (mIds.begin(), mIds.end()); std::sort (mIds.begin(), mIds.end());
} }
void CellStore::loadRefs(const MWWorld::ESMStore &store, ESM::ESMReader &esm) void CellStore::loadRefs(const MWWorld::ESMStore &store, std::vector<ESM::ESMReader> &esm)
{ {
assert (mCell); assert (mCell);
if (mCell->mContext.filename.empty()) if (mCell->mContextList.size() == 0)
return; // this is a dynamically generated cell -> skipping. return; // this is a dynamically generated cell -> skipping.
// Reopen the ESM reader and seek to the right position. // Load references from all plugins that do something with this cell.
mCell->restore(esm); for (size_t i = 0; i < mCell->mContextList.size(); i++)
{
// Reopen the ESM reader and seek to the right position.
int index = mCell->mContextList.at(i).index;
mCell->restore (esm[index], i);
ESM::CellRef ref;
// Get each reference in turn
while(mCell->getNextRef(esm[index], ref))
{
// Don't load reference if it was moved to a different cell.
std::string lowerCase = Misc::StringUtils::lowerCase(ref.mRefID);
ESM::MovedCellRefTracker::const_iterator iter = std::find(mCell->mMovedRefs.begin(), mCell->mMovedRefs.end(), ref.mRefnum);
if (iter != mCell->mMovedRefs.end()) {
continue;
}
int rec = store.find(ref.mRefID);
ref.mRefID = lowerCase;
/* We can optimize this further by storing the pointer to the
record itself in store.all, so that we don't need to look it
up again here. However, never optimize. There are infinite
opportunities to do that later.
*/
switch(rec)
{
case ESM::REC_ACTI: mActivators.load(ref, store); break;
case ESM::REC_ALCH: mPotions.load(ref, store); break;
case ESM::REC_APPA: mAppas.load(ref, store); break;
case ESM::REC_ARMO: mArmors.load(ref, store); break;
case ESM::REC_BOOK: mBooks.load(ref, store); break;
case ESM::REC_CLOT: mClothes.load(ref, store); break;
case ESM::REC_CONT: mContainers.load(ref, store); break;
case ESM::REC_CREA: mCreatures.load(ref, store); break;
case ESM::REC_DOOR: mDoors.load(ref, store); break;
case ESM::REC_INGR: mIngreds.load(ref, store); break;
case ESM::REC_LEVC: mCreatureLists.load(ref, store); break;
case ESM::REC_LEVI: mItemLists.load(ref, store); break;
case ESM::REC_LIGH: mLights.load(ref, store); break;
case ESM::REC_LOCK: mLockpicks.load(ref, store); break;
case ESM::REC_MISC: mMiscItems.load(ref, store); break;
case ESM::REC_NPC_: mNpcs.load(ref, store); break;
case ESM::REC_PROB: mProbes.load(ref, store); break;
case ESM::REC_REPA: mRepairs.load(ref, store); break;
case ESM::REC_STAT: mStatics.load(ref, store); break;
case ESM::REC_WEAP: mWeapons.load(ref, store); break;
ESM::CellRef ref; case 0: std::cout << "Cell reference " + ref.mRefID + " not found!\n"; break;
default:
std::cout << "WARNING: Ignoring reference '" << ref.mRefID << "' of unhandled type\n";
}
}
}
// Get each reference in turn // Load moved references, from separately tracked list.
while(mCell->getNextRef(esm, ref)) for (ESM::CellRefTracker::const_iterator it = mCell->mLeasedRefs.begin(); it != mCell->mLeasedRefs.end(); it++)
{ {
std::string lowerCase = Misc::StringUtils::lowerCase(ref.mRefID); // Doesn't seem to work in one line... huh? Too sleepy to check...
ESM::CellRef &ref = const_cast<ESM::CellRef&>(*it);
//ESM::CellRef &ref = const_cast<ESM::CellRef&>(it->second);
int rec = store.find(ref.mRefID); std::string lowerCase;
std::transform (ref.mRefID.begin(), ref.mRefID.end(), std::back_inserter (lowerCase),
(int(*)(int)) std::tolower);
int rec = store.find(ref.mRefID);
ref.mRefID = lowerCase; ref.mRefID = lowerCase;
/* We can optimize this further by storing the pointer to the /* We can optimize this further by storing the pointer to the
record itself in store.all, so that we don't need to look it record itself in store.all, so that we don't need to look it
up again here. However, never optimize. There are infinite up again here. However, never optimize. There are infinite
opportunities to do that later. opportunities to do that later.
*/ */
switch(rec) switch(rec)
{ {
case ESM::REC_ACTI: mActivators.load(ref, store); break; case ESM::REC_ACTI: mActivators.load(ref, store); break;
case ESM::REC_ALCH: mPotions.load(ref, store); break; case ESM::REC_ALCH: mPotions.load(ref, store); break;
case ESM::REC_APPA: mAppas.load(ref, store); break; case ESM::REC_APPA: mAppas.load(ref, store); break;
@ -113,10 +217,11 @@ namespace MWWorld
case ESM::REC_STAT: mStatics.load(ref, store); break; case ESM::REC_STAT: mStatics.load(ref, store); break;
case ESM::REC_WEAP: mWeapons.load(ref, store); break; case ESM::REC_WEAP: mWeapons.load(ref, store); break;
case 0: std::cout << "Cell reference " + ref.mRefID + " not found!\n"; break; case 0: std::cout << "Cell reference " + ref.mRefID + " not found!\n"; break;
default: default:
std::cout << "WARNING: Ignoring reference '" << ref.mRefID << "' of unhandled type\n"; std::cout << "WARNING: Ignoring reference '" << ref.mRefID << "' of unhandled type\n";
} }
} }
} }
} }

@ -3,17 +3,18 @@
#include <components/esm/records.hpp> #include <components/esm/records.hpp>
#include <list> #include <deque>
#include <algorithm> #include <algorithm>
#include "refdata.hpp" #include "refdata.hpp"
#include "esmstore.hpp" #include "esmstore.hpp"
struct C;
namespace MWWorld namespace MWWorld
{ {
class Ptr; class Ptr;
class ESMStore; class ESMStore;
/// A reference to one object (of any type) in a cell. /// A reference to one object (of any type) in a cell.
/// ///
/// Constructing this with a CellRef instance in the constructor means that /// Constructing this with a CellRef instance in the constructor means that
@ -42,6 +43,8 @@ namespace MWWorld
/// runtime-data /// runtime-data
RefData mData; RefData mData;
}; };
template<typename X> bool operator==(const LiveCellRef<X>& ref, int pRefnum);
/// A list of cell references /// A list of cell references
template <typename X> template <typename X>
@ -51,21 +54,14 @@ namespace MWWorld
typedef std::list<LiveRef> List; typedef std::list<LiveRef> List;
List mList; List mList;
/// Searches for reference of appropriate type in given ESMStore. // Search for the given reference in the given reclist from
/// If reference exists, loads it into container, throws an exception // ESMStore. Insert the reference into the list if a match is
/// on miss // found. If not, throw an exception.
void load(ESM::CellRef &ref, const MWWorld::ESMStore &esmStore) // Moved to cpp file, as we require a custom compare operator for it,
{ // and the build will fail with an ugly three-way cyclic header dependence
// for throwing exception on unhandled record type // so we need to pass the instantiation of the method to the lnker, when
const MWWorld::Store<X> &store = esmStore.get<X>(); // all methods are known.
const X *ptr = store.find(ref.mRefID); void load(ESM::CellRef &ref, const MWWorld::ESMStore &esmStore);
/// \note redundant because Store<X>::find() throws exception on miss
if (ptr == NULL) {
throw std::runtime_error("Error resolving cell reference " + ref.mRefID);
}
mList.push_back(LiveRef(ref, ptr));
}
LiveRef *find (const std::string& name) LiveRef *find (const std::string& name)
{ {
@ -124,9 +120,9 @@ namespace MWWorld
CellRefList<ESM::Static> mStatics; CellRefList<ESM::Static> mStatics;
CellRefList<ESM::Weapon> mWeapons; CellRefList<ESM::Weapon> mWeapons;
void load (const MWWorld::ESMStore &store, ESM::ESMReader &esm); void load (const MWWorld::ESMStore &store, std::vector<ESM::ESMReader> &esm);
void preload (const MWWorld::ESMStore &store, ESM::ESMReader &esm); void preload (const MWWorld::ESMStore &store, std::vector<ESM::ESMReader> &esm);
/// Call functor (ref) for each reference. functor must return a bool. Returning /// Call functor (ref) for each reference. functor must return a bool. Returning
/// false will abort the iteration. /// false will abort the iteration.
@ -185,9 +181,9 @@ namespace MWWorld
} }
/// Run through references and store IDs /// Run through references and store IDs
void listRefs(const MWWorld::ESMStore &store, ESM::ESMReader &esm); void listRefs(const MWWorld::ESMStore &store, std::vector<ESM::ESMReader> &esm);
void loadRefs(const MWWorld::ESMStore &store, ESM::ESMReader &esm); void loadRefs(const MWWorld::ESMStore &store, std::vector<ESM::ESMReader> &esm);
}; };
} }

@ -3,6 +3,8 @@
#include <set> #include <set>
#include <iostream> #include <iostream>
#include <boost/filesystem/v3/operations.hpp>
namespace MWWorld namespace MWWorld
{ {
@ -25,6 +27,33 @@ void ESMStore::load(ESM::ESMReader &esm)
ESM::Dialogue *dialogue = 0; ESM::Dialogue *dialogue = 0;
// 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.
int index = ~0;
const ESM::ESMReader::MasterList &masters = esm.getMasters();
std::vector<ESM::ESMReader> *allPlugins = esm.getGlobalReaderList();
for (size_t j = 0; j < masters.size(); j++) {
ESM::MasterData &mast = const_cast<ESM::MasterData&>(masters[j]);
std::string fname = mast.name;
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 (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 " + fname + " asks for parent file " + masters[j].name
+ ", but it has not been loaded yet. Please check your load order.";
esm.fail(fstring);
}
mast.index = index;
}
// Loop through all records // Loop through all records
while(esm.hasMoreRecs()) while(esm.hasMoreRecs())
{ {
@ -55,12 +84,21 @@ void ESMStore::load(ESM::ESMReader &esm)
} else { } else {
// Load it // Load it
std::string id = esm.getHNOString("NAME"); std::string id = esm.getHNOString("NAME");
// ... unless it got deleted! This means that the following record
// has been deleted, and trying to load it using standard assumptions
// on the structure will (probably) fail.
if (esm.isNextSub("DELE")) {
esm.skipRecord();
it->second->eraseStatic(id);
continue;
}
it->second->load(esm, id); it->second->load(esm, id);
if (n.val==ESM::REC_DIAL) { if (n.val==ESM::REC_DIAL) {
// dirty hack, but it is better than non-const search() // dirty hack, but it is better than non-const search()
// or friends // or friends
dialogue = &mDialogs.mStatic.back(); //dialogue = &mDialogs.mStatic.back();
dialogue = const_cast<ESM::Dialogue*>(mDialogs.find(id));
assert (dialogue->mId == id); assert (dialogue->mId == id);
} else { } else {
dialogue = 0; dialogue = 0;
@ -84,7 +122,6 @@ void ESMStore::load(ESM::ESMReader &esm)
cout << *it << " "; cout << *it << " ";
cout << endl; cout << endl;
*/ */
setUp();
} }
void ESMStore::setUp() void ESMStore::setUp()
@ -100,12 +137,11 @@ void ESMStore::setUp()
ESM::NPC item; ESM::NPC item;
item.mId = "player"; item.mId = "player";
std::vector<ESM::NPC>::iterator pIt = const ESM::NPC *pIt = mNpcs.find("player");
std::lower_bound(mNpcs.mStatic.begin(), mNpcs.mStatic.end(), item, RecordCmp()); assert(pIt != NULL);
assert(pIt != mNpcs.mStatic.end() && pIt->mId == "player");
mNpcs.insert(*pIt); mNpcs.insert(*pIt);
mNpcs.mStatic.erase(pIt); mNpcs.eraseStatic(pIt->mId);
} }
} // end namespace } // end namespace

@ -94,6 +94,9 @@ namespace MWWorld
ESMStore() ESMStore()
: mDynamicCount(0) : mDynamicCount(0)
{ {
// Cell store needs access to this for tracking moved references
mCells.mEsmStore = this;
mStores[ESM::REC_ACTI] = &mActivators; mStores[ESM::REC_ACTI] = &mActivators;
mStores[ESM::REC_ALCH] = &mPotions; mStores[ESM::REC_ALCH] = &mPotions;
mStores[ESM::REC_APPA] = &mAppas; mStores[ESM::REC_APPA] = &mAppas;
@ -168,7 +171,8 @@ namespace MWWorld
return ptr; return ptr;
} }
private: // This method must be called once, after loading all master/plugin files. This can only be done
// from the outside, so it must be public.
void setUp(); void setUp();
}; };

@ -0,0 +1,65 @@
#include "store.hpp"
namespace MWWorld {
void Store<ESM::Cell>::load(ESM::ESMReader &esm, const std::string &id)
{
// Don't automatically assume that a new cell must be spawned. Multiple plugins write to the same cell,
// and we merge all this data into one Cell object. However, we can't simply search for the cell id,
// as many exterior cells do not have a name. Instead, we need to search by (x,y) coordinates - and they
// are not available until both cells have been loaded! So first, proceed as usual.
// All cells have a name record, even nameless exterior cells.
std::string idLower = Misc::StringUtils::lowerCase(id);
ESM::Cell *cell = new ESM::Cell;
cell->mName = id;
// The cell itself takes care of some of the hairy details
cell->load(esm, *mEsmStore);
if(cell->mData.mFlags & ESM::Cell::Interior)
{
// Store interior cell by name, try to merge with existing parent data.
ESM::Cell *oldcell = const_cast<ESM::Cell*>(search(idLower));
if (oldcell) {
// push the new references on the list of references to manage
oldcell->mContextList.push_back(cell->mContextList.at(0));
// copy list into new cell
cell->mContextList = oldcell->mContextList;
// have new cell replace old cell
*oldcell = *cell;
} else
mInt[idLower] = *cell;
}
else
{
// Store exterior cells by grid position, try to merge with existing parent data.
ESM::Cell *oldcell = const_cast<ESM::Cell*>(search(cell->getGridX(), cell->getGridY()));
if (oldcell) {
// push the new references on the list of references to manage
oldcell->mContextList.push_back(cell->mContextList.at(0));
// copy list into new cell
cell->mContextList = oldcell->mContextList;
// merge lists of leased references, use newer data in case of conflict
for (ESM::MovedCellRefTracker::const_iterator it = cell->mMovedRefs.begin(); it != cell->mMovedRefs.end(); it++) {
// remove reference from current leased ref tracker and add it to new cell
ESM::MovedCellRefTracker::iterator itold = std::find(oldcell->mMovedRefs.begin(), oldcell->mMovedRefs.end(), it->mRefnum);
if (itold != oldcell->mMovedRefs.end()) {
ESM::MovedCellRef target0 = *itold;
ESM::Cell *wipecell = const_cast<ESM::Cell*>(search(target0.mTarget[0], target0.mTarget[1]));
ESM::CellRefTracker::iterator it_lease = std::find(wipecell->mLeasedRefs.begin(), wipecell->mLeasedRefs.end(), it->mRefnum);
wipecell->mLeasedRefs.erase(it_lease);
*itold = *it;
}
}
cell->mMovedRefs = oldcell->mMovedRefs;
// have new cell replace old cell
*oldcell = *cell;
} else
mExt[std::make_pair(cell->mData.mX, cell->mData.mY)] = *cell;
}
delete cell;
}
}

@ -19,6 +19,8 @@ namespace MWWorld
virtual int getSize() const = 0; virtual int getSize() const = 0;
virtual void load(ESM::ESMReader &esm, const std::string &id) = 0; virtual void load(ESM::ESMReader &esm, const std::string &id) = 0;
virtual bool eraseStatic(const std::string &id) {return false;}
}; };
template <class T> template <class T>
@ -85,7 +87,7 @@ namespace MWWorld
template <class T> template <class T>
class Store : public StoreBase class Store : public StoreBase
{ {
std::vector<T> mStatic; std::map<std::string, T> mStatic;
std::vector<T *> mShared; std::vector<T *> mShared;
std::map<std::string, T> mDynamic; std::map<std::string, T> mDynamic;
@ -107,11 +109,10 @@ namespace MWWorld
T item; T item;
item.mId = Misc::StringUtils::lowerCase(id); item.mId = Misc::StringUtils::lowerCase(id);
typename std::vector<T>::const_iterator it = typename std::map<std::string, T>::const_iterator it = mStatic.find(item.mId);
std::lower_bound(mStatic.begin(), mStatic.end(), item, RecordCmp());
if (it != mStatic.end() && Misc::StringUtils::ciEqual(it->second.mId, id)) {
if (it != mStatic.end() && Misc::StringUtils::ciEqual(it->mId, id)) { return &(it->second);
return &(*it);
} }
typename Dynamic::const_iterator dit = mDynamic.find(item.mId); typename Dynamic::const_iterator dit = mDynamic.find(item.mId);
@ -133,18 +134,19 @@ namespace MWWorld
} }
void load(ESM::ESMReader &esm, const std::string &id) { void load(ESM::ESMReader &esm, const std::string &id) {
mStatic.push_back(T()); std::string idLower = Misc::StringUtils::lowerCase(id);
mStatic.back().mId = Misc::StringUtils::lowerCase(id); mStatic[idLower] = T();
mStatic.back().load(esm); mStatic[idLower].mId = idLower;
mStatic[idLower].load(esm);
} }
void setUp() { void setUp() {
std::sort(mStatic.begin(), mStatic.end(), RecordCmp()); //std::sort(mStatic.begin(), mStatic.end(), RecordCmp());
mShared.reserve(mStatic.size()); mShared.reserve(mStatic.size());
typename std::vector<T>::iterator it = mStatic.begin(); typename std::map<std::string, T>::iterator it = mStatic.begin();
for (; it != mStatic.end(); ++it) { for (; it != mStatic.end(); ++it) {
mShared.push_back(&(*it)); mShared.push_back(&(it->second));
} }
} }
@ -181,6 +183,19 @@ namespace MWWorld
return ptr; return ptr;
} }
bool eraseStatic(const std::string &id) {
T item;
item.mId = Misc::StringUtils::lowerCase(id);
typename std::map<std::string, T>::iterator it = mStatic.find(item.mId);
if (it != mStatic.end() && Misc::StringUtils::ciEqual(it->second.mId, id)) {
mStatic.erase(it);
}
return true;
}
bool erase(const std::string &id) { bool erase(const std::string &id) {
std::string key = Misc::StringUtils::lowerCase(id); std::string key = Misc::StringUtils::lowerCase(id);
typename Dynamic::iterator it = mDynamic.find(key); typename Dynamic::iterator it = mDynamic.find(key);
@ -204,39 +219,48 @@ namespace MWWorld
template <> template <>
inline void Store<ESM::Dialogue>::load(ESM::ESMReader &esm, const std::string &id) { inline void Store<ESM::Dialogue>::load(ESM::ESMReader &esm, const std::string &id) {
mStatic.push_back(ESM::Dialogue()); std::string idLower = Misc::StringUtils::lowerCase(id);
mStatic.back().mId = id; mStatic[idLower] = ESM::Dialogue();
mStatic.back().load(esm); mStatic[idLower].mId = id; // don't smash case here, as this line is printed... I think
mStatic[idLower].load(esm);
} }
template <> template <>
inline void Store<ESM::Script>::load(ESM::ESMReader &esm, const std::string &id) { inline void Store<ESM::Script>::load(ESM::ESMReader &esm, const std::string &id) {
mStatic.push_back(ESM::Script()); ESM::Script scpt;
mStatic.back().load(esm); scpt.load(esm);
Misc::StringUtils::toLower(mStatic.back().mId); Misc::StringUtils::toLower(scpt.mId);
mStatic[scpt.mId] = scpt;
} }
template <> template <>
class Store<ESM::LandTexture> : public StoreBase class Store<ESM::LandTexture> : public StoreBase
{ {
std::vector<ESM::LandTexture> mStatic; // For multiple ESM/ESP files we need one list per file.
typedef std::vector<ESM::LandTexture> LandTextureList;
std::vector<LandTextureList> mStatic;
public: public:
Store<ESM::LandTexture>() { Store<ESM::LandTexture>() {
mStatic.reserve(128); mStatic.push_back(LandTextureList());
LandTextureList &ltexl = mStatic[0];
// More than enough to hold Morrowind.esm. Extra lists for plugins will we
// added on-the-fly in a different method.
ltexl.reserve(128);
} }
typedef std::vector<ESM::LandTexture>::const_iterator iterator; typedef std::vector<ESM::LandTexture>::const_iterator iterator;
const ESM::LandTexture *search(size_t index) const { const ESM::LandTexture *search(size_t index, size_t plugin) const {
if (index < mStatic.size()) { assert(plugin < mStatic.size());
return &mStatic.at(index); const LandTextureList &ltexl = mStatic[plugin];
}
return 0; assert(index < ltexl.size());
return &ltexl.at(index);
} }
const ESM::LandTexture *find(size_t index) const { const ESM::LandTexture *find(size_t index, size_t plugin) const {
const ESM::LandTexture *ptr = search(index); const ESM::LandTexture *ptr = search(index, plugin);
if (ptr == 0) { if (ptr == 0) {
std::ostringstream msg; std::ostringstream msg;
msg << "Land texture with index " << index << " not found"; msg << "Land texture with index " << index << " not found";
@ -249,23 +273,40 @@ namespace MWWorld
return mStatic.size(); return mStatic.size();
} }
void load(ESM::ESMReader &esm, const std::string &id) { int getSize(size_t plugin) const {
ESM::LandTexture ltex; assert(plugin < mStatic.size());
ltex.load(esm); return mStatic[plugin].size();
}
void load(ESM::ESMReader &esm, const std::string &id, size_t plugin) {
ESM::LandTexture lt;
lt.load(esm);
lt.mId = id;
if (ltex.mIndex >= (int) mStatic.size()) { // Make sure we have room for the structure
mStatic.resize(ltex.mIndex + 1); if (plugin >= mStatic.size()) {
mStatic.resize(plugin+1);
} }
mStatic[ltex.mIndex] = ltex; LandTextureList &ltexl = mStatic[plugin];
mStatic[ltex.mIndex].mId = id; if(lt.mIndex + 1 > (int)ltexl.size())
ltexl.resize(lt.mIndex+1);
// Store it
ltexl[lt.mIndex] = lt;
} }
iterator begin() const { void load(ESM::ESMReader &esm, const std::string &id) {
return mStatic.begin(); load(esm, id, esm.getIndex());
} }
iterator end() const { iterator begin(size_t plugin) const {
return mStatic.end(); assert(plugin < mStatic.size());
return mStatic[plugin].begin();
}
iterator end(size_t plugin) const {
assert(plugin < mStatic.size());
return mStatic[plugin].end();
} }
}; };
@ -357,19 +398,18 @@ namespace MWWorld
} }
}; };
std::vector<ESM::Cell> mInt; typedef std::map<std::string, ESM::Cell> DynamicInt;
std::vector<ESM::Cell> mExt; typedef std::map<std::pair<int, int>, ESM::Cell> DynamicExt;
DynamicInt mInt;
DynamicExt mExt;
std::vector<ESM::Cell *> mSharedInt; std::vector<ESM::Cell *> mSharedInt;
std::vector<ESM::Cell *> mSharedExt; std::vector<ESM::Cell *> mSharedExt;
typedef std::map<std::string, ESM::Cell> DynamicInt;
typedef std::map<std::pair<int, int>, ESM::Cell> DynamicExt;
DynamicInt mDynamicInt; DynamicInt mDynamicInt;
DynamicExt mDynamicExt; DynamicExt mDynamicExt;
const ESM::Cell *search(const ESM::Cell &cell) const { const ESM::Cell *search(const ESM::Cell &cell) const {
if (cell.isExterior()) { if (cell.isExterior()) {
return search(cell.getGridX(), cell.getGridY()); return search(cell.getGridX(), cell.getGridY());
@ -378,6 +418,8 @@ namespace MWWorld
} }
public: public:
ESMStore *mEsmStore;
typedef SharedIterator<ESM::Cell> iterator; typedef SharedIterator<ESM::Cell> iterator;
Store<ESM::Cell>() Store<ESM::Cell>()
@ -387,11 +429,10 @@ namespace MWWorld
ESM::Cell cell; ESM::Cell cell;
cell.mName = Misc::StringUtils::lowerCase(id); cell.mName = Misc::StringUtils::lowerCase(id);
std::vector<ESM::Cell>::const_iterator it = std::map<std::string, ESM::Cell>::const_iterator it = mInt.find(cell.mName);
std::lower_bound(mInt.begin(), mInt.end(), cell, RecordCmp());
if (it != mInt.end() && Misc::StringUtils::ciEqual(it->mName, id)) { if (it != mInt.end() && Misc::StringUtils::ciEqual(it->second.mName, id)) {
return &(*it); return &(it->second);
} }
DynamicInt::const_iterator dit = mDynamicInt.find(cell.mName); DynamicInt::const_iterator dit = mDynamicInt.find(cell.mName);
@ -406,20 +447,42 @@ namespace MWWorld
ESM::Cell cell; ESM::Cell cell;
cell.mData.mX = x, cell.mData.mY = y; cell.mData.mX = x, cell.mData.mY = y;
std::vector<ESM::Cell>::const_iterator it = std::pair<int, int> key(x, y);
std::lower_bound(mExt.begin(), mExt.end(), cell, ExtCmp()); std::map<std::pair<int, int>, ESM::Cell>::const_iterator it = mExt.find(key);
if (it != mExt.end()) {
return &(it->second);
}
if (it != mExt.end() && it->mData.mX == x && it->mData.mY == y) { DynamicExt::const_iterator dit = mDynamicExt.find(key);
return &(*it); if (dit != mDynamicExt.end()) {
return &dit->second;
} }
return 0;
}
const ESM::Cell *searchOrCreate(int x, int y) {
ESM::Cell cell;
cell.mData.mX = x, cell.mData.mY = y;
std::pair<int, int> key(x, y); std::pair<int, int> key(x, y);
std::map<std::pair<int, int>, ESM::Cell>::const_iterator it = mExt.find(key);
if (it != mExt.end()) {
return &(it->second);
}
DynamicExt::const_iterator dit = mDynamicExt.find(key); DynamicExt::const_iterator dit = mDynamicExt.find(key);
if (dit != mDynamicExt.end()) { if (dit != mDynamicExt.end()) {
return &dit->second; return &dit->second;
} }
return 0; ESM::Cell *newCell = new ESM::Cell;
newCell->mData.mX = x;
newCell->mData.mY = y;
mExt[std::make_pair(x, y)] = *newCell;
delete newCell;
return &mExt[std::make_pair(x, y)];
} }
const ESM::Cell *find(const std::string &id) const { const ESM::Cell *find(const std::string &id) const {
@ -443,33 +506,29 @@ namespace MWWorld
} }
void setUp() { void setUp() {
typedef std::vector<ESM::Cell>::iterator Iterator; //typedef std::vector<ESM::Cell>::iterator Iterator;
typedef std::map<std::pair<int, int>, ESM::Cell>::iterator ExtIterator;
typedef std::map<std::string, ESM::Cell>::iterator IntIterator;
std::sort(mInt.begin(), mInt.end(), RecordCmp()); //std::sort(mInt.begin(), mInt.end(), RecordCmp());
mSharedInt.reserve(mInt.size()); mSharedInt.reserve(mInt.size());
for (Iterator it = mInt.begin(); it != mInt.end(); ++it) { for (IntIterator it = mInt.begin(); it != mInt.end(); ++it) {
mSharedInt.push_back(&(*it)); mSharedInt.push_back(&(it->second));
} }
std::sort(mExt.begin(), mExt.end(), ExtCmp()); //std::sort(mExt.begin(), mExt.end(), ExtCmp());
mSharedExt.reserve(mExt.size()); mSharedExt.reserve(mExt.size());
for (Iterator it = mExt.begin(); it != mExt.end(); ++it) { for (ExtIterator it = mExt.begin(); it != mExt.end(); ++it) {
mSharedExt.push_back(&(*it)); mSharedExt.push_back(&(it->second));
}
}
void load(ESM::ESMReader &esm, const std::string &id) {
ESM::Cell cell;
cell.mName = id;
cell.load(esm);
if (cell.isExterior()) {
mExt.push_back(cell);
} else {
mInt.push_back(cell);
} }
} }
// HACK: Method implementation had to be moved to a separate cpp file, as we would otherwise get
// errors related to the compare operator used in std::find for ESM::MovedCellRefTracker::find.
// There some nasty three-way cyclic header dependency involved, which I could only fix by moving
// this method.
void load(ESM::ESMReader &esm, const std::string &id);
iterator intBegin() const { iterator intBegin() const {
return iterator(mSharedInt.begin()); return iterator(mSharedInt.begin());
} }

@ -55,7 +55,7 @@ namespace
for (iterator iter (refList.mList.begin()); iter!=refList.mList.end(); ++iter) for (iterator iter (refList.mList.begin()); iter!=refList.mList.end(); ++iter)
{ {
if(iter->mData.getCount() > 0 && iter->mData.getBaseNode()){ if (iter->mData.getCount() > 0 && iter->mData.getBaseNode()){
if (iter->mData.getHandle()==handle) if (iter->mData.getHandle()==handle)
{ {
return &*iter; return &*iter;
@ -171,7 +171,8 @@ namespace MWWorld
World::World (OEngine::Render::OgreRenderer& renderer, World::World (OEngine::Render::OgreRenderer& renderer,
const Files::Collections& fileCollections, const Files::Collections& fileCollections,
const std::string& master, const boost::filesystem::path& resDir, const boost::filesystem::path& cacheDir, bool newGame, const std::vector<std::string>& master, const std::vector<std::string>& plugins,
const boost::filesystem::path& resDir, const boost::filesystem::path& cacheDir, bool newGame,
ToUTF8::Utf8Encoder* encoder, std::map<std::string,std::string> fallbackMap, int mActivationDistanceOverride) ToUTF8::Utf8Encoder* encoder, std::map<std::string,std::string> fallbackMap, int mActivationDistanceOverride)
: mPlayer (0), mLocalScripts (mStore), mGlobalVariables (0), : mPlayer (0), mLocalScripts (mStore), mGlobalVariables (0),
mSky (true), mCells (mStore, mEsm), mSky (true), mCells (mStore, mEsm),
@ -184,14 +185,42 @@ namespace MWWorld
mWeatherManager = new MWWorld::WeatherManager(mRendering); mWeatherManager = new MWWorld::WeatherManager(mRendering);
boost::filesystem::path masterPath (fileCollections.getCollection (".esm").getPath (master)); int idx = 0;
// NOTE: We might need to reserve one more for the running game / save.
std::cout << "Loading ESM " << masterPath.string() << "\n"; mEsm.resize(master.size() + plugins.size());
for (std::vector<std::string>::size_type i = 0; i < master.size(); i++, idx++)
// This parses the ESM file and loads a sample cell {
mEsm.setEncoder(encoder); boost::filesystem::path masterPath (fileCollections.getCollection (".esm").getPath (master[i]));
mEsm.open (masterPath.string());
mStore.load (mEsm); std::cout << "Loading ESM " << masterPath.string() << "\n";
// This parses the ESM file
ESM::ESMReader lEsm;
lEsm.setEncoder(encoder);
lEsm.setIndex(idx);
lEsm.setGlobalReaderList(&mEsm);
lEsm.open (masterPath.string());
mEsm[idx] = lEsm;
mStore.load (mEsm[idx]);
}
for (std::vector<std::string>::size_type i = 0; i < plugins.size(); i++, idx++)
{
boost::filesystem::path pluginPath (fileCollections.getCollection (".esp").getPath (plugins[i]));
std::cout << "Loading ESP " << pluginPath.string() << "\n";
// This parses the ESP file
ESM::ESMReader lEsm;
lEsm.setEncoder(encoder);
lEsm.setIndex(idx);
lEsm.setGlobalReaderList(&mEsm);
lEsm.open (pluginPath.string());
mEsm[idx] = lEsm;
mStore.load (mEsm[idx]);
}
mStore.setUp();
mPlayer = new MWWorld::Player (mStore.get<ESM::NPC>().find ("player"), *this); mPlayer = new MWWorld::Player (mStore.get<ESM::NPC>().find ("player"), *this);
mRendering->attachCameraTo(mPlayer->getPlayer()); mRendering->attachCameraTo(mPlayer->getPlayer());
@ -270,7 +299,7 @@ namespace MWWorld
return mStore; return mStore;
} }
ESM::ESMReader& World::getEsmReader() std::vector<ESM::ESMReader>& World::getEsmReader()
{ {
return mEsm; return mEsm;
} }
@ -1233,8 +1262,8 @@ namespace MWWorld
std::vector<World::DoorMarker> result; std::vector<World::DoorMarker> result;
MWWorld::CellRefList<ESM::Door>& doors = cell->mDoors; MWWorld::CellRefList<ESM::Door>& doors = cell->mDoors;
std::list< MWWorld::LiveCellRef<ESM::Door> >& refList = doors.mList; CellRefList<ESM::Door>::List& refList = doors.mList;
for (std::list< MWWorld::LiveCellRef<ESM::Door> >::iterator it = refList.begin(); it != refList.end(); ++it) for (CellRefList<ESM::Door>::List::iterator it = refList.begin(); it != refList.end(); ++it)
{ {
MWWorld::LiveCellRef<ESM::Door>& ref = *it; MWWorld::LiveCellRef<ESM::Door>& ref = *it;

@ -54,7 +54,7 @@ namespace MWWorld
MWWorld::Scene *mWorldScene; MWWorld::Scene *mWorldScene;
MWWorld::Player *mPlayer; MWWorld::Player *mPlayer;
ESM::ESMReader mEsm; std::vector<ESM::ESMReader> mEsm;
MWWorld::ESMStore mStore; MWWorld::ESMStore mStore;
LocalScripts mLocalScripts; LocalScripts mLocalScripts;
MWWorld::Globals *mGlobalVariables; MWWorld::Globals *mGlobalVariables;
@ -113,7 +113,8 @@ namespace MWWorld
World (OEngine::Render::OgreRenderer& renderer, World (OEngine::Render::OgreRenderer& renderer,
const Files::Collections& fileCollections, const Files::Collections& fileCollections,
const std::string& master, const boost::filesystem::path& resDir, const boost::filesystem::path& cacheDir, bool newGame, const std::vector<std::string>& master, const std::vector<std::string>& plugins,
const boost::filesystem::path& resDir, const boost::filesystem::path& cacheDir, bool newGame,
ToUTF8::Utf8Encoder* encoder, std::map<std::string,std::string> fallbackMap, int mActivationDistanceOverride); ToUTF8::Utf8Encoder* encoder, std::map<std::string,std::string> fallbackMap, int mActivationDistanceOverride);
virtual ~World(); virtual ~World();
@ -143,7 +144,7 @@ namespace MWWorld
virtual const MWWorld::ESMStore& getStore() const; virtual const MWWorld::ESMStore& getStore() const;
virtual ESM::ESMReader& getEsmReader(); virtual std::vector<ESM::ESMReader>& getEsmReader();
virtual LocalScripts& getLocalScripts(); virtual LocalScripts& getLocalScripts();

@ -89,6 +89,7 @@ struct MasterData
{ {
std::string name; std::string name;
uint64_t size; uint64_t size;
int index; // Position of the parent file in the global list of loaded files
}; };
// Data that is only present in save game files // Data that is only present in save game files
@ -113,6 +114,10 @@ struct ESM_Context
size_t leftFile; size_t leftFile;
NAME recName, subName; NAME recName, subName;
HEDRstruct header; HEDRstruct header;
// When working with multiple esX files, we will generate lists of all files that
// actually contribute to a specific cell. Therefore, we need to store the index
// of the file belonging to this contest. See CellStore::(list/load)refs for details.
int index;
// True if subName has been read but not used. // True if subName has been read but not used.
bool subCached; bool subCached;

@ -78,6 +78,17 @@ public:
void openRaw(const std::string &file); void openRaw(const std::string &file);
// This is a quick hack for multiple esm/esp files. Each plugin introduces its own
// terrain palette, but ESMReader does not pass a reference to the correct plugin
// to the individual load() methods. This hack allows to pass this reference
// indirectly to the load() method.
int mIdx;
void setIndex(const int index) {mIdx = index; mCtx.index = index;}
const int getIndex() {return mIdx;}
void setGlobalReaderList(std::vector<ESMReader> *list) {mGlobalReaderList = list;}
std::vector<ESMReader> *getGlobalReaderList() {return mGlobalReaderList;}
/************************************************************************* /*************************************************************************
* *
* Medium-level reading shortcuts * Medium-level reading shortcuts
@ -110,6 +121,14 @@ public:
getHT(x); getHT(x);
} }
template <typename X>
void getHNOT(X &x, const char* name, int size)
{
assert(sizeof(X) == size);
if(isNextSub(name))
getHT(x);
}
int64_t getHNLong(const char *name); int64_t getHNLong(const char *name);
// Get data of a given type/size, including subrecord header // Get data of a given type/size, including subrecord header
@ -251,6 +270,7 @@ private:
SaveData mSaveData; SaveData mSaveData;
MasterList mMasters; MasterList mMasters;
std::vector<ESMReader> *mGlobalReaderList;
ToUTF8::Utf8Encoder* mEncoder; ToUTF8::Utf8Encoder* mEncoder;
}; };
} }

@ -2,13 +2,29 @@
#include <string> #include <string>
#include <sstream> #include <sstream>
#include <list>
#include <boost/concept_check.hpp>
#include "esmreader.hpp" #include "esmreader.hpp"
#include "esmwriter.hpp" #include "esmwriter.hpp"
#include <apps/openmw/mwworld/store.hpp>
#include <apps/openmw/mwworld/cellstore.hpp>
namespace ESM namespace ESM
{ {
/// Some overloaded copare operators.
bool operator==(const MovedCellRef& ref, int pRefnum)
{
return (ref.mRefnum == pRefnum);
}
bool operator==(const CellRef& ref, int pRefnum)
{
return (ref.mRefnum == pRefnum);
}
void CellRef::save(ESMWriter &esm) void CellRef::save(ESMWriter &esm)
{ {
esm.writeHNT("FRMR", mRefnum); esm.writeHNT("FRMR", mRefnum);
@ -63,10 +79,11 @@ void CellRef::save(ESMWriter &esm)
} }
} }
void Cell::load(ESMReader &esm) void Cell::load(ESMReader &esm, MWWorld::ESMStore &store)
{ {
// Ignore this for now, it might mean we should delete the entire // Ignore this for now, it might mean we should delete the entire
// cell? // cell?
// TODO: treat the special case "another plugin moved this ref, but we want to delete it"!
if (esm.isNextSub("DELE")) { if (esm.isNextSub("DELE")) {
esm.skipHSub(); esm.skipHSub();
} }
@ -109,9 +126,37 @@ void Cell::load(ESMReader &esm)
if (esm.isNextSub("NAM0")) { if (esm.isNextSub("NAM0")) {
esm.getHT(mNAM0); esm.getHT(mNAM0);
} }
// preload moved references
while (esm.isNextSub("MVRF")) {
CellRef ref;
MovedCellRef cMRef;
getNextMVRF(esm, cMRef);
MWWorld::Store<ESM::Cell> &cStore = const_cast<MWWorld::Store<ESM::Cell>&>(store.get<ESM::Cell>());
ESM::Cell *cellAlt = const_cast<ESM::Cell*>(cStore.searchOrCreate(cMRef.mTarget[0], cMRef.mTarget[1]));
// Get regular moved reference data. Adapted from CellStore::loadRefs. Maybe we can optimize the following
// implementation when the oher implementation works as well.
getNextRef(esm, ref);
std::string lowerCase;
std::transform (ref.mRefID.begin(), ref.mRefID.end(), std::back_inserter (lowerCase),
(int(*)(int)) std::tolower);
// Add data required to make reference appear in the correct cell.
// We should not need to test for duplicates, as this part of the code is pre-cell merge.
mMovedRefs.push_back(cMRef);
// But there may be duplicates here!
ESM::CellRefTracker::iterator iter = std::find(cellAlt->mLeasedRefs.begin(), cellAlt->mLeasedRefs.end(), ref.mRefnum);
if (iter == cellAlt->mLeasedRefs.end())
cellAlt->mLeasedRefs.push_back(ref);
else
*iter = ref;
}
// Save position of the cell references and move on // Save position of the cell references and move on
mContext = esm.getContext(); mContextList.push_back(esm.getContext());
esm.skipRecord(); esm.skipRecord();
} }
@ -146,9 +191,9 @@ void Cell::save(ESMWriter &esm)
esm.writeHNT("NAM0", mNAM0); esm.writeHNT("NAM0", mNAM0);
} }
void Cell::restore(ESMReader &esm) const void Cell::restore(ESMReader &esm, int iCtx) const
{ {
esm.restoreContext(mContext); esm.restoreContext(mContextList[iCtx]);
} }
std::string Cell::getDescription() const std::string Cell::getDescription() const
@ -167,17 +212,61 @@ std::string Cell::getDescription() const
bool Cell::getNextRef(ESMReader &esm, CellRef &ref) bool Cell::getNextRef(ESMReader &esm, CellRef &ref)
{ {
// TODO: Try and document reference numbering, I don't think this has been done anywhere else.
if (!esm.hasMoreSubs()) if (!esm.hasMoreSubs())
return false; return false;
// NOTE: We should not need this check. It is a safety check until we have checked
// more plugins, and how they treat these moved references.
if (esm.isNextSub("MVRF")) {
esm.skipRecord(); // skip MVRF
esm.skipRecord(); // skip CNDT
// That should be it, I haven't seen any other fields yet.
}
esm.getHNT(ref.mRefnum, "FRMR"); esm.getHNT(ref.mRefnum, "FRMR");
ref.mRefID = esm.getHNString("NAME"); ref.mRefID = esm.getHNString("NAME");
// Identify references belonging to a parent file and adapt the ID accordingly.
int local = (ref.mRefnum & 0xff000000) >> 24;
size_t global = esm.getIndex() + 1;
if (local)
{
// If the most significant 8 bits are used, then this reference already exists.
// In this case, do not spawn a new reference, but overwrite the old one.
ref.mRefnum &= 0x00ffffff; // delete old plugin ID
const ESM::ESMReader::MasterList &masters = esm.getMasters();
global = masters[local-1].index + 1;
ref.mRefnum |= global << 24; // insert global plugin ID
}
else
{
// This is an addition by the present plugin. Set the corresponding plugin index.
ref.mRefnum |= global << 24; // insert global plugin ID
}
// getHNOT will not change the existing value if the subrecord is // getHNOT will not change the existing value if the subrecord is
// missing // missing
ref.mScale = 1.0; ref.mScale = 1.0;
esm.getHNOT(ref.mScale, "XSCL"); esm.getHNOT(ref.mScale, "XSCL");
// TODO: support loading references from saves, there are tons of keys not recognized yet.
// The following is just an incomplete list.
if (esm.isNextSub("ACTN"))
esm.skipHSub();
if (esm.isNextSub("STPR"))
esm.skipHSub();
if (esm.isNextSub("ACDT"))
esm.skipHSub();
if (esm.isNextSub("ACSC"))
esm.skipHSub();
if (esm.isNextSub("ACSL"))
esm.skipHSub();
if (esm.isNextSub("CHRD"))
esm.skipHSub();
else if (esm.isNextSub("CRED")) // ???
esm.skipHSub();
ref.mOwner = esm.getHNOString("ANAM"); ref.mOwner = esm.getHNOString("ANAM");
ref.mGlob = esm.getHNOString("BNAM"); ref.mGlob = esm.getHNOString("BNAM");
ref.mSoul = esm.getHNOString("XSOL"); ref.mSoul = esm.getHNOString("XSOL");
@ -215,17 +304,43 @@ bool Cell::getNextRef(ESMReader &esm, CellRef &ref)
esm.getHNOT(ref.mUnam, "UNAM"); esm.getHNOT(ref.mUnam, "UNAM");
esm.getHNOT(ref.mFltv, "FLTV"); esm.getHNOT(ref.mFltv, "FLTV");
esm.getHNT(ref.mPos, "DATA", 24); esm.getHNOT(ref.mPos, "DATA", 24);
// Number of references in the cell? Maximum once in each cell, // Number of references in the cell? Maximum once in each cell,
// but not always at the beginning, and not always right. In other // but not always at the beginning, and not always right. In other
// words, completely useless. // words, completely useless.
// Update: Well, maybe not completely useless. This might actually be
// number_of_references + number_of_references_moved_here_Across_boundaries,
// and could be helpful for collecting these weird moved references.
ref.mNam0 = 0; ref.mNam0 = 0;
if (esm.isNextSub("NAM0")) if (esm.isNextSub("NAM0"))
{ {
esm.getHT(ref.mNam0); esm.getHT(ref.mNam0);
//esm.getHNOT(NAM0, "NAM0"); //esm.getHNOT(NAM0, "NAM0");
} }
if (esm.isNextSub("DELE")) {
esm.skipHSub();
ref.mDeleted = 2; // Deleted, will not respawn.
// TODO: find out when references do respawn.
} else
ref.mDeleted = 0;
return true;
}
bool Cell::getNextMVRF(ESMReader &esm, MovedCellRef &mref)
{
esm.getHT(mref.mRefnum);
esm.getHNOT(mref.mTarget, "CNDT");
// Identify references belonging to a parent file and adapt the ID accordingly.
int local = (mref.mRefnum & 0xff000000) >> 24;
size_t global = esm.getIndex() + 1;
mref.mRefnum &= 0x00ffffff; // delete old plugin ID
const ESM::ESMReader::MasterList &masters = esm.getMasters();
global = masters[local-1].index + 1;
mref.mRefnum |= global << 24; // insert global plugin ID
return true; return true;
} }

@ -2,9 +2,18 @@
#define OPENMW_ESM_CELL_H #define OPENMW_ESM_CELL_H
#include <string> #include <string>
#include <vector>
#include "esmcommon.hpp" #include "esmcommon.hpp"
#include "defs.hpp" #include "defs.hpp"
#include "apps/openmw/mwbase/world.hpp"
/*
namespace MWWorld {
class ESMStore;
class CellStore;
}
*/
namespace ESM namespace ESM
{ {
@ -69,6 +78,9 @@ public:
// No idea - occurs ONCE in Morrowind.esm, for an activator // No idea - occurs ONCE in Morrowind.esm, for an activator
char mUnam; char mUnam;
// Track deleted references. 0 - not deleted, 1 - deleted, but respawns, 2 - deleted and does not respawn.
int mDeleted;
// Occurs in Tribunal.esm, eg. in the cell "Mournhold, Plaza // Occurs in Tribunal.esm, eg. in the cell "Mournhold, Plaza
// Brindisi Dorom", where it has the value 100. Also only for // Brindisi Dorom", where it has the value 100. Also only for
@ -82,6 +94,31 @@ public:
void save(ESMWriter &esm); void save(ESMWriter &esm);
}; };
/* Moved cell reference tracking object. This mainly stores the target cell
of the reference, so we can easily know where it has been moved when another
plugin tries to move it independently.
Unfortunately, we need to implement this here.
*/
class MovedCellRef
{
public:
int mRefnum;
// Target cell (if exterior)
int mTarget[2];
// TODO: Support moving references between exterior and interior cells!
// This may happen in saves, when an NPC follows the player. Tribunal
// introduces a henchman (which no one uses), so we may need this as well.
};
/// Overloaded copare operator used to search inside a list of cell refs.
bool operator==(const MovedCellRef& ref, int pRefnum);
bool operator==(const CellRef& ref, int pRefnum);
typedef std::list<MovedCellRef> MovedCellRefTracker;
typedef std::list<CellRef> CellRefTracker;
/* Cells hold data about objects, creatures, statics (rocks, walls, /* Cells hold data about objects, creatures, statics (rocks, walls,
buildings) and landscape (for exterior cells). Cells frequently buildings) and landscape (for exterior cells). Cells frequently
also has other associated LAND and PGRD records. Combined, all this also has other associated LAND and PGRD records. Combined, all this
@ -120,15 +157,24 @@ struct Cell
// Optional region name for exterior and quasi-exterior cells. // Optional region name for exterior and quasi-exterior cells.
std::string mRegion; std::string mRegion;
ESM_Context mContext; // File position std::vector<ESM_Context> mContextList; // File position; multiple positions for multiple plugin support
DATAstruct mData; DATAstruct mData;
AMBIstruct mAmbi; AMBIstruct mAmbi;
float mWater; // Water level float mWater; // Water level
bool mWaterInt; bool mWaterInt;
int mMapColor; int mMapColor;
int mNAM0; int mNAM0;
void load(ESMReader &esm); // References "leased" from another cell (i.e. a different cell
// introduced this ref, and it has been moved here by a plugin)
CellRefTracker mLeasedRefs;
MovedCellRefTracker mMovedRefs;
void load(ESMReader &esm, MWWorld::ESMStore &store);
// This method is left in for compatibility with esmtool. Parsing moved references currently requires
// passing ESMStore, bit it does not know about this parameter, so we do it this way.
void load(ESMReader &esm) {};
void save(ESMWriter &esm); void save(ESMWriter &esm);
bool isExterior() const bool isExterior() const
@ -151,7 +197,7 @@ struct Cell
// somewhere other than the file system, you need to pre-open the // somewhere other than the file system, you need to pre-open the
// ESMReader, and the filename must match the stored filename // ESMReader, and the filename must match the stored filename
// exactly. // exactly.
void restore(ESMReader &esm) const; void restore(ESMReader &esm, int iCtx) const;
std::string getDescription() const; std::string getDescription() const;
///< Return a short string describing the cell (mostly used for debugging/logging purpose) ///< Return a short string describing the cell (mostly used for debugging/logging purpose)
@ -163,6 +209,11 @@ struct Cell
reuse one memory location without blanking it between calls. reuse one memory location without blanking it between calls.
*/ */
static bool getNextRef(ESMReader &esm, CellRef &ref); static bool getNextRef(ESMReader &esm, CellRef &ref);
/* This fetches an MVRF record, which is used to track moved references.
* Since they are comparably rare, we use a separate method for this.
*/
static bool getNextMVRF(ESMReader &esm, MovedCellRef &mref);
}; };
} }
#endif #endif

@ -81,6 +81,7 @@ Land::~Land()
void Land::load(ESMReader &esm) void Land::load(ESMReader &esm)
{ {
mEsm = &esm; mEsm = &esm;
mPlugin = mEsm->getIndex();
// Get the grid location // Get the grid location
esm.getSubNameIs("INTV"); esm.getSubNameIs("INTV");

@ -23,6 +23,7 @@ struct Land
int mFlags; // Only first four bits seem to be used, don't know what int mFlags; // Only first four bits seem to be used, don't know what
// they mean. // they mean.
int mX, mY; // Map coordinates. int mX, mY; // Map coordinates.
int mPlugin; // Plugin index, used to reference the correct material palette.
// File context. This allows the ESM reader to be 'reset' to this // File context. This allows the ESM reader to be 'reset' to this
// location later when we are ready to load the full data set. // location later when we are ready to load the full data set.

@ -50,7 +50,7 @@ namespace sh
{ {
assert(mCurrentLanguage != Language_None); assert(mCurrentLanguage != Language_None);
bool anyShaderDirty = false; bool removeBinaryCache = false;
if (boost::filesystem::exists (mPlatform->getCacheFolder () + "/lastModified.txt")) if (boost::filesystem::exists (mPlatform->getCacheFolder () + "/lastModified.txt"))
{ {
@ -182,57 +182,33 @@ namespace sh
} }
} }
std::string sourceFile = mPlatform->getBasePath() + "/" + it->second->findChild("source")->getValue(); std::string sourceAbsolute = mPlatform->getBasePath() + "/" + it->second->findChild("source")->getValue();
std::string sourceRelative = it->second->findChild("source")->getValue();
ShaderSet newSet (it->second->findChild("type")->getValue(), cg_profile, hlsl_profile, ShaderSet newSet (it->second->findChild("type")->getValue(), cg_profile, hlsl_profile,
sourceFile, sourceAbsolute,
mPlatform->getBasePath(), mPlatform->getBasePath(),
it->first, it->first,
&mGlobalSettings); &mGlobalSettings);
int lastModified = boost::filesystem::last_write_time (boost::filesystem::path(sourceFile)); int lastModified = boost::filesystem::last_write_time (boost::filesystem::path(sourceAbsolute));
mShadersLastModifiedNew[sourceFile] = lastModified; mShadersLastModifiedNew[sourceRelative] = lastModified;
if (mShadersLastModified.find(sourceFile) != mShadersLastModified.end() if (mShadersLastModified.find(sourceRelative) != mShadersLastModified.end())
&& mShadersLastModified[sourceFile] != lastModified)
{ {
// delete any outdated shaders based on this shader set. if (mShadersLastModified[sourceRelative] != lastModified)
if ( boost::filesystem::exists(mPlatform->getCacheFolder())
&& boost::filesystem::is_directory(mPlatform->getCacheFolder()))
{ {
boost::filesystem::directory_iterator end_iter; // delete any outdated shaders based on this shader set
for( boost::filesystem::directory_iterator dir_iter(mPlatform->getCacheFolder()) ; dir_iter != end_iter ; ++dir_iter) removeCache (it->first);
{ // remove the whole binary cache (removing only the individual shaders does not seem to be possible at this point with OGRE)
if (boost::filesystem::is_regular_file(dir_iter->status()) ) removeBinaryCache = true;
{
boost::filesystem::path file = dir_iter->path();
std::string pathname = file.filename().string();
// get first part of filename, e.g. main_fragment_546457654 -> main_fragment
// there is probably a better method for this...
std::vector<std::string> tokens;
boost::split(tokens, pathname, boost::is_any_of("_"));
tokens.erase(--tokens.end());
std::string shaderName;
for (std::vector<std::string>::const_iterator vector_iter = tokens.begin(); vector_iter != tokens.end();)
{
shaderName += *(vector_iter++);
if (vector_iter != tokens.end())
shaderName += "_";
}
if (shaderName == it->first)
{
boost::filesystem::remove(file);
std::cout << "Removing outdated file: " << file << std::endl;
}
}
}
} }
anyShaderDirty = true;
} }
else
{
// if we get here, this is either the first run or a new shader file was added
// in both cases we can safely delete
removeCache (it->first);
}
mShaderSets.insert(std::make_pair(it->first, newSet)); mShaderSets.insert(std::make_pair(it->first, newSet));
} }
} }
@ -326,7 +302,7 @@ namespace sh
} }
} }
if (mPlatform->supportsShaderSerialization () && mReadMicrocodeCache && !anyShaderDirty) if (mPlatform->supportsShaderSerialization () && mReadMicrocodeCache && !removeBinaryCache)
{ {
std::string file = mPlatform->getCacheFolder () + "/shShaderCache.txt"; std::string file = mPlatform->getCacheFolder () + "/shShaderCache.txt";
if (boost::filesystem::exists(file)) if (boost::filesystem::exists(file))
@ -613,4 +589,41 @@ namespace sh
assert(m); assert(m);
m->createForConfiguration (configuration, 0); m->createForConfiguration (configuration, 0);
} }
void Factory::removeCache(const std::string& pattern)
{
if ( boost::filesystem::exists(mPlatform->getCacheFolder())
&& boost::filesystem::is_directory(mPlatform->getCacheFolder()))
{
boost::filesystem::directory_iterator end_iter;
for( boost::filesystem::directory_iterator dir_iter(mPlatform->getCacheFolder()) ; dir_iter != end_iter ; ++dir_iter)
{
if (boost::filesystem::is_regular_file(dir_iter->status()) )
{
boost::filesystem::path file = dir_iter->path();
std::string pathname = file.filename().string();
// get first part of filename, e.g. main_fragment_546457654 -> main_fragment
// there is probably a better method for this...
std::vector<std::string> tokens;
boost::split(tokens, pathname, boost::is_any_of("_"));
tokens.erase(--tokens.end());
std::string shaderName;
for (std::vector<std::string>::const_iterator vector_iter = tokens.begin(); vector_iter != tokens.end();)
{
shaderName += *(vector_iter++);
if (vector_iter != tokens.end())
shaderName += "_";
}
if (shaderName == pattern)
{
boost::filesystem::remove(file);
std::cout << "Removing outdated shader: " << file << std::endl;
}
}
}
}
}
} }

@ -202,6 +202,8 @@ namespace sh
MaterialInstance* findInstance (const std::string& name); MaterialInstance* findInstance (const std::string& name);
MaterialInstance* searchInstance (const std::string& name); MaterialInstance* searchInstance (const std::string& name);
void removeCache (const std::string& pattern);
}; };
} }

Loading…
Cancel
Save