feature: scripted listener actions (#11468) f3a89390cb

Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head
index e1626a4..c880cd3 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-9112280455e25db4649d446601f443c763151649
+f3a89390cb428a5ea841d21de91f9cb2adc312df
diff --git a/dev/defs/animation/scripted_listener_action.json b/dev/defs/animation/scripted_listener_action.json
new file mode 100644
index 0000000..8ee9d6d
--- /dev/null
+++ b/dev/defs/animation/scripted_listener_action.json
@@ -0,0 +1,20 @@
+{
+  "name": "ScriptedListenerAction",
+  "key": {
+    "int": 646,
+    "string": "scriptedlisteneraction"
+  },
+  "extends": "animation/listener_action.json",
+  "properties": {
+    "scriptAssetId": {
+      "type": "Id",
+      "typeRuntime": "uint",
+      "initialValue": "Core.missingId",
+      "initialValueRuntime": "-1",
+      "key": {
+        "int": 930,
+        "string": "scriptassetid"
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/include/rive/animation/listener_action.hpp b/include/rive/animation/listener_action.hpp
index 691ab0f..a66df47 100644
--- a/include/rive/animation/listener_action.hpp
+++ b/include/rive/animation/listener_action.hpp
@@ -12,7 +12,8 @@
     StatusCode import(ImportStack& importStack) override;
     virtual void perform(StateMachineInstance* stateMachineInstance,
                          Vec2D position,
-                         Vec2D previousPosition) const = 0;
+                         Vec2D previousPosition,
+                         int pointerId) const = 0;
 };
 } // namespace rive
 
diff --git a/include/rive/animation/listener_align_target.hpp b/include/rive/animation/listener_align_target.hpp
index 2a150d6..aacae88 100644
--- a/include/rive/animation/listener_align_target.hpp
+++ b/include/rive/animation/listener_align_target.hpp
@@ -9,7 +9,8 @@
 public:
     void perform(StateMachineInstance* stateMachineInstance,
                  Vec2D position,
-                 Vec2D previousPosition) const override;
+                 Vec2D previousPosition,
+                 int pointerId) const override;
 };
 } // namespace rive
 
diff --git a/include/rive/animation/listener_bool_change.hpp b/include/rive/animation/listener_bool_change.hpp
index a6f25c2..2d05804 100644
--- a/include/rive/animation/listener_bool_change.hpp
+++ b/include/rive/animation/listener_bool_change.hpp
@@ -12,7 +12,8 @@
     bool validateNestedInputType(const NestedInput* input) const override;
     void perform(StateMachineInstance* stateMachineInstance,
                  Vec2D position,
-                 Vec2D previousPosition) const override;
+                 Vec2D previousPosition,
+                 int pointerId) const override;
 };
 } // namespace rive
 
diff --git a/include/rive/animation/listener_fire_event.hpp b/include/rive/animation/listener_fire_event.hpp
index 4e4a7d2..40fadbf 100644
--- a/include/rive/animation/listener_fire_event.hpp
+++ b/include/rive/animation/listener_fire_event.hpp
@@ -9,7 +9,8 @@
 public:
     void perform(StateMachineInstance* stateMachineInstance,
                  Vec2D position,
-                 Vec2D previousPosition) const override;
+                 Vec2D previousPosition,
+                 int pointerId) const override;
 };
 } // namespace rive
 
diff --git a/include/rive/animation/listener_number_change.hpp b/include/rive/animation/listener_number_change.hpp
index 613d7d3..619bdfb 100644
--- a/include/rive/animation/listener_number_change.hpp
+++ b/include/rive/animation/listener_number_change.hpp
@@ -12,7 +12,8 @@
     bool validateNestedInputType(const NestedInput* input) const override;
     void perform(StateMachineInstance* stateMachineInstance,
                  Vec2D position,
-                 Vec2D previousPosition) const override;
+                 Vec2D previousPosition,
+                 int pointerId) const override;
 };
 } // namespace rive
 
diff --git a/include/rive/animation/listener_trigger_change.hpp b/include/rive/animation/listener_trigger_change.hpp
index 005f472..838b332 100644
--- a/include/rive/animation/listener_trigger_change.hpp
+++ b/include/rive/animation/listener_trigger_change.hpp
@@ -12,7 +12,8 @@
     bool validateNestedInputType(const NestedInput* input) const override;
     void perform(StateMachineInstance* stateMachineInstance,
                  Vec2D position,
-                 Vec2D previousPosition) const override;
+                 Vec2D previousPosition,
+                 int pointerId) const override;
 };
 } // namespace rive
 
diff --git a/include/rive/animation/listener_viewmodel_change.hpp b/include/rive/animation/listener_viewmodel_change.hpp
index a58ef49..6f4c8eb 100644
--- a/include/rive/animation/listener_viewmodel_change.hpp
+++ b/include/rive/animation/listener_viewmodel_change.hpp
@@ -11,7 +11,8 @@
     ~ListenerViewModelChange();
     void perform(StateMachineInstance* stateMachineInstance,
                  Vec2D position,
-                 Vec2D previousPosition) const override;
+                 Vec2D previousPosition,
+                 int pointerId) const override;
     StatusCode import(ImportStack& importStack) override;
 
 private:
diff --git a/include/rive/animation/scripted_listener_action.hpp b/include/rive/animation/scripted_listener_action.hpp
new file mode 100644
index 0000000..b464337
--- /dev/null
+++ b/include/rive/animation/scripted_listener_action.hpp
@@ -0,0 +1,36 @@
+#ifndef _RIVE_SCRIPTED_LISTENER_ACTION_HPP_
+#define _RIVE_SCRIPTED_LISTENER_ACTION_HPP_
+#include "rive/generated/animation/scripted_listener_action_base.hpp"
+#include "rive/scripted/scripted_object.hpp"
+#include <stdio.h>
+namespace rive
+{
+class ScriptedListenerAction : public ScriptedListenerActionBase,
+                               public ScriptedObject
+{
+public:
+    void perform(StateMachineInstance* stateMachineInstance,
+                 Vec2D position,
+                 Vec2D previousPosition,
+                 int pointerId) const override;
+    void performStateful(StateMachineInstance* stateMachineInstance,
+                         Vec2D position,
+                         Vec2D previousPosition,
+                         int pointerId) const;
+
+    uint32_t assetId() override { return scriptAssetId(); }
+    bool addScriptedDirt(ComponentDirt value, bool recurse = false) override
+    {
+        return false;
+    }
+    ScriptProtocol scriptProtocol() override
+    {
+        return ScriptProtocol::listenerAction;
+    }
+    Component* component() override { return nullptr; }
+    StatusCode import(ImportStack& importStack) override;
+    Core* clone() const override;
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp
index 840c77e..641630c 100644
--- a/include/rive/animation/state_machine_instance.hpp
+++ b/include/rive/animation/state_machine_instance.hpp
@@ -40,6 +40,7 @@
 class BindableProperty;
 class HitDrawable;
 class ListenerViewModel;
+class ScriptedListenerAction;
 typedef void (*DataBindChanged)();
 
 #ifdef WITH_RIVE_TOOLS
@@ -205,6 +206,7 @@
     bool hasListeners() { return m_hitComponents.size() > 0; }
     void clearDataContext();
     void internalDataContext(DataContext* dataContext);
+    ScriptedObject* scriptedObject(const ScriptedObject*);
 #ifdef TESTING
     size_t hitComponentsCount() { return m_hitComponents.size(); };
     HitComponent* hitComponent(size_t index)
@@ -238,6 +240,8 @@
     std::vector<ListenerViewModel*> m_reportingListenerViewModels;
     std::unordered_map<BindableProperty*, BindableProperty*>
         m_bindablePropertyInstances;
+    std::unordered_map<const ScriptedObject*, ScriptedObject*>
+        m_scriptedListenerActionsMap;
     std::unordered_map<BindableProperty*, DataBind*>
         m_bindableDataBindsToTarget;
     std::unordered_map<BindableProperty*, DataBind*>
diff --git a/include/rive/animation/state_machine_listener.hpp b/include/rive/animation/state_machine_listener.hpp
index 27e62f6..ac1c9a6 100644
--- a/include/rive/animation/state_machine_listener.hpp
+++ b/include/rive/animation/state_machine_listener.hpp
@@ -31,7 +31,8 @@
 
     void performChanges(StateMachineInstance* stateMachineInstance,
                         Vec2D position,
-                        Vec2D previousPosition) const;
+                        Vec2D previousPosition,
+                        int pointerId) const;
     void decodeViewModelPathIds(Span<const uint8_t> value) override;
     void copyViewModelPathIds(const StateMachineListenerBase& object) override;
     std::vector<uint32_t> viewModelPathIdsBuffer() const;
diff --git a/include/rive/assets/script_asset.hpp b/include/rive/assets/script_asset.hpp
index 8ae0501..6de0360 100644
--- a/include/rive/assets/script_asset.hpp
+++ b/include/rive/assets/script_asset.hpp
@@ -25,7 +25,8 @@
     node,
     layout,
     converter,
-    pathEffect
+    pathEffect,
+    listenerAction
 };
 
 #ifdef WITH_RIVE_SCRIPTING
diff --git a/include/rive/generated/animation/scripted_listener_action_base.hpp b/include/rive/generated/animation/scripted_listener_action_base.hpp
new file mode 100644
index 0000000..9a92033
--- /dev/null
+++ b/include/rive/generated/animation/scripted_listener_action_base.hpp
@@ -0,0 +1,71 @@
+#ifndef _RIVE_SCRIPTED_LISTENER_ACTION_BASE_HPP_
+#define _RIVE_SCRIPTED_LISTENER_ACTION_BASE_HPP_
+#include "rive/animation/listener_action.hpp"
+#include "rive/core/field_types/core_uint_type.hpp"
+namespace rive
+{
+class ScriptedListenerActionBase : public ListenerAction
+{
+protected:
+    typedef ListenerAction Super;
+
+public:
+    static const uint16_t typeKey = 646;
+
+    /// Helper to quickly determine if a core object extends another without
+    /// RTTI at runtime.
+    bool isTypeOf(uint16_t typeKey) const override
+    {
+        switch (typeKey)
+        {
+            case ScriptedListenerActionBase::typeKey:
+            case ListenerActionBase::typeKey:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    uint16_t coreType() const override { return typeKey; }
+
+    static const uint16_t scriptAssetIdPropertyKey = 930;
+
+protected:
+    uint32_t m_ScriptAssetId = -1;
+
+public:
+    inline uint32_t scriptAssetId() const { return m_ScriptAssetId; }
+    void scriptAssetId(uint32_t value)
+    {
+        if (m_ScriptAssetId == value)
+        {
+            return;
+        }
+        m_ScriptAssetId = value;
+        scriptAssetIdChanged();
+    }
+
+    Core* clone() const override;
+    void copy(const ScriptedListenerActionBase& object)
+    {
+        m_ScriptAssetId = object.m_ScriptAssetId;
+        ListenerAction::copy(object);
+    }
+
+    bool deserialize(uint16_t propertyKey, BinaryReader& reader) override
+    {
+        switch (propertyKey)
+        {
+            case scriptAssetIdPropertyKey:
+                m_ScriptAssetId = CoreUintType::deserialize(reader);
+                return true;
+        }
+        return ListenerAction::deserialize(propertyKey, reader);
+    }
+
+protected:
+    virtual void scriptAssetIdChanged() {}
+};
+} // namespace rive
+
+#endif
\ No newline at end of file
diff --git a/include/rive/generated/core_registry.hpp b/include/rive/generated/core_registry.hpp
index f868968..4a541ae 100644
--- a/include/rive/generated/core_registry.hpp
+++ b/include/rive/generated/core_registry.hpp
@@ -50,6 +50,7 @@
 #include "rive/animation/nested_simple_animation.hpp"
 #include "rive/animation/nested_state_machine.hpp"
 #include "rive/animation/nested_trigger.hpp"
+#include "rive/animation/scripted_listener_action.hpp"
 #include "rive/animation/state_machine.hpp"
 #include "rive/animation/state_machine_bool.hpp"
 #include "rive/animation/state_machine_component.hpp"
@@ -477,6 +478,8 @@
                 return new AnimationState();
             case NestedTriggerBase::typeKey:
                 return new NestedTrigger();
+            case ScriptedListenerActionBase::typeKey:
+                return new ScriptedListenerAction();
             case KeyedObjectBase::typeKey:
                 return new KeyedObject();
             case AnimationBase::typeKey:
@@ -1194,6 +1197,9 @@
             case NestedInputBase::inputIdPropertyKey:
                 object->as<NestedInputBase>()->inputId(value);
                 break;
+            case ScriptedListenerActionBase::scriptAssetIdPropertyKey:
+                object->as<ScriptedListenerActionBase>()->scriptAssetId(value);
+                break;
             case KeyedObjectBase::objectIdPropertyKey:
                 object->as<KeyedObjectBase>()->objectId(value);
                 break;
@@ -2737,6 +2743,9 @@
                 return object->as<AnimationStateBase>()->animationId();
             case NestedInputBase::inputIdPropertyKey:
                 return object->as<NestedInputBase>()->inputId();
+            case ScriptedListenerActionBase::scriptAssetIdPropertyKey:
+                return object->as<ScriptedListenerActionBase>()
+                    ->scriptAssetId();
             case KeyedObjectBase::objectIdPropertyKey:
                 return object->as<KeyedObjectBase>()->objectId();
             case BlendAnimationBase::animationIdPropertyKey:
@@ -3725,6 +3734,7 @@
             case ListenerInputChangeBase::nestedInputIdPropertyKey:
             case AnimationStateBase::animationIdPropertyKey:
             case NestedInputBase::inputIdPropertyKey:
+            case ScriptedListenerActionBase::scriptAssetIdPropertyKey:
             case KeyedObjectBase::objectIdPropertyKey:
             case BlendAnimationBase::animationIdPropertyKey:
             case BlendAnimationDirectBase::inputIdPropertyKey:
@@ -4377,6 +4387,8 @@
                 return object->is<AnimationStateBase>();
             case NestedInputBase::inputIdPropertyKey:
                 return object->is<NestedInputBase>();
+            case ScriptedListenerActionBase::scriptAssetIdPropertyKey:
+                return object->is<ScriptedListenerActionBase>();
             case KeyedObjectBase::objectIdPropertyKey:
                 return object->is<KeyedObjectBase>();
             case BlendAnimationBase::animationIdPropertyKey:
diff --git a/include/rive/scripted/scripted_object.hpp b/include/rive/scripted/scripted_object.hpp
index 568a171..c712716 100644
--- a/include/rive/scripted/scripted_object.hpp
+++ b/include/rive/scripted/scripted_object.hpp
@@ -33,6 +33,8 @@
 #ifdef WITH_RIVE_TOOLS
     bool hasValidVM();
 #endif
+private:
+    DataContext* m_dataContext = nullptr;
 
 public:
     virtual ~ScriptedObject() { scriptDispose(); }
@@ -48,7 +50,8 @@
     void scriptUpdate();
     void reinit();
     virtual void markNeedsUpdate();
-    virtual DataContext* dataContext() { return nullptr; }
+    virtual DataContext* dataContext() { return m_dataContext; }
+    void dataContext(DataContext* value) { m_dataContext = value; }
 #ifdef WITH_RIVE_SCRIPTING
     virtual bool scriptInit(lua_State* state);
     lua_State* state() { return m_state; }
diff --git a/src/animation/listener_align_target.cpp b/src/animation/listener_align_target.cpp
index 415321e..4cc53e4 100644
--- a/src/animation/listener_align_target.cpp
+++ b/src/animation/listener_align_target.cpp
@@ -7,7 +7,8 @@
 
 void ListenerAlignTarget::perform(StateMachineInstance* stateMachineInstance,
                                   Vec2D position,
-                                  Vec2D previousPosition) const
+                                  Vec2D previousPosition,
+                                  int pointerId) const
 {
     auto coreTarget = stateMachineInstance->artboard()->resolve(targetId());
     if (coreTarget == nullptr || !coreTarget->is<Node>())
diff --git a/src/animation/listener_bool_change.cpp b/src/animation/listener_bool_change.cpp
index 20bdf0e..e1c1c12 100644
--- a/src/animation/listener_bool_change.cpp
+++ b/src/animation/listener_bool_change.cpp
@@ -26,7 +26,8 @@
 
 void ListenerBoolChange::perform(StateMachineInstance* stateMachineInstance,
                                  Vec2D position,
-                                 Vec2D previousPosition) const
+                                 Vec2D previousPosition,
+                                 int pointerId) const
 {
     if (nestedInputId() != Core::emptyId)
     {
diff --git a/src/animation/listener_fire_event.cpp b/src/animation/listener_fire_event.cpp
index bd6d074..e1821d3 100644
--- a/src/animation/listener_fire_event.cpp
+++ b/src/animation/listener_fire_event.cpp
@@ -6,7 +6,8 @@
 
 void ListenerFireEvent::perform(StateMachineInstance* stateMachineInstance,
                                 Vec2D position,
-                                Vec2D previousPosition) const
+                                Vec2D previousPosition,
+                                int pointerId) const
 {
     auto coreEvent = stateMachineInstance->artboard()->resolve(eventId());
     if (coreEvent == nullptr || !coreEvent->is<Event>())
diff --git a/src/animation/listener_number_change.cpp b/src/animation/listener_number_change.cpp
index a57d0df..c0716d2 100644
--- a/src/animation/listener_number_change.cpp
+++ b/src/animation/listener_number_change.cpp
@@ -29,7 +29,8 @@
 
 void ListenerNumberChange::perform(StateMachineInstance* stateMachineInstance,
                                    Vec2D position,
-                                   Vec2D previousPosition) const
+                                   Vec2D previousPosition,
+                                   int pointerId) const
 {
     if (nestedInputId() != Core::emptyId)
     {
diff --git a/src/animation/listener_trigger_change.cpp b/src/animation/listener_trigger_change.cpp
index f8d8584..178d199 100644
--- a/src/animation/listener_trigger_change.cpp
+++ b/src/animation/listener_trigger_change.cpp
@@ -30,7 +30,8 @@
 
 void ListenerTriggerChange::perform(StateMachineInstance* stateMachineInstance,
                                     Vec2D position,
-                                    Vec2D previousPosition) const
+                                    Vec2D previousPosition,
+                                    int pointerId) const
 {
     if (nestedInputId() != Core::emptyId)
     {
diff --git a/src/animation/listener_viewmodel_change.cpp b/src/animation/listener_viewmodel_change.cpp
index 5244d9c..c4bd477 100644
--- a/src/animation/listener_viewmodel_change.cpp
+++ b/src/animation/listener_viewmodel_change.cpp
@@ -37,7 +37,8 @@
 void ListenerViewModelChange::perform(
     StateMachineInstance* stateMachineInstance,
     Vec2D position,
-    Vec2D previousPosition) const
+    Vec2D previousPosition,
+    int pointerId) const
 {
     // Get the bindable property instance from the state machine instance
     // context
diff --git a/src/animation/scripted_listener_action.cpp b/src/animation/scripted_listener_action.cpp
new file mode 100644
index 0000000..4b3b959
--- /dev/null
+++ b/src/animation/scripted_listener_action.cpp
@@ -0,0 +1,86 @@
+#include "rive/animation/scripted_listener_action.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+
+using namespace rive;
+
+// Note: performStateful is the actual instance of the ScriptedListenerAction
+// that will run the script. perform itself will look for the map between the
+// stateless and the stateful instances of this class.
+void ScriptedListenerAction::performStateful(
+    StateMachineInstance* stateMachineInstance,
+    Vec2D position,
+    Vec2D previousPosition,
+    int pointerId) const
+{
+#ifdef WITH_RIVE_SCRIPTING
+    if (m_state == nullptr)
+    {
+        return;
+    }
+    // Stack: []
+    rive_lua_pushRef(m_state, m_self);
+    // Stack: [self]
+    lua_getfield(m_state, -1, "perform");
+
+    // Stack: [self, field]
+    lua_pushvalue(m_state, -2);
+
+    // Stack: [self, field, self]
+    lua_newrive<ScriptedPointerEvent>(m_state, pointerId, position);
+
+    // Stack: [self, field, self, pointerEvent]
+    if (static_cast<lua_Status>(rive_lua_pcall(m_state, 2, 0)) == LUA_OK)
+    {
+        rive_lua_pop(m_state, 1);
+    }
+    else
+    {
+        rive_lua_pop(m_state, 2);
+    }
+#endif
+}
+
+void ScriptedListenerAction::perform(StateMachineInstance* stateMachineInstance,
+                                     Vec2D position,
+                                     Vec2D previousPosition,
+                                     int pointerId) const
+{
+#ifdef WITH_RIVE_SCRIPTING
+    auto scriptedObject = stateMachineInstance->scriptedObject(this);
+    if (scriptedObject != nullptr)
+    {
+        auto statefulListenerAction =
+            static_cast<ScriptedListenerAction*>(scriptedObject);
+        statefulListenerAction->performStateful(stateMachineInstance,
+                                                position,
+                                                previousPosition,
+                                                pointerId);
+    }
+#endif
+}
+
+StatusCode ScriptedListenerAction::import(ImportStack& importStack)
+{
+    auto result = registerReferencer(importStack);
+    if (result != StatusCode::Ok)
+    {
+        return result;
+    }
+    return Super::import(importStack);
+}
+
+Core* ScriptedListenerAction::clone() const
+{
+    ScriptedListenerAction* twin =
+        ScriptedListenerActionBase::clone()->as<ScriptedListenerAction>();
+    if (m_fileAsset != nullptr)
+    {
+        twin->setAsset(m_fileAsset);
+    }
+    for (auto prop : m_customProperties)
+    {
+        auto clonedValue = prop->clone()->as<CustomProperty>();
+        twin->addProperty(clonedValue);
+    }
+    return twin;
+}
\ No newline at end of file
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp
index e7d02ee..964b7a2 100644
--- a/src/animation/state_machine_instance.cpp
+++ b/src/animation/state_machine_instance.cpp
@@ -19,6 +19,8 @@
 #include "rive/animation/state_machine_trigger.hpp"
 #include "rive/animation/state_machine.hpp"
 #include "rive/animation/state_transition.hpp"
+#include "rive/animation/listener_action.hpp"
+#include "rive/animation/scripted_listener_action.hpp"
 #include "rive/animation/transition_condition.hpp"
 #include "rive/animation/transition_comparator.hpp"
 #include "rive/animation/transition_property_viewmodel_comparator.hpp"
@@ -1480,9 +1482,41 @@
             this);
         m_hitComponents.push_back(std::move(hc));
     }
+    // Initialize local instances of ScriptedListenerActions
+    for (std::size_t i = 0; i < machine->listenerCount(); i++)
+    {
+        auto listener = machine->listener(i);
+
+        for (std::size_t j = 0; j < listener->actionCount(); j++)
+        {
+            auto action = listener->action(j);
+            if (action->is<ScriptedListenerAction>())
+            {
+                auto scriptedListenerAction =
+                    action->as<ScriptedListenerAction>();
+                auto scriptedListenerActionClone =
+                    static_cast<ScriptedListenerAction*>(
+                        scriptedListenerAction->clone());
+                scriptedListenerActionClone->reinit();
+                m_scriptedListenerActionsMap[scriptedListenerAction] =
+                    scriptedListenerActionClone;
+            }
+        }
+    }
     sortHitComponents();
 }
 
+ScriptedObject* StateMachineInstance::scriptedObject(
+    const ScriptedObject* source)
+{
+    auto itr = m_scriptedListenerActionsMap.find(source);
+    if (itr != m_scriptedListenerActionsMap.end())
+    {
+        return itr->second;
+    }
+    return nullptr;
+}
+
 StateMachineInstance::~StateMachineInstance()
 {
     unbind();
@@ -1506,6 +1540,12 @@
         delete listenerViewModel;
     }
     m_bindablePropertyInstances.clear();
+    for (auto& pair : m_scriptedListenerActionsMap)
+    {
+        delete pair.second;
+        pair.second = nullptr;
+    }
+    m_scriptedListenerActionsMap.clear();
 }
 
 void StateMachineInstance::removeEventListeners()
@@ -1816,6 +1856,10 @@
     {
         listenerViewModel->bindFromContext(dataContext);
     }
+    for (auto& scriptedObjectItr : m_scriptedListenerActionsMap)
+    {
+        scriptedObjectItr.second->dataContext(dataContext);
+    }
 }
 
 void StateMachineInstance::rebind()
@@ -1955,7 +1999,8 @@
         {
             listenerViewModel->listener()->performChanges(this,
                                                           Vec2D(),
-                                                          Vec2D());
+                                                          Vec2D(),
+                                                          0);
         }
     }
 }
@@ -2011,7 +2056,7 @@
                         sourceArtboard->resolve(listener->eventId());
                     if (listenerEvent == event.event())
                     {
-                        listener->performChanges(this, Vec2D(), Vec2D());
+                        listener->performChanges(this, Vec2D(), Vec2D(), 0);
                         break;
                     }
                 }
diff --git a/src/animation/state_machine_listener.cpp b/src/animation/state_machine_listener.cpp
index 3595f6b..7b39e3a 100644
--- a/src/animation/state_machine_listener.cpp
+++ b/src/animation/state_machine_listener.cpp
@@ -44,11 +44,15 @@
 void StateMachineListener::performChanges(
     StateMachineInstance* stateMachineInstance,
     Vec2D position,
-    Vec2D previousPosition) const
+    Vec2D previousPosition,
+    int pointerId) const
 {
     for (auto& action : m_actions)
     {
-        action->perform(stateMachineInstance, position, previousPosition);
+        action->perform(stateMachineInstance,
+                        position,
+                        previousPosition,
+                        pointerId);
     }
 }
 
diff --git a/src/assets/script_asset.cpp b/src/assets/script_asset.cpp
index c6eb4f6..1542c46 100644
--- a/src/assets/script_asset.cpp
+++ b/src/assets/script_asset.cpp
@@ -66,7 +66,8 @@
     if (scriptProtocol == ScriptProtocol::node ||
         scriptProtocol == ScriptProtocol::layout ||
         scriptProtocol == ScriptProtocol::converter ||
-        scriptProtocol == ScriptProtocol::pathEffect)
+        scriptProtocol == ScriptProtocol::pathEffect ||
+        scriptProtocol == ScriptProtocol::listenerAction)
     {
         if (static_cast<lua_Type>(lua_getfield(state, -1, "update")) ==
             LUA_TFUNCTION)
diff --git a/src/generated/animation/scripted_listener_action_base.cpp b/src/generated/animation/scripted_listener_action_base.cpp
new file mode 100644
index 0000000..6ac036e
--- /dev/null
+++ b/src/generated/animation/scripted_listener_action_base.cpp
@@ -0,0 +1,11 @@
+#include "rive/generated/animation/scripted_listener_action_base.hpp"
+#include "rive/animation/scripted_listener_action.hpp"
+
+using namespace rive;
+
+Core* ScriptedListenerActionBase::clone() const
+{
+    auto cloned = new ScriptedListenerAction();
+    cloned->copy(*this);
+    return cloned;
+}
diff --git a/src/listener_group.cpp b/src/listener_group.cpp
index dec81ac..217e6a1 100644
--- a/src/listener_group.cpp
+++ b/src/listener_group.cpp
@@ -191,7 +191,8 @@
         _listener->performChanges(
             stateMachineInstance,
             position,
-            Vec2D(previousPosition->x, previousPosition->y));
+            Vec2D(previousPosition->x, previousPosition->y),
+            pointerId);
         stateMachineInstance->markNeedsAdvance();
         consume();
     }
@@ -206,7 +207,8 @@
         _listener->performChanges(
             stateMachineInstance,
             position,
-            Vec2D(previousPosition->x, previousPosition->y));
+            Vec2D(previousPosition->x, previousPosition->y),
+            pointerId);
         stateMachineInstance->markNeedsAdvance();
         consume();
     }
@@ -221,7 +223,8 @@
         _listener->performChanges(
             stateMachineInstance,
             position,
-            Vec2D(previousPosition->x, previousPosition->y));
+            Vec2D(previousPosition->x, previousPosition->y),
+            pointerId);
         stateMachineInstance->markNeedsAdvance();
         if (!m_hasDragged)
         {
diff --git a/src/lua/lua_state.cpp b/src/lua/lua_state.cpp
index 0327364..bfdd7cb 100644
--- a/src/lua/lua_state.cpp
+++ b/src/lua/lua_state.cpp
@@ -14,7 +14,14 @@
     ViewModel* viewModel = (ViewModel*)lua_touserdata(L, lua_upvalueindex(1));
     if (viewModel)
     {
+
+#ifdef WITH_RIVE_TOOLS
+        viewModel->file()->triggerViewModelCreatedCallback(true);
+#endif
         auto instance = viewModel->createInstance();
+#ifdef WITH_RIVE_TOOLS
+        viewModel->file()->triggerViewModelCreatedCallback(false);
+#endif
         lua_newrive<ScriptedViewModel>(L, L, ref_rcp(viewModel), instance);
         return 1;
     }
diff --git a/tests/unit_tests/assets/scripted_listener_action.riv b/tests/unit_tests/assets/scripted_listener_action.riv
new file mode 100644
index 0000000..bdb08d8
--- /dev/null
+++ b/tests/unit_tests/assets/scripted_listener_action.riv
Binary files differ
diff --git a/tests/unit_tests/runtime/scripting/scripting_listener_action_test.cpp b/tests/unit_tests/runtime/scripting/scripting_listener_action_test.cpp
new file mode 100644
index 0000000..38889b8
--- /dev/null
+++ b/tests/unit_tests/runtime/scripting/scripting_listener_action_test.cpp
@@ -0,0 +1,46 @@
+
+#include "catch.hpp"
+#include "scripting_test_utilities.hpp"
+#include "rive/animation/state_machine_instance.hpp"
+#include "rive/lua/rive_lua_libs.hpp"
+#include "rive/viewmodel/viewmodel_instance_string.hpp"
+#include "rive_file_reader.hpp"
+
+using namespace rive;
+
+TEST_CASE("scripted listener action", "[silver]")
+{
+    rive::SerializingFactory silver;
+    auto file = ReadRiveFile("assets/scripted_listener_action.riv", &silver);
+    auto artboard = file->artboardDefault();
+
+    silver.frameSize(artboard->width(), artboard->height());
+    REQUIRE(artboard != nullptr);
+    auto stateMachine = artboard->stateMachineAt(0);
+
+    auto vmi = file->createViewModelInstance(artboard.get());
+    stateMachine->bindViewModelInstance(vmi);
+    stateMachine->advanceAndApply(0.1f);
+
+    auto renderer = silver.makeRenderer();
+    artboard->draw(renderer.get());
+
+    silver.addFrame();
+
+    stateMachine->pointerDown(rive::Vec2D(200.0f, 20.0f), 1);
+    stateMachine->pointerUp(rive::Vec2D(200.0f, 20.0f), 1);
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+
+    stateMachine->pointerDown(rive::Vec2D(300.0f, 20.0f), 2);
+    stateMachine->pointerUp(rive::Vec2D(300.0f, 20.0f), 2);
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+
+    stateMachine->pointerDown(rive::Vec2D(400.0f, 20.0f), 3);
+    stateMachine->pointerUp(rive::Vec2D(400.0f, 20.0f), 3);
+    stateMachine->advanceAndApply(0.016f);
+    artboard->draw(renderer.get());
+
+    CHECK(silver.matches("scripted_listener_action"));
+}
\ No newline at end of file
diff --git a/tests/unit_tests/silvers/scripted_listener_action.sriv b/tests/unit_tests/silvers/scripted_listener_action.sriv
new file mode 100644
index 0000000..961305a
--- /dev/null
+++ b/tests/unit_tests/silvers/scripted_listener_action.sriv
Binary files differ