From 4d7947d27c68201f5d8d544556c869829eebfbfe Mon Sep 17 00:00:00 2001
From: Evil Eye <malusluminis@hotmail.com>
Date: Tue, 2 Jun 2020 21:59:37 +0200
Subject: [PATCH] Mutate base records when editing AI settings (#2798)

---
 apps/openmw/mwbase/world.hpp            |  9 ++++++
 apps/openmw/mwclass/creature.cpp        |  5 ++++
 apps/openmw/mwclass/creature.hpp        |  2 ++
 apps/openmw/mwclass/npc.cpp             |  5 ++++
 apps/openmw/mwclass/npc.hpp             |  2 ++
 apps/openmw/mwmechanics/actorutil.hpp   | 37 +++++++++++++++++++++++++
 apps/openmw/mwscript/aiextensions.cpp   |  7 +++--
 apps/openmw/mwstate/statemanagerimp.cpp |  1 +
 apps/openmw/mwworld/class.cpp           |  5 ++++
 apps/openmw/mwworld/class.hpp           |  4 ++-
 apps/openmw/mwworld/esmstore.cpp        | 35 +++++++++++------------
 apps/openmw/mwworld/esmstore.hpp        |  3 ++
 apps/openmw/mwworld/worldimp.cpp        | 11 ++++++++
 apps/openmw/mwworld/worldimp.hpp        |  8 ++++++
 14 files changed, 112 insertions(+), 22 deletions(-)

diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp
index 84f9b49844..05802c6584 100644
--- a/apps/openmw/mwbase/world.hpp
+++ b/apps/openmw/mwbase/world.hpp
@@ -35,6 +35,7 @@ namespace ESM
     struct Position;
     struct Cell;
     struct Class;
+    struct Creature;
     struct Potion;
     struct Spell;
     struct NPC;
@@ -377,6 +378,14 @@ namespace MWBase
             ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID.
             /// \return pointer to created record
 
+            virtual const ESM::Creature *createOverrideRecord (const ESM::Creature& record) = 0;
+            ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID.
+            /// \return pointer to created record
+
+            virtual const ESM::NPC *createOverrideRecord (const ESM::NPC& record) = 0;
+            ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID.
+            /// \return pointer to created record
+
             virtual void update (float duration, bool paused) = 0;
             virtual void updatePhysics (float duration, bool paused) = 0;
 
diff --git a/apps/openmw/mwclass/creature.cpp b/apps/openmw/mwclass/creature.cpp
index d1a4395283..375b70dde7 100644
--- a/apps/openmw/mwclass/creature.cpp
+++ b/apps/openmw/mwclass/creature.cpp
@@ -878,4 +878,9 @@ namespace MWClass
         const MWWorld::LiveCellRef<ESM::Creature> *ref = ptr.get<ESM::Creature>();
         scale *= ref->mBase->mScale;
     }
+
+    void Creature::setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const
+    {
+        MWMechanics::setBaseAISetting<ESM::Creature>(id, setting, value);
+    }
 }
diff --git a/apps/openmw/mwclass/creature.hpp b/apps/openmw/mwclass/creature.hpp
index 35688ed1ac..9be5c42720 100644
--- a/apps/openmw/mwclass/creature.hpp
+++ b/apps/openmw/mwclass/creature.hpp
@@ -129,6 +129,8 @@ namespace MWClass
 
             virtual void adjustScale(const MWWorld::ConstPtr& ptr, osg::Vec3f& scale, bool rendering) const;
             /// @param rendering Indicates if the scale to adjust is for the rendering mesh, or for the collision mesh
+
+            virtual void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const;
     };
 }
 
diff --git a/apps/openmw/mwclass/npc.cpp b/apps/openmw/mwclass/npc.cpp
index 297471e07f..df483962ab 100644
--- a/apps/openmw/mwclass/npc.cpp
+++ b/apps/openmw/mwclass/npc.cpp
@@ -1437,4 +1437,9 @@ namespace MWClass
         const MWWorld::LiveCellRef<ESM::NPC> *ref = ptr.get<ESM::NPC>();
         return ref->mBase->getFactionRank();
     }
+
+    void Npc::setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const
+    {
+        MWMechanics::setBaseAISetting<ESM::NPC>(id, setting, value);
+    }
 }
diff --git a/apps/openmw/mwclass/npc.hpp b/apps/openmw/mwclass/npc.hpp
index 9b92f63382..3d63697fb9 100644
--- a/apps/openmw/mwclass/npc.hpp
+++ b/apps/openmw/mwclass/npc.hpp
@@ -164,6 +164,8 @@ namespace MWClass
 
             virtual std::string getPrimaryFaction(const MWWorld::ConstPtr &ptr) const;
             virtual int getPrimaryFactionRank(const MWWorld::ConstPtr &ptr) const;
+
+            virtual void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const;
     };
 }
 
diff --git a/apps/openmw/mwmechanics/actorutil.hpp b/apps/openmw/mwmechanics/actorutil.hpp
index 82a904799b..cdb7873114 100644
--- a/apps/openmw/mwmechanics/actorutil.hpp
+++ b/apps/openmw/mwmechanics/actorutil.hpp
@@ -1,6 +1,16 @@
 #ifndef OPENMW_MWMECHANICS_ACTORUTIL_H
 #define OPENMW_MWMECHANICS_ACTORUTIL_H
 
+#include <components/esm/loadcrea.hpp>
+#include <components/esm/loadnpc.hpp>
+
+#include "../mwbase/environment.hpp"
+#include "../mwbase/world.hpp"
+
+#include "../mwworld/esmstore.hpp"
+
+#include "./creaturestats.hpp"
+
 namespace MWWorld
 {
     class Ptr;
@@ -11,6 +21,33 @@ namespace MWMechanics
     MWWorld::Ptr getPlayer();
     bool isPlayerInCombat();
     bool canActorMoveByZAxis(const MWWorld::Ptr& actor);
+
+    template<class T>
+    void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value)
+    {
+        T copy = *MWBase::Environment::get().getWorld()->getStore().get<T>().find(id);
+        switch(setting)
+        {
+            case MWMechanics::CreatureStats::AiSetting::AI_Hello:
+                copy.mAiData.mHello = value;
+                break;
+            case MWMechanics::CreatureStats::AiSetting::AI_Fight:
+                copy.mAiData.mFight = value;
+                break;
+            case MWMechanics::CreatureStats::AiSetting::AI_Flee:
+                copy.mAiData.mFlee = value;
+                break;
+            case MWMechanics::CreatureStats::AiSetting::AI_Alarm:
+                copy.mAiData.mAlarm = value;
+                break;
+            default:
+                assert(0);
+        }
+        MWBase::Environment::get().getWorld()->createOverrideRecord(copy);
+    }
+
+    template void setBaseAISetting<ESM::Creature>(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value);
+    template void setBaseAISetting<ESM::NPC>(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value);
 }
 
 #endif
diff --git a/apps/openmw/mwscript/aiextensions.cpp b/apps/openmw/mwscript/aiextensions.cpp
index 79639197d3..fdd13cfb6d 100644
--- a/apps/openmw/mwscript/aiextensions.cpp
+++ b/apps/openmw/mwscript/aiextensions.cpp
@@ -257,8 +257,10 @@ namespace MWScript
                     Interpreter::Type_Integer value = runtime[0].mInteger;
                     runtime.pop();
 
-                    ptr.getClass().getCreatureStats (ptr).setAiSetting (mIndex,
-                        ptr.getClass().getCreatureStats (ptr).getAiSetting (mIndex).getBase() + value);
+                    int modified = ptr.getClass().getCreatureStats (ptr).getAiSetting (mIndex).getBase() + value;
+
+                    ptr.getClass().getCreatureStats (ptr).setAiSetting (mIndex, modified);
+                    ptr.getClass().setBaseAISetting(ptr.getCellRef().getRefId(), mIndex, modified);
                 }
         };
         template<class R>
@@ -277,6 +279,7 @@ namespace MWScript
                     MWMechanics::Stat<int> stat = ptr.getClass().getCreatureStats(ptr).getAiSetting(mIndex);
                     stat.setModified(value, 0);
                     ptr.getClass().getCreatureStats(ptr).setAiSetting(mIndex, stat);
+                    ptr.getClass().setBaseAISetting(ptr.getCellRef().getRefId(), mIndex, value);
                 }
         };
 
diff --git a/apps/openmw/mwstate/statemanagerimp.cpp b/apps/openmw/mwstate/statemanagerimp.cpp
index 86a26212fd..560c24578d 100644
--- a/apps/openmw/mwstate/statemanagerimp.cpp
+++ b/apps/openmw/mwstate/statemanagerimp.cpp
@@ -460,6 +460,7 @@ void MWState::StateManager::loadGame (const Character *character, const std::str
                 case ESM::REC_ENAB:
                 case ESM::REC_LEVC:
                 case ESM::REC_LEVI:
+                case ESM::REC_CREA:
                     MWBase::Environment::get().getWorld()->readRecord(reader, n.intval, contentFileMap);
                     break;
 
diff --git a/apps/openmw/mwworld/class.cpp b/apps/openmw/mwworld/class.cpp
index 0287db56fa..07981bf2af 100644
--- a/apps/openmw/mwworld/class.cpp
+++ b/apps/openmw/mwworld/class.cpp
@@ -516,4 +516,9 @@ namespace MWWorld
         result.z() = magicEffect->mData.mBlue / 255.f;
         return result;
     }
+
+    void Class::setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const
+    {
+        throw std::runtime_error ("class does not have creature stats");
+    }
 }
diff --git a/apps/openmw/mwworld/class.hpp b/apps/openmw/mwworld/class.hpp
index 8c3740eddf..fb816d8109 100644
--- a/apps/openmw/mwworld/class.hpp
+++ b/apps/openmw/mwworld/class.hpp
@@ -10,6 +10,7 @@
 
 #include "ptr.hpp"
 #include "doorstate.hpp"
+#include "../mwmechanics/creaturestats.hpp"
 
 namespace ESM
 {
@@ -28,7 +29,6 @@ namespace MWPhysics
 
 namespace MWMechanics
 {
-    class CreatureStats;
     class NpcStats;
     struct Movement;
 }
@@ -360,6 +360,8 @@ namespace MWWorld
             virtual float getEffectiveArmorRating(const MWWorld::ConstPtr& armor, const MWWorld::Ptr& actor) const;
 
             virtual osg::Vec4f getEnchantmentColor(const MWWorld::ConstPtr& item) const;
+
+            virtual void setBaseAISetting(const std::string& id, MWMechanics::CreatureStats::AiSetting setting, int value) const;
     };
 }
 
diff --git a/apps/openmw/mwworld/esmstore.cpp b/apps/openmw/mwworld/esmstore.cpp
index 1f6ed51027..f862266404 100644
--- a/apps/openmw/mwworld/esmstore.cpp
+++ b/apps/openmw/mwworld/esmstore.cpp
@@ -273,7 +273,8 @@ void ESMStore::validate()
             +mSpells.getDynamicSize()
             +mWeapons.getDynamicSize()
             +mCreatureLists.getDynamicSize()
-            +mItemLists.getDynamicSize();
+            +mItemLists.getDynamicSize()
+            +mCreatures.getDynamicSize();
     }
 
     void ESMStore::write (ESM::ESMWriter& writer, Loading::Listener& progress) const
@@ -295,6 +296,7 @@ void ESMStore::validate()
         mNpcs.write (writer, progress);
         mItemLists.write (writer, progress);
         mCreatureLists.write (writer, progress);
+        mCreatures.write (writer, progress);
     }
 
     bool ESMStore::readRecord (ESM::ESMReader& reader, uint32_t type)
@@ -312,24 +314,8 @@ void ESMStore::validate()
             case ESM::REC_NPC_:
             case ESM::REC_LEVI:
             case ESM::REC_LEVC:
-
-                {
-                    mStores[type]->read (reader);
-                }
-
-                if (type==ESM::REC_NPC_)
-                {
-                    // NPC record will always be last and we know that there can be only one
-                    // dynamic NPC record (player) -> We are done here with dynamic record loading
-                    setUp();
-
-                    const ESM::NPC *player = mNpcs.find ("player");
-
-                    if (!mRaces.find (player->mRace) ||
-                        !mClasses.find (player->mClass))
-                        throw std::runtime_error ("Invalid player record (race or class unavailable");
-                }
-
+            case ESM::REC_CREA:
+                mStores[type]->read (reader);
                 return true;
 
             case ESM::REC_DYNA:
@@ -343,4 +329,15 @@ void ESMStore::validate()
         }
     }
 
+    void ESMStore::checkPlayer()
+    {
+        setUp();
+
+        const ESM::NPC *player = mNpcs.find ("player");
+
+        if (!mRaces.find (player->mRace) ||
+            !mClasses.find (player->mClass))
+            throw std::runtime_error ("Invalid player record (race or class unavailable");
+    }
+
 } // end namespace
diff --git a/apps/openmw/mwworld/esmstore.hpp b/apps/openmw/mwworld/esmstore.hpp
index d170a32c58..fe1cfc7087 100644
--- a/apps/openmw/mwworld/esmstore.hpp
+++ b/apps/openmw/mwworld/esmstore.hpp
@@ -239,6 +239,9 @@ namespace MWWorld
 
         bool readRecord (ESM::ESMReader& reader, uint32_t type);
         ///< \return Known type?
+
+        // To be called when we are done with dynamic record loading
+        void checkPlayer();
     };
 
     template <>
diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp
index ba4a6467ac..6d20f5d455 100644
--- a/apps/openmw/mwworld/worldimp.cpp
+++ b/apps/openmw/mwworld/worldimp.cpp
@@ -403,6 +403,7 @@ namespace MWWorld
                 reader.getHNT(mLevitationEnabled, "LEVT");
                 return;
             case ESM::REC_PLAY:
+                mStore.checkPlayer();
                 mPlayer->readRecord(reader, type);
                 if (getPlayerPtr().isInCell())
                 {
@@ -1815,6 +1816,16 @@ namespace MWWorld
         return mStore.overrideRecord(record);
     }
 
+    const ESM::Creature *World::createOverrideRecord(const ESM::Creature &record)
+    {
+        return mStore.overrideRecord(record);
+    }
+
+    const ESM::NPC *World::createOverrideRecord(const ESM::NPC &record)
+    {
+        return mStore.overrideRecord(record);
+    }
+
     const ESM::NPC *World::createRecord(const ESM::NPC &record)
     {
         bool update = false;
diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp
index 7b6d2afdcd..201803a481 100644
--- a/apps/openmw/mwworld/worldimp.hpp
+++ b/apps/openmw/mwworld/worldimp.hpp
@@ -490,6 +490,14 @@ namespace MWWorld
             ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID.
             /// \return pointer to created record
 
+            const ESM::Creature *createOverrideRecord (const ESM::Creature& record) override;
+            ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID.
+            /// \return pointer to created record
+
+            const ESM::NPC *createOverrideRecord (const ESM::NPC& record) override;
+            ///< Write this record to the ESM store, allowing it to override a pre-existing record with the same ID.
+            /// \return pointer to created record
+
             void update (float duration, bool paused) override;
             void updatePhysics (float duration, bool paused) override;