diff --git a/apps/opencs/CMakeLists.txt b/apps/opencs/CMakeLists.txt index 41d44ca18c..30c78e710b 100644 --- a/apps/opencs/CMakeLists.txt +++ b/apps/opencs/CMakeLists.txt @@ -19,7 +19,7 @@ opencs_hdrs_noqt (model/doc opencs_units (model/world idtable idtableproxymodel regionmap data commanddispatcher idtablebase resourcetable nestedtableproxymodel idtree infotableproxymodel - pathgridcommands + pathgridcommands npcautocalc ) @@ -36,7 +36,7 @@ opencs_hdrs_noqt (model/world opencs_units (model/tools - tools reportmodel mergeoperation + tools reportmodel mergeoperation ) opencs_units_noqt (model/tools diff --git a/apps/opencs/model/world/data.cpp b/apps/opencs/model/world/data.cpp index dfb2d99d32..84cb505c00 100644 --- a/apps/opencs/model/world/data.cpp +++ b/apps/opencs/model/world/data.cpp @@ -10,10 +10,6 @@ #include #include -#include -#include -#include - #include "idtable.hpp" #include "idtree.hpp" #include "columnimp.hpp" @@ -23,70 +19,7 @@ #include "resourcetable.hpp" #include "nestedcoladapterimp.hpp" #include "npcstats.hpp" - -namespace -{ - class CSStore : public AutoCalc::StoreCommon - { - const CSMWorld::IdCollection& mGmstTable; - const CSMWorld::IdCollection& mSkillTable; - const CSMWorld::IdCollection& mMagicEffectTable; - const CSMWorld::NestedIdCollection& mSpells; - - public: - - CSStore(const CSMWorld::IdCollection& gmst, - const CSMWorld::IdCollection& skills, - const CSMWorld::IdCollection& magicEffects, - const CSMWorld::NestedIdCollection& spells) - : mGmstTable(gmst), mSkillTable(skills), mMagicEffectTable(magicEffects), mSpells(spells) - { } - - ~CSStore() {} - - virtual int findGmstInt(const std::string& name) const - { - return mGmstTable.getRecord(name).get().getInt(); - } - - virtual float findGmstFloat(const std::string& name) const - { - return mGmstTable.getRecord(name).get().getFloat(); - } - - virtual const ESM::Skill *findSkill(int index) const - { - // if the skill does not exist, throws std::runtime_error ("invalid ID: " + id) - return &mSkillTable.getRecord(ESM::Skill::indexToId(index)).get(); - } - - virtual const ESM::MagicEffect* findMagicEffect(int id) const - { - // if the magic effect does not exist, throws std::runtime_error ("invalid ID: " + id) - return &mMagicEffectTable.getRecord(ESM::MagicEffect::indexToId((short)id)).get(); - } - - virtual void getSpells(std::vector& spells) - { - // prepare data in a format used by OpenMW store - for (int index = 0; index < mSpells.getSize(); ++index) - spells.push_back(const_cast(&mSpells.getRecord(index).get())); - } - }; - - unsigned short autoCalculateMana(AutoCalc::StatsBase& stats) - { - return stats.getBaseAttribute(ESM::Attribute::Intelligence) * 2; - } - - unsigned short autoCalculateFatigue(AutoCalc::StatsBase& stats) - { - return stats.getBaseAttribute(ESM::Attribute::Strength) - + stats.getBaseAttribute(ESM::Attribute::Willpower) - + stats.getBaseAttribute(ESM::Attribute::Agility) - + stats.getBaseAttribute(ESM::Attribute::Endurance); - } -} +#include "npcautocalc.hpp" void CSMWorld::Data::addModel (QAbstractItemModel *model, UniversalId::Type type, bool update) { @@ -129,8 +62,9 @@ int CSMWorld::Data::count (RecordBase::State state, const CollectionBase& collec } CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourcesManager) -: mEncoder (encoding), mPathgrids (mCells), mReferenceables(self()), mRefs (mCells), - mResourcesManager (resourcesManager), mReader (0), mDialogue (0), mReaderIndex(0) +: mEncoder (encoding), mPathgrids (mCells), mReferenceables (self()), mRefs (mCells), + mResourcesManager (resourcesManager), mReader (0), mDialogue (0), mReaderIndex(0), + mNpcAutoCalc (0) { int index = 0; @@ -621,32 +555,18 @@ CSMWorld::Data::Data (ToUTF8::FromType encoding, const ResourcesManager& resourc CSMWorld::IdTree *objects = static_cast(getTableModel(UniversalId::Type_Referenceable)); - connect (gmsts, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (gmstDataChanged (const QModelIndex&, const QModelIndex&))); - connect (skills, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (skillDataChanged (const QModelIndex&, const QModelIndex&))); - connect (classes, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (classDataChanged (const QModelIndex&, const QModelIndex&))); - connect (races, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (raceDataChanged (const QModelIndex&, const QModelIndex&))); - connect (objects, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), - this, SLOT (npcDataChanged (const QModelIndex&, const QModelIndex&))); - connect (this, SIGNAL (updateNpcAutocalc (int, const std::string&)), - objects, SLOT (updateNpcAutocalc (int, const std::string&))); - connect (this, SIGNAL (cacheNpcStats (const std::string&, NpcStats*)), - this, SLOT (cacheNpcStatsEvent (const std::string&, NpcStats*))); + mNpcAutoCalc = new NpcAutoCalc (self(), gmsts, skills, classes, races, objects); mRefLoadCache.clear(); // clear here rather than startLoading() and continueLoading() for multiple content files } CSMWorld::Data::~Data() { - clearNpcStatsCache(); - for (std::vector::iterator iter (mModels.begin()); iter!=mModels.end(); ++iter) delete *iter; delete mReader; + delete mNpcAutoCalc; } const CSMWorld::IdCollection& CSMWorld::Data::getGlobals() const @@ -946,6 +866,11 @@ void CSMWorld::Data::setMetaData (const MetaData& metaData) mMetaData.setRecord (0, record); } +const CSMWorld::NpcAutoCalc& CSMWorld::Data::getNpcAutoCalc() const +{ + return *mNpcAutoCalc; +} + QAbstractItemModel *CSMWorld::Data::getTableModel (const CSMWorld::UniversalId& id) { std::map::iterator iter = mModelIndex.find (id.getType()); @@ -1297,271 +1222,3 @@ const CSMWorld::Data& CSMWorld::Data::self () { return *this; } - -void CSMWorld::Data::skillDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) -{ - // mData.mAttribute (affects attributes skill bonus autocalc) - // mData.mSpecialization (affects skills autocalc) - CSMWorld::IdTable *skillModel = - static_cast(getTableModel(CSMWorld::UniversalId::Type_Skill)); - - int attributeColumn = skillModel->findColumnIndex(CSMWorld::Columns::ColumnId_Attribute); - int specialisationColumn = skillModel->findColumnIndex(CSMWorld::Columns::ColumnId_Specialisation); - - if ((topLeft.column() <= attributeColumn && attributeColumn <= bottomRight.column()) - || (topLeft.column() <= specialisationColumn && specialisationColumn <= bottomRight.column())) - { - clearNpcStatsCache(); - - std::string empty; - emit updateNpcAutocalc(0/*all*/, empty); - } -} - -void CSMWorld::Data::classDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) -{ - // update autocalculated attributes/skills of every NPC with matching class - // - mData.mAttribute[2] - // - mData.mSkills[5][2] - // - mData.mSpecialization - CSMWorld::IdTable *classModel = - static_cast(getTableModel(CSMWorld::UniversalId::Type_Class)); - - int attribute1Column = classModel->findColumnIndex(CSMWorld::Columns::ColumnId_Attribute1); // +1 - int majorSkill1Column = classModel->findColumnIndex(CSMWorld::Columns::ColumnId_MajorSkill1); // +4 - int minorSkill1Column = classModel->findColumnIndex(CSMWorld::Columns::ColumnId_MinorSkill1); // +4 - int specialisationColumn = classModel->findColumnIndex(CSMWorld::Columns::ColumnId_Specialisation); - - if ((topLeft.column() > attribute1Column+1 || attribute1Column > bottomRight.column()) - && (topLeft.column() > majorSkill1Column+4 || majorSkill1Column > bottomRight.column()) - && (topLeft.column() > minorSkill1Column+4 || minorSkill1Column > bottomRight.column()) - && (topLeft.column() > specialisationColumn || specialisationColumn > bottomRight.column())) - { - return; - } - - // get the affected class - int idColumn = classModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); - for (int classRow = topLeft.row(); classRow <= bottomRight.row(); ++classRow) - { - clearNpcStatsCache(); - - std::string classId = - classModel->data(classModel->index(classRow, idColumn)).toString().toUtf8().constData(); - emit updateNpcAutocalc(1/*class*/, classId); - } -} - -void CSMWorld::Data::raceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) -{ - // affects racial bonus attributes & skills - // - mData.mAttributeValues[] - // - mData.mBonus[].mBonus - // - mPowers.mList[] - CSMWorld::IdTree *raceModel = - static_cast(getTableModel(CSMWorld::UniversalId::Type_Race)); - - int attrColumn = raceModel->findColumnIndex(CSMWorld::Columns::ColumnId_RaceAttributes); - int bonusColumn = raceModel->findColumnIndex(CSMWorld::Columns::ColumnId_RaceSkillBonus); - int powersColumn = raceModel->findColumnIndex(CSMWorld::Columns::ColumnId_PowerList); - - bool match = false; - int raceRow = topLeft.row(); - int raceEnd = bottomRight.row(); - if (topLeft.parent().isValid() && bottomRight.parent().isValid()) - { - if ((topLeft.parent().column() <= attrColumn && attrColumn <= bottomRight.parent().column()) - || (topLeft.parent().column() <= bonusColumn && bonusColumn <= bottomRight.parent().column()) - || (topLeft.parent().column() <= powersColumn && powersColumn <= bottomRight.parent().column())) - { - match = true; // TODO: check for specific nested column? - raceRow = topLeft.parent().row(); - raceEnd = bottomRight.parent().row(); - } - } - else - { - if ((topLeft.column() <= attrColumn && attrColumn <= bottomRight.column()) - || (topLeft.column() <= bonusColumn && bonusColumn <= bottomRight.column()) - || (topLeft.column() <= powersColumn && powersColumn <= bottomRight.column())) - { - match = true; // maybe the whole table changed - } - } - - if (!match) - return; - - // update autocalculated attributes/skills of every NPC with matching race - int idColumn = raceModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); - for (; raceRow <= raceEnd; ++raceRow) - { - clearNpcStatsCache(); - - std::string raceId = - raceModel->data(raceModel->index(raceRow, idColumn)).toString().toUtf8().constData(); - emit updateNpcAutocalc(2/*race*/, raceId); - } -} - -void CSMWorld::Data::npcDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) -{ - // TODO: for now always recalculate - clearNpcStatsCache(); -} - -void CSMWorld::Data::gmstDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) -{ - static const QStringList gmsts(QStringList()<< "fNPCbaseMagickaMult" << "fAutoSpellChance" - << "fEffectCostMult" << "iAutoSpellAlterationMax" << "iAutoSpellConjurationMax" - << "iAutoSpellDestructionMax" << "iAutoSpellIllusionMax" << "iAutoSpellMysticismMax" - << "iAutoSpellRestorationMax" << "iAutoSpellTimesCanCast" << "iAutoSpellAttSkillMin"); - - bool match = false; - for (int row = topLeft.row(); row <= bottomRight.row(); ++row) - { - if (gmsts.contains(mGmsts.getRecord(row).get().mId.c_str())) - { - match = true; - break; - } - } - - if (!match) - return; - - clearNpcStatsCache(); - - std::string empty; - emit updateNpcAutocalc(0/*all*/, empty); -} - -void CSMWorld::Data::clearNpcStatsCache () -{ - for (std::map::iterator it (mNpcStatCache.begin()); - it != mNpcStatCache.end(); ++it) - delete it->second; - - mNpcStatCache.clear(); -} - -CSMWorld::NpcStats* CSMWorld::Data::npcAutoCalculate(const ESM::NPC& npc) const -{ - CSMWorld::NpcStats * cachedStats = getCachedNpcData (npc.mId); - if (cachedStats) - return cachedStats; - - int raceIndex = mRaces.searchId(npc.mRace); - int classIndex = mClasses.searchId(npc.mClass); - // this can happen when creating a new game from scratch - if (raceIndex == -1 || classIndex == -1) - return 0; - - const ESM::Race *race = &mRaces.getRecord(raceIndex).get(); - const ESM::Class *class_ = &mClasses.getRecord(classIndex).get(); - - bool autoCalc = npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS; - short level = npc.mNpdt52.mLevel; - if (autoCalc) - level = npc.mNpdt12.mLevel; - - std::auto_ptr stats (new CSMWorld::NpcStats()); - - CSStore store(mGmsts, mSkills, mMagicEffects, mSpells); - - if (autoCalc) - { - AutoCalc::autoCalcAttributesImpl (&npc, race, class_, level, *stats, &store); - - stats->setHealth(autoCalculateHealth(level, class_, *stats)); - stats->setMana(autoCalculateMana(*stats)); - stats->setFatigue(autoCalculateFatigue(*stats)); - - AutoCalc::autoCalcSkillsImpl(&npc, race, class_, level, *stats, &store); - - AutoCalc::autoCalculateSpells(race, *stats, &store); - } - else - { - for (std::vector::const_iterator it = npc.mSpells.mList.begin(); - it != npc.mSpells.mList.end(); ++it) - { - stats->addSpell(*it); - } - } - - // update spell info - const std::vector &racePowers = race->mPowers.mList; - for (unsigned int i = 0; i < racePowers.size(); ++i) - { - int type = -1; - int spellIndex = mSpells.searchId(racePowers[i]); - if (spellIndex != -1) - type = mSpells.getRecord(spellIndex).get().mData.mType; - stats->addPowers(racePowers[i], type); - } - // cost/chance - int skills[ESM::Skill::Length]; - if (autoCalc) - for (int i = 0; i< ESM::Skill::Length; ++i) - skills[i] = stats->getBaseSkill(i); - else - for (int i = 0; i< ESM::Skill::Length; ++i) - skills[i] = npc.mNpdt52.mSkills[i]; - - int attributes[ESM::Attribute::Length]; - if (autoCalc) - for (int i = 0; i< ESM::Attribute::Length; ++i) - attributes[i] = stats->getBaseAttribute(i); - else - { - attributes[ESM::Attribute::Strength] = npc.mNpdt52.mStrength; - attributes[ESM::Attribute::Willpower] = npc.mNpdt52.mWillpower; - attributes[ESM::Attribute::Agility] = npc.mNpdt52.mAgility; - attributes[ESM::Attribute::Speed] = npc.mNpdt52.mSpeed; - attributes[ESM::Attribute::Endurance] = npc.mNpdt52.mEndurance; - attributes[ESM::Attribute::Personality] = npc.mNpdt52.mPersonality; - attributes[ESM::Attribute::Luck] = npc.mNpdt52.mLuck; - } - - const std::vector& spells = stats->spells(); - for (std::vector::const_iterator it = spells.begin(); it != spells.end(); ++it) - { - int cost = -1; - int spellIndex = mSpells.searchId((*it).mName); - const ESM::Spell* spell = 0; - if (spellIndex != -1) - { - spell = &mSpells.getRecord(spellIndex).get(); - cost = spell->mData.mCost; - - int school; - float skillTerm; - AutoCalc::calcWeakestSchool(spell, skills, school, skillTerm, &store); - float chance = calcAutoCastChance(spell, skills, attributes, school, &store); - - stats->addCostAndChance((*it).mName, cost, (int)ceil(chance)); // percent - } - } - - if (stats.get() == 0) - return 0; - - CSMWorld::NpcStats *result = stats.release(); - emit cacheNpcStats (npc.mId, result); - return result; -} - -void CSMWorld::Data::cacheNpcStatsEvent (const std::string& id, CSMWorld::NpcStats *stats) -{ - mNpcStatCache[id] = stats; -} - -CSMWorld::NpcStats* CSMWorld::Data::getCachedNpcData (const std::string& id) const -{ - std::map::const_iterator it = mNpcStatCache.find(id); - if (it != mNpcStatCache.end()) - return it->second; - else - return 0; -} diff --git a/apps/opencs/model/world/data.hpp b/apps/opencs/model/world/data.hpp index 944a636f0a..3d3996a1de 100644 --- a/apps/opencs/model/world/data.hpp +++ b/apps/opencs/model/world/data.hpp @@ -62,6 +62,7 @@ namespace CSMWorld class ResourcesManager; class Resources; class NpcStats; + class NpcAutoCalc; class Data : public QObject { @@ -109,7 +110,7 @@ namespace CSMWorld std::vector > mReaders; - std::map mNpcStatCache; + NpcAutoCalc *mNpcAutoCalc; // not implemented Data (const Data&); @@ -282,37 +283,17 @@ namespace CSMWorld int count (RecordBase::State state) const; ///< Return number of top-level records with the given \a state. - NpcStats* npcAutoCalculate (const ESM::NPC& npc) const; - - NpcStats* getCachedNpcData (const std::string& id) const; + const NpcAutoCalc& getNpcAutoCalc() const; signals: void idListChanged(); - // refresh NPC dialogue subviews via object table model - void updateNpcAutocalc (int type, const std::string& id); - - void cacheNpcStats (const std::string& id, NpcStats *stats) const; - private slots: void dataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); void rowsChanged (const QModelIndex& parent, int start, int end); - - // for autocalc updates when gmst/race/class/skils tables change - void gmstDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); - - void raceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); - - void classDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); - - void skillDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); - - void npcDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); - - void cacheNpcStatsEvent (const std::string& id, NpcStats *stats); }; } diff --git a/apps/opencs/model/world/npcautocalc.cpp b/apps/opencs/model/world/npcautocalc.cpp new file mode 100644 index 0000000000..21eb822660 --- /dev/null +++ b/apps/opencs/model/world/npcautocalc.cpp @@ -0,0 +1,367 @@ +#include "npcautocalc.hpp" + +#include + +#include +#include +#include + +#include "npcstats.hpp" +#include "data.hpp" +#include "idtable.hpp" +#include "idtree.hpp" + +namespace +{ + class CSStore : public AutoCalc::StoreCommon + { + const CSMWorld::IdCollection& mGmstTable; + const CSMWorld::IdCollection& mSkillTable; + const CSMWorld::IdCollection& mMagicEffectTable; + const CSMWorld::NestedIdCollection& mSpells; + + public: + + CSStore(const CSMWorld::IdCollection& gmst, + const CSMWorld::IdCollection& skills, + const CSMWorld::IdCollection& magicEffects, + const CSMWorld::NestedIdCollection& spells) + : mGmstTable(gmst), mSkillTable(skills), mMagicEffectTable(magicEffects), mSpells(spells) + {} + + ~CSStore() {} + + virtual int findGmstInt(const std::string& name) const + { + return mGmstTable.getRecord(name).get().getInt(); + } + + virtual float findGmstFloat(const std::string& name) const + { + return mGmstTable.getRecord(name).get().getFloat(); + } + + virtual const ESM::Skill *findSkill(int index) const + { + // if the skill does not exist, throws std::runtime_error ("invalid ID: " + id) + return &mSkillTable.getRecord(ESM::Skill::indexToId(index)).get(); + } + + virtual const ESM::MagicEffect* findMagicEffect(int id) const + { + // if the magic effect does not exist, throws std::runtime_error ("invalid ID: " + id) + return &mMagicEffectTable.getRecord(ESM::MagicEffect::indexToId((short)id)).get(); + } + + virtual void getSpells(std::vector& spells) + { + // prepare data in a format used by OpenMW store + for (int index = 0; index < mSpells.getSize(); ++index) + spells.push_back(const_cast(&mSpells.getRecord(index).get())); + } + }; + + unsigned short autoCalculateMana(const AutoCalc::StatsBase& stats) + { + return stats.getBaseAttribute(ESM::Attribute::Intelligence) * 2; + } + + unsigned short autoCalculateFatigue(const AutoCalc::StatsBase& stats) + { + return stats.getBaseAttribute(ESM::Attribute::Strength) + + stats.getBaseAttribute(ESM::Attribute::Willpower) + + stats.getBaseAttribute(ESM::Attribute::Agility) + + stats.getBaseAttribute(ESM::Attribute::Endurance); + } +} + +CSMWorld::NpcAutoCalc::NpcAutoCalc (const Data& data, + const IdTable *gmsts, const IdTable *skills, const IdTable *classes, const IdTree *races, const IdTree *objects) +: mData(data), mSkillModel(skills), mClassModel(classes), mRaceModel(races) +{ + // for autocalc updates when gmst/race/class/skils tables change + connect (gmsts, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (gmstDataChanged (const QModelIndex&, const QModelIndex&))); + connect (mSkillModel, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (skillDataChanged (const QModelIndex&, const QModelIndex&))); + connect (mClassModel, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (classDataChanged (const QModelIndex&, const QModelIndex&))); + connect (mRaceModel, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (raceDataChanged (const QModelIndex&, const QModelIndex&))); + connect (objects, SIGNAL (dataChanged (const QModelIndex&, const QModelIndex&)), + this, SLOT (npcDataChanged (const QModelIndex&, const QModelIndex&))); + connect (this, SIGNAL (updateNpcAutocalc (int, const std::string&)), + objects, SLOT (updateNpcAutocalc (int, const std::string&))); + + //connect (this, SIGNAL (cacheNpcStats (const std::string&, NpcStats*)), + //this, SLOT (cacheNpcStatsEvent (const std::string&, NpcStats*))); +} + +CSMWorld::NpcAutoCalc::~NpcAutoCalc() +{ + clearNpcStatsCache(); +} + +void CSMWorld::NpcAutoCalc::skillDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + // mData.mAttribute (affects attributes skill bonus autocalc) + // mData.mSpecialization (affects skills autocalc) + int attributeColumn = mSkillModel->findColumnIndex(CSMWorld::Columns::ColumnId_Attribute); + int specialisationColumn = mSkillModel->findColumnIndex(CSMWorld::Columns::ColumnId_Specialisation); + + if ((topLeft.column() <= attributeColumn && attributeColumn <= bottomRight.column()) + || (topLeft.column() <= specialisationColumn && specialisationColumn <= bottomRight.column())) + { + clearNpcStatsCache(); + + std::string empty; + emit updateNpcAutocalc(0/*all*/, empty); + } +} + +void CSMWorld::NpcAutoCalc::classDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + // update autocalculated attributes/skills of every NPC with matching class + // - mData.mAttribute[2] + // - mData.mSkills[5][2] + // - mData.mSpecialization + int attribute1Column = mClassModel->findColumnIndex(CSMWorld::Columns::ColumnId_Attribute1); // +1 + int majorSkill1Column = mClassModel->findColumnIndex(CSMWorld::Columns::ColumnId_MajorSkill1); // +4 + int minorSkill1Column = mClassModel->findColumnIndex(CSMWorld::Columns::ColumnId_MinorSkill1); // +4 + int specialisationColumn = mClassModel->findColumnIndex(CSMWorld::Columns::ColumnId_Specialisation); + + if ((topLeft.column() > attribute1Column+1 || attribute1Column > bottomRight.column()) + && (topLeft.column() > majorSkill1Column+4 || majorSkill1Column > bottomRight.column()) + && (topLeft.column() > minorSkill1Column+4 || minorSkill1Column > bottomRight.column()) + && (topLeft.column() > specialisationColumn || specialisationColumn > bottomRight.column())) + { + return; + } + + // get the affected class + int idColumn = mClassModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); + for (int classRow = topLeft.row(); classRow <= bottomRight.row(); ++classRow) + { + clearNpcStatsCache(); + + std::string classId = + mClassModel->data(mClassModel->index(classRow, idColumn)).toString().toUtf8().constData(); + emit updateNpcAutocalc(1/*class*/, classId); + } +} + +void CSMWorld::NpcAutoCalc::raceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + // affects racial bonus attributes & skills + // - mData.mAttributeValues[] + // - mData.mBonus[].mBonus + // - mPowers.mList[] + int attrColumn = mRaceModel->findColumnIndex(CSMWorld::Columns::ColumnId_RaceAttributes); + int bonusColumn = mRaceModel->findColumnIndex(CSMWorld::Columns::ColumnId_RaceSkillBonus); + int powersColumn = mRaceModel->findColumnIndex(CSMWorld::Columns::ColumnId_PowerList); + + bool match = false; + int raceRow = topLeft.row(); + int raceEnd = bottomRight.row(); + if (topLeft.parent().isValid() && bottomRight.parent().isValid()) + { + if ((topLeft.parent().column() <= attrColumn && attrColumn <= bottomRight.parent().column()) + || (topLeft.parent().column() <= bonusColumn && bonusColumn <= bottomRight.parent().column()) + || (topLeft.parent().column() <= powersColumn && powersColumn <= bottomRight.parent().column())) + { + match = true; // TODO: check for specific nested column? + raceRow = topLeft.parent().row(); + raceEnd = bottomRight.parent().row(); + } + } + else + { + if ((topLeft.column() <= attrColumn && attrColumn <= bottomRight.column()) + || (topLeft.column() <= bonusColumn && bonusColumn <= bottomRight.column()) + || (topLeft.column() <= powersColumn && powersColumn <= bottomRight.column())) + { + match = true; // maybe the whole table changed + } + } + + if (!match) + return; + + // update autocalculated attributes/skills of every NPC with matching race + int idColumn = mRaceModel->findColumnIndex(CSMWorld::Columns::ColumnId_Id); + for (; raceRow <= raceEnd; ++raceRow) + { + clearNpcStatsCache(); + + std::string raceId = + mRaceModel->data(mRaceModel->index(raceRow, idColumn)).toString().toUtf8().constData(); + emit updateNpcAutocalc(2/*race*/, raceId); + } +} + +void CSMWorld::NpcAutoCalc::npcDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + // TODO: for now always recalculate + clearNpcStatsCache(); + + // TODO: check if below signal slows things down + std::string empty; + emit updateNpcAutocalc(0/*all*/, empty); +} + +void CSMWorld::NpcAutoCalc::gmstDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight) +{ + static const QStringList gmsts(QStringList()<< "fNPCbaseMagickaMult" << "fAutoSpellChance" + << "fEffectCostMult" << "iAutoSpellAlterationMax" << "iAutoSpellConjurationMax" + << "iAutoSpellDestructionMax" << "iAutoSpellIllusionMax" << "iAutoSpellMysticismMax" + << "iAutoSpellRestorationMax" << "iAutoSpellTimesCanCast" << "iAutoSpellAttSkillMin"); + + bool match = false; + for (int row = topLeft.row(); row <= bottomRight.row(); ++row) + { + if (gmsts.contains(mData.getGmsts().getRecord(row).get().mId.c_str())) + { + match = true; + break; + } + } + + if (!match) + return; + + clearNpcStatsCache(); + + std::string empty; + emit updateNpcAutocalc(0/*all*/, empty); +} + +void CSMWorld::NpcAutoCalc::clearNpcStatsCache () +{ + for (std::map::iterator it (mNpcStatCache.begin()); + it != mNpcStatCache.end(); ++it) + delete it->second; + + mNpcStatCache.clear(); +} + +CSMWorld::NpcStats* CSMWorld::NpcAutoCalc::npcAutoCalculate(const ESM::NPC& npc) const +{ + CSMWorld::NpcStats *cachedStats = getCachedNpcData (npc.mId); + if (cachedStats) + return cachedStats; + + int raceIndex = mData.getRaces().searchId(npc.mRace); + int classIndex = mData.getClasses().searchId(npc.mClass); + // this can happen when creating a new game from scratch + if (raceIndex == -1 || classIndex == -1) + return 0; + + const ESM::Race *race = &mData.getRaces().getRecord(raceIndex).get(); + const ESM::Class *class_ = &mData.getClasses().getRecord(classIndex).get(); + + bool autoCalc = npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS; + short level = npc.mNpdt52.mLevel; + if (autoCalc) + level = npc.mNpdt12.mLevel; + + std::auto_ptr stats (new CSMWorld::NpcStats()); + + CSStore store(mData.getGmsts(), mData.getSkills(), mData.getMagicEffects(), static_cast&>(mData.getSpells())); + + if (autoCalc) + { + AutoCalc::autoCalcAttributesImpl (&npc, race, class_, level, *stats, &store); + + stats->setHealth(autoCalculateHealth(level, class_, *stats)); + stats->setMana(autoCalculateMana(*stats)); + stats->setFatigue(autoCalculateFatigue(*stats)); + + AutoCalc::autoCalcSkillsImpl(&npc, race, class_, level, *stats, &store); + + AutoCalc::autoCalculateSpells(race, *stats, &store); + } + else + { + for (std::vector::const_iterator it = npc.mSpells.mList.begin(); + it != npc.mSpells.mList.end(); ++it) + { + stats->addSpell(*it); + } + } + + // update spell info + const std::vector &racePowers = race->mPowers.mList; + for (unsigned int i = 0; i < racePowers.size(); ++i) + { + int type = -1; + int spellIndex = mData.getSpells().searchId(racePowers[i]); + if (spellIndex != -1) + type = mData.getSpells().getRecord(spellIndex).get().mData.mType; + stats->addPowers(racePowers[i], type); + } + // cost/chance + int skills[ESM::Skill::Length]; + if (autoCalc) + for (int i = 0; i< ESM::Skill::Length; ++i) + skills[i] = stats->getBaseSkill(i); + else + for (int i = 0; i< ESM::Skill::Length; ++i) + skills[i] = npc.mNpdt52.mSkills[i]; + + int attributes[ESM::Attribute::Length]; + if (autoCalc) + for (int i = 0; i< ESM::Attribute::Length; ++i) + attributes[i] = stats->getBaseAttribute(i); + else + { + attributes[ESM::Attribute::Strength] = npc.mNpdt52.mStrength; + attributes[ESM::Attribute::Willpower] = npc.mNpdt52.mWillpower; + attributes[ESM::Attribute::Agility] = npc.mNpdt52.mAgility; + attributes[ESM::Attribute::Speed] = npc.mNpdt52.mSpeed; + attributes[ESM::Attribute::Endurance] = npc.mNpdt52.mEndurance; + attributes[ESM::Attribute::Personality] = npc.mNpdt52.mPersonality; + attributes[ESM::Attribute::Luck] = npc.mNpdt52.mLuck; + } + + const std::vector& spells = stats->spells(); + for (std::vector::const_iterator it = spells.begin(); it != spells.end(); ++it) + { + int cost = -1; + int spellIndex = mData.getSpells().searchId((*it).mName); + const ESM::Spell* spell = 0; + if (spellIndex != -1) + { + spell = &mData.getSpells().getRecord(spellIndex).get(); + cost = spell->mData.mCost; + + int school; + float skillTerm; + AutoCalc::calcWeakestSchool(spell, skills, school, skillTerm, &store); + float chance = calcAutoCastChance(spell, skills, attributes, school, &store); + + stats->addCostAndChance((*it).mName, cost, (int)ceil(chance)); // percent + } + } + + if (stats.get() == 0) + return 0; + + CSMWorld::NpcStats *result = stats.release(); + //emit cacheNpcStats (npc.mId, result); + mNpcStatCache[npc.mId] = result; + return result; +} + +//void CSMWorld::NpcAutoCalc::cacheNpcStatsEvent (const std::string& id, CSMWorld::NpcStats *stats) +//{ + //mNpcStatCache[id] = stats; +//} + +CSMWorld::NpcStats* CSMWorld::NpcAutoCalc::getCachedNpcData (const std::string& id) const +{ + std::map::const_iterator it = mNpcStatCache.find(id); + if (it != mNpcStatCache.end()) + return it->second; + else + return 0; +} diff --git a/apps/opencs/model/world/npcautocalc.hpp b/apps/opencs/model/world/npcautocalc.hpp new file mode 100644 index 0000000000..ceb604d353 --- /dev/null +++ b/apps/opencs/model/world/npcautocalc.hpp @@ -0,0 +1,75 @@ +#ifndef CSM_WORLD_NPCAUTOCALC_H +#define CSM_WORLD_NPCAUTOCALC_H + +#include +#include + +#include +#include + +namespace ESM +{ + struct NPC; +} + +namespace CSMWorld +{ + class Data; + class NpcStats; + class IdTable; + class IdTree; + + class NpcAutoCalc : public QObject + { + Q_OBJECT + + const Data& mData; + const IdTable *mSkillModel; + const IdTable *mClassModel; + const IdTree *mRaceModel; + mutable std::map mNpcStatCache; + + public: + + NpcAutoCalc (const Data& data, const IdTable *gmsts, const IdTable *skills, const IdTable *classes, + const IdTree *races, const IdTree *objects); + + ~NpcAutoCalc (); + + NpcStats* npcAutoCalculate (const ESM::NPC& npc) const; + + private: + + // not implemented + NpcAutoCalc (const NpcAutoCalc&); + NpcAutoCalc& operator= (const NpcAutoCalc&); + + NpcStats* getCachedNpcData (const std::string& id) const; + + void clearNpcStatsCache (); + + signals: + + // refresh NPC dialogue subviews via object table model + void updateNpcAutocalc (int type, const std::string& id); + + //void cacheNpcStats (const std::string& id, NpcStats *stats) const; + + private slots: + + // for autocalc updates when gmst/race/class/skils tables change + void gmstDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + void raceDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + void classDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + void skillDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + void npcDataChanged (const QModelIndex& topLeft, const QModelIndex& bottomRight); + + //void cacheNpcStatsEvent (const std::string& id, NpcStats *stats); + }; +} + +#endif // CSM_WORLD_NPCAUTOCALC_H diff --git a/apps/opencs/model/world/refidadapterimp.cpp b/apps/opencs/model/world/refidadapterimp.cpp index a268cb5f08..1683fd000f 100644 --- a/apps/opencs/model/world/refidadapterimp.cpp +++ b/apps/opencs/model/world/refidadapterimp.cpp @@ -12,6 +12,7 @@ #include "usertype.hpp" #include "idtree.hpp" #include "npcstats.hpp" +#include "npcautocalc.hpp" CSMWorld::PotionColumns::PotionColumns (const InventoryColumns& columns) : InventoryColumns (columns) {} @@ -777,7 +778,7 @@ void CSMWorld::NpcRefIdAdapter::setData (const RefIdColumn *column, RefIdData& d { if (npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) { - CSMWorld::NpcStats *stats = mData.npcAutoCalculate(npc); + CSMWorld::NpcStats *stats = mData.getNpcAutoCalc().npcAutoCalculate(npc); if (!stats) { record.setModified (npc); @@ -817,7 +818,7 @@ void CSMWorld::NpcRefIdAdapter::setData (const RefIdColumn *column, RefIdData& d { npc.mNpdtType = ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS; npc.mNpdt12.mLevel = npc.mNpdt52.mLevel; // for NPC's loaded as non-autocalc - mData.npcAutoCalculate(npc); + mData.getNpcAutoCalc().npcAutoCalculate(npc); } } } @@ -888,7 +889,7 @@ QVariant CSMWorld::NpcAttributesRefIdAdapter::getNestedData (const RefIdColumn * else if (subColIndex == 1) if (npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) { - CSMWorld::NpcStats *stats = mData.npcAutoCalculate(npc); + CSMWorld::NpcStats *stats = mData.getNpcAutoCalc().npcAutoCalculate(npc); if (!stats) return QVariant(); @@ -1020,7 +1021,7 @@ QVariant CSMWorld::NpcSkillsRefIdAdapter::getNestedData (const RefIdColumn *colu { if (npc.mNpdtType == ESM::NPC::NPC_WITH_AUTOCALCULATED_STATS) { - CSMWorld::NpcStats *stats = mData.npcAutoCalculate(npc); + CSMWorld::NpcStats *stats = mData.getNpcAutoCalc().npcAutoCalculate(npc); if (!stats) return QVariant(); @@ -1108,7 +1109,7 @@ QVariant CSMWorld::NpcMiscRefIdAdapter::getNestedData (const RefIdColumn *column if (autoCalc) { - CSMWorld::NpcStats *stats = mData.npcAutoCalculate(npc); + CSMWorld::NpcStats *stats = mData.getNpcAutoCalc().npcAutoCalculate(npc); switch (subColIndex) { @@ -1732,7 +1733,7 @@ QVariant NestedSpellRefIdAdapter::getNestedData (const RefIdColumn *co const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); - CSMWorld::NpcStats *stats = mData.npcAutoCalculate(record.get()); + CSMWorld::NpcStats *stats = mData.getNpcAutoCalc().npcAutoCalculate(record.get()); if (!stats) return QVariant(); @@ -1766,7 +1767,7 @@ int NestedSpellRefIdAdapter::getNestedRowsCount(const RefIdColumn *col const Record& record = static_cast&> (data.getRecord (RefIdData::LocalIndex (index, mType))); - CSMWorld::NpcStats *stats = mData.npcAutoCalculate(record.get()); + CSMWorld::NpcStats *stats = mData.getNpcAutoCalc().npcAutoCalculate(record.get()); if (!stats) return 0;