From 61d207bd7869e31e3387851ad245867612e9db0e Mon Sep 17 00:00:00 2001
From: Petr Mikheev <ptmikheev@gmail.com>
Date: Sun, 16 Apr 2023 23:50:27 +0200
Subject: [PATCH] Allow Lua scripts to extend or override standard activation
 mechanics

---
 apps/openmw/mwlua/luabindings.cpp             | 11 +++
 apps/openmw/mwlua/objectbindings.cpp          | 49 ++-----------
 apps/openmw/mwscript/interpretercontext.cpp   | 10 +++
 apps/openmw/mwworld/worldimp.cpp              |  5 --
 docs/source/luadoc_data_paths.sh              |  1 +
 docs/source/reference/lua-scripting/api.rst   |  4 ++
 .../lua-scripting/interface_activation.rst    |  6 ++
 .../reference/lua-scripting/overview.rst      |  3 +
 files/data/CMakeLists.txt                     |  1 +
 files/data/builtin.omwscripts                 |  8 +++
 files/data/scripts/omw/activationhandlers.lua | 68 +++++++++++++++++++
 files/lua_api/openmw/core.lua                 |  1 +
 files/lua_api/openmw/interfaces.lua           |  9 ++-
 13 files changed, 126 insertions(+), 50 deletions(-)
 create mode 100644 docs/source/reference/lua-scripting/interface_activation.rst
 create mode 100644 files/data/scripts/omw/activationhandlers.lua

diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp
index 73a87fc1e0..0bc8f20dc4 100644
--- a/apps/openmw/mwlua/luabindings.cpp
+++ b/apps/openmw/mwlua/luabindings.cpp
@@ -8,6 +8,7 @@
 
 #include "../mwbase/environment.hpp"
 #include "../mwbase/statemanager.hpp"
+#include "../mwworld/action.hpp"
 #include "../mwworld/class.hpp"
 #include "../mwworld/esmstore.hpp"
 #include "../mwworld/manualref.hpp"
@@ -106,6 +107,16 @@ namespace MWLua
             // TODO: add here overloads for other records
         );
 
+        api["_runStandardActivationAction"] = [context](const GObject& object, const GObject& actor) {
+            context.mLuaManager->addAction(
+                [object, actor] {
+                    const MWWorld::Ptr& objPtr = object.ptr();
+                    const MWWorld::Ptr& actorPtr = actor.ptr();
+                    objPtr.getClass().activate(objPtr, actorPtr)->execute(actorPtr);
+                },
+                "_runStandardActivationAction");
+        };
+
         return LuaUtil::makeReadOnly(api);
     }
 
diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp
index 767b056bdf..b8d54aec23 100644
--- a/apps/openmw/mwlua/objectbindings.cpp
+++ b/apps/openmw/mwlua/objectbindings.cpp
@@ -2,7 +2,6 @@
 
 #include <components/lua/luastate.hpp>
 
-#include "../mwworld/action.hpp"
 #include "../mwworld/cellstore.hpp"
 #include "../mwworld/class.hpp"
 #include "../mwworld/containerstore.hpp"
@@ -102,44 +101,6 @@ namespace MWLua
             osg::Vec3f mRot;
         };
 
-        class ActivateAction final : public LuaManager::Action
-        {
-        public:
-            ActivateAction(LuaUtil::LuaState* state, ObjectId object, ObjectId actor)
-                : Action(state)
-                , mObject(object)
-                , mActor(actor)
-            {
-            }
-
-            void apply() const override
-            {
-                MWWorld::Ptr object = MWBase::Environment::get().getWorldModel()->getPtr(mObject);
-                if (object.isEmpty())
-                    throw std::runtime_error(std::string("Object not found: " + mObject.toString()));
-                MWWorld::Ptr actor = MWBase::Environment::get().getWorldModel()->getPtr(mActor);
-                if (actor.isEmpty())
-                    throw std::runtime_error(std::string("Actor not found: " + mActor.toString()));
-
-                if (object.getRefData().activate())
-                {
-                    MWBase::Environment::get().getLuaManager()->objectActivated(object, actor);
-                    std::unique_ptr<MWWorld::Action> action = object.getClass().activate(object, actor);
-                    action->execute(actor);
-                }
-            }
-
-            std::string toString() const override
-            {
-                return std::string("ActivateAction object=") + mObject.toString() + std::string(" actor=")
-                    + mActor.toString();
-            }
-
-        private:
-            ObjectId mObject;
-            ObjectId mActor;
-        };
-
         template <typename ObjT>
         using Cell = std::conditional_t<std::is_same_v<ObjT, LObject>, LCell, GCell>;
 
@@ -165,6 +126,7 @@ namespace MWLua
         template <class ObjectT>
         void addBasicBindings(sol::usertype<ObjectT>& objectT, const Context& context)
         {
+            objectT["id"] = sol::readonly_property([](const ObjectT& o) -> std::string { return o.id().toString(); });
             objectT["isValid"] = [](const ObjectT& o) { return !o.ptrOrNull().isEmpty(); };
             objectT["recordId"] = sol::readonly_property(
                 [](const ObjectT& o) -> std::string { return o.ptr().getCellRef().getRefId().serializeText(); });
@@ -192,13 +154,16 @@ namespace MWLua
                     { dest.id(), std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) });
             };
 
-            objectT["activateBy"] = [context](const ObjectT& o, const ObjectT& actor) {
-                uint32_t esmRecordType = actor.ptr().getType();
+            objectT["activateBy"] = [](const ObjectT& object, const ObjectT& actor) {
+                const MWWorld::Ptr& objPtr = object.ptr();
+                const MWWorld::Ptr& actorPtr = actor.ptr();
+                uint32_t esmRecordType = actorPtr.getType();
                 if (esmRecordType != ESM::REC_CREA && esmRecordType != ESM::REC_NPC_)
                     throw std::runtime_error(
                         "The argument of `activateBy` must be an actor who activates the object. Got: "
                         + actor.toString());
-                context.mLuaManager->addAction(std::make_unique<ActivateAction>(context.mLua, o.id(), actor.id()));
+                if (objPtr.getRefData().activate())
+                    MWBase::Environment::get().getLuaManager()->objectActivated(objPtr, actorPtr);
             };
 
             auto isEnabled = [](const ObjectT& o) { return o.ptr().getRefData().isEnabled(); };
diff --git a/apps/openmw/mwscript/interpretercontext.cpp b/apps/openmw/mwscript/interpretercontext.cpp
index 7508d4a785..a2b818115d 100644
--- a/apps/openmw/mwscript/interpretercontext.cpp
+++ b/apps/openmw/mwscript/interpretercontext.cpp
@@ -419,13 +419,23 @@ namespace MWScript
 
     void InterpreterContext::executeActivation(const MWWorld::Ptr& ptr, const MWWorld::Ptr& actor)
     {
+        // MWScripted activations don't go through Lua because 1-frame delay can brake mwscripts.
+#if 0
         MWBase::Environment::get().getLuaManager()->objectActivated(ptr, actor);
+
+        // TODO: Enable this branch after implementing one of the options:
+        // 1) Pause this mwscript (or maybe all mwscripts) for one frame and continue from the same
+        //     command when the activation is processed by Lua script.
+        // 2) Force Lua scripts to handle a zero-length extra frame right now, so when control
+        //     returns to the mwscript, the activation is already processed.
+#else
         std::unique_ptr<MWWorld::Action> action = (ptr.getClass().activate(ptr, actor));
         action->execute(actor);
         if (action->getTarget() != MWWorld::Ptr() && action->getTarget() != ptr)
         {
             updatePtr(ptr, action->getTarget());
         }
+#endif
     }
 
     int InterpreterContext::getMemberShort(const ESM::RefId& id, std::string_view name, bool global) const
diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp
index f1e2a762f7..ffb8311e96 100644
--- a/apps/openmw/mwworld/worldimp.cpp
+++ b/apps/openmw/mwworld/worldimp.cpp
@@ -3779,13 +3779,8 @@ namespace MWWorld
     void World::activate(const Ptr& object, const Ptr& actor)
     {
         breakInvisibility(actor);
-
         if (object.getRefData().activate())
-        {
             MWBase::Environment::get().getLuaManager()->objectActivated(object, actor);
-            std::unique_ptr<MWWorld::Action> action = object.getClass().activate(object, actor);
-            action->execute(actor);
-        }
     }
 
     struct ResetActorsVisitor
diff --git a/docs/source/luadoc_data_paths.sh b/docs/source/luadoc_data_paths.sh
index 792e957c11..91bfcbd48d 100755
--- a/docs/source/luadoc_data_paths.sh
+++ b/docs/source/luadoc_data_paths.sh
@@ -1,5 +1,6 @@
 paths=(
   openmw_aux/*lua
+  scripts/omw/activationhandlers.lua
   scripts/omw/ai.lua
   scripts/omw/playercontrols.lua
   scripts/omw/camera/camera.lua
diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst
index 061f2ce5c7..4165eb3119 100644
--- a/docs/source/reference/lua-scripting/api.rst
+++ b/docs/source/reference/lua-scripting/api.rst
@@ -27,6 +27,7 @@ Lua API reference
     openmw_aux_util
     openmw_aux_time
     openmw_aux_ui
+    interface_activation
     interface_ai
     interface_camera
     interface_controls
@@ -68,6 +69,9 @@ Interfaces of built-in scripts
   * - Interface
     - Can be used
     - Description
+  * - :ref:`Activation <Interface Activation>`
+    - by global scripts
+    - Allows to extend or override built-in activation mechanics.
   * - :ref:`AI <Interface AI>`
     - by local scripts
     - Control basic AI of NPCs and creatures.
diff --git a/docs/source/reference/lua-scripting/interface_activation.rst b/docs/source/reference/lua-scripting/interface_activation.rst
new file mode 100644
index 0000000000..ccc51ca457
--- /dev/null
+++ b/docs/source/reference/lua-scripting/interface_activation.rst
@@ -0,0 +1,6 @@
+Interface Activation
+====================
+
+.. raw:: html
+   :file: generated_html/scripts_omw_activationhandlers.html
+
diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst
index 36334492a7..c4543cd47c 100644
--- a/docs/source/reference/lua-scripting/overview.rst
+++ b/docs/source/reference/lua-scripting/overview.rst
@@ -461,6 +461,9 @@ The order in which the scripts are started is important. So if one mod should ov
   * - Interface
     - Can be used
     - Description
+  * - :ref:`Activation <Interface Activation>`
+    - by global scripts
+    - Allows to extend or override built-in activation mechanics.
   * - :ref:`AI <Interface AI>`
     - by local scripts
     - Control basic AI of NPCs and creatures.
diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt
index 64cbee0479..bc6d4b0055 100644
--- a/files/data/CMakeLists.txt
+++ b/files/data/CMakeLists.txt
@@ -63,6 +63,7 @@ set(BUILTIN_DATA_FILES
 
     builtin.omwscripts
 
+    scripts/omw/activationhandlers.lua
     scripts/omw/ai.lua
     scripts/omw/camera/camera.lua
     scripts/omw/camera/head_bobbing.lua
diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts
index a3eed5ea16..46a1f3f32f 100644
--- a/files/data/builtin.omwscripts
+++ b/files/data/builtin.omwscripts
@@ -1,9 +1,17 @@
+# UI framework
 PLAYER: scripts/omw/mwui/init.lua
+
+# Settings framework
 GLOBAL: scripts/omw/settings/global.lua
 PLAYER: scripts/omw/settings/player.lua
+
+# Mechanics
+GLOBAL: scripts/omw/activationhandlers.lua
 PLAYER: scripts/omw/playercontrols.lua
 PLAYER: scripts/omw/camera/camera.lua
 NPC,CREATURE: scripts/omw/ai.lua
+
+# Lua console
 PLAYER: scripts/omw/console/player.lua
 GLOBAL: scripts/omw/console/global.lua
 CUSTOM: scripts/omw/console/local.lua
diff --git a/files/data/scripts/omw/activationhandlers.lua b/files/data/scripts/omw/activationhandlers.lua
new file mode 100644
index 0000000000..c2ac4db884
--- /dev/null
+++ b/files/data/scripts/omw/activationhandlers.lua
@@ -0,0 +1,68 @@
+local types = require('openmw.types')
+local world = require('openmw.world')
+
+local handlersPerObject = {}
+local handlersPerType = {}
+
+local function onActivate(obj, actor)
+    local handlers = handlersPerObject[obj.id]
+    if handlers then
+        for i = #handlers, 1, -1 do
+            if handlers[i](obj, actor) == false then
+                return -- skip other handlers
+            end
+        end
+    end
+    handlers = handlersPerType[obj.type]
+    if handlers then
+        for i = #handlers, 1, -1 do
+            if handlers[i](obj, actor) == false then
+                return -- skip other handlers
+            end
+        end
+    end
+    world._runStandardActivationAction(obj, actor)
+end
+
+return {
+    interfaceName = 'Activation',
+    ---
+    -- @module Activation
+    -- @usage require('openmw.interfaces').Activation
+    interface = {
+        --- Interface version
+        -- @field [parent=#Activation] #number version
+        version = 0,
+
+        --- Add new activation handler for a specific object.
+        -- If `handler(object, actor)` returns false, other handlers for
+        -- the same object (including type handlers) will be skipped.
+        -- @function [parent=#Activation] addHandlerForObject
+        -- @param openmw.core#GameObject obj The object.
+        -- @param #function handler The handler.
+        addHandlerForObject = function(obj, handler)
+            local handlers = handlersPerObject[obj.id]
+            if handlers == nil then
+                handlers = {}
+                handlersPerObject[obj.id] = handlers
+            end
+            handlers[#handlers + 1] = handler
+        end,
+
+        --- Add new activation handler for a type of objects.
+        -- If `handler(object, actor)` returns false, other handlers for
+        -- the same object (including type handlers) will be skipped.
+        -- @function [parent=#Activation] addHandlerForType
+        -- @param #userdata type A type from the `openmw.types` package.
+        -- @param #function handler The handler.
+        addHandlerForType = function(type, handler)
+            local handlers = handlersPerType[type]
+            if handlers == nil then
+                handlers = {}
+                handlersPerType[type] = handlers
+            end
+            handlers[#handlers + 1] = handler
+        end,
+    },
+    engineHandlers = { onActivate = onActivate },
+}
diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua
index 361bb9a3d5..cf7838c8dd 100644
--- a/files/lua_api/openmw/core.lua
+++ b/files/lua_api/openmw/core.lua
@@ -107,6 +107,7 @@
 -- Player, actors, items, and statics are game objects.
 -- @type GameObject
 -- @extends #userdata
+-- @field #string id A unique id of this object (not record id), can be used as a key in a table.
 -- @field #boolean enabled Whether the object is enabled or disabled. Global scripts can set the value. Items in containers or inventories can't be disabled.
 -- @field openmw.util#Vector3 position Object position.
 -- @field openmw.util#Vector3 rotation Object rotation (ZXY order).
diff --git a/files/lua_api/openmw/interfaces.lua b/files/lua_api/openmw/interfaces.lua
index 3a2e5627c2..36032acb2f 100644
--- a/files/lua_api/openmw/interfaces.lua
+++ b/files/lua_api/openmw/interfaces.lua
@@ -3,16 +3,19 @@
 -- @usage local I = require('openmw.interfaces')
 
 ---
--- @field [parent=#interfaces] scripts.omw.camera.camera#scripts.omw.camera.camera Camera
+-- @field [parent=#interfaces] scripts.omw.ai#scripts.omw.activationhandlers Activation
 
 ---
--- @field [parent=#interfaces] scripts.omw.settings.player#scripts.omw.settings.player Settings
+-- @field [parent=#interfaces] scripts.omw.ai#scripts.omw.ai AI
+
+---
+-- @field [parent=#interfaces] scripts.omw.camera.camera#scripts.omw.camera.camera Camera
 
 ---
 -- @field [parent=#interfaces] scripts.omw.mwui.init#scripts.omw.mwui.init MWUI
 
 ---
--- @field [parent=#interfaces] scripts.omw.ai#scripts.omw.ai AI
+-- @field [parent=#interfaces] scripts.omw.settings.player#scripts.omw.settings.player Settings
 
 ---
 -- @function [parent=#interfaces] __index