feature: add scripted transition condition protocol (#11479) 853b2a08b5 * feature: add scripted transition condition protocol Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head index a1a4e90..93a602c 100644 --- a/.rive_head +++ b/.rive_head
@@ -1 +1 @@ -3253c0beae3bdc2d68db7cf52eb4490e17276c58 +853b2a08b5743038ac40d2d4b80697d8983a23df
diff --git a/dev/defs/animation/scripted_transition_condition.json b/dev/defs/animation/scripted_transition_condition.json new file mode 100644 index 0000000..3ffba7e --- /dev/null +++ b/dev/defs/animation/scripted_transition_condition.json
@@ -0,0 +1,20 @@ +{ + "name": "ScriptedTransitionCondition", + "key": { + "int": 647, + "string": "scriptedtransitioncondition" + }, + "extends": "animation/transition_condition.json", + "properties": { + "scriptAssetId": { + "type": "Id", + "typeRuntime": "uint", + "initialValue": "Core.missingId", + "initialValueRuntime": "-1", + "key": { + "int": 931, + "string": "scriptassetid" + } + } + } +} \ No newline at end of file
diff --git a/include/rive/animation/scripted_listener_action.hpp b/include/rive/animation/scripted_listener_action.hpp index b464337..32e615f 100644 --- a/include/rive/animation/scripted_listener_action.hpp +++ b/include/rive/animation/scripted_listener_action.hpp
@@ -30,6 +30,7 @@ Component* component() override { return nullptr; } StatusCode import(ImportStack& importStack) override; Core* clone() const override; + ScriptedObject* cloneScriptedObject() const override; }; } // namespace rive
diff --git a/include/rive/animation/scripted_transition_condition.hpp b/include/rive/animation/scripted_transition_condition.hpp new file mode 100644 index 0000000..e26683a --- /dev/null +++ b/include/rive/animation/scripted_transition_condition.hpp
@@ -0,0 +1,32 @@ +#ifndef _RIVE_SCRIPTED_TRANSITION_CONDITION_HPP_ +#define _RIVE_SCRIPTED_TRANSITION_CONDITION_HPP_ +#include "rive/generated/animation/scripted_transition_condition_base.hpp" +#include "rive/scripted/scripted_object.hpp" +#include <stdio.h> +namespace rive +{ +class ScriptedTransitionCondition : public ScriptedTransitionConditionBase, + public ScriptedObject +{ +public: + bool evaluate(const StateMachineInstance* stateMachineInstance, + StateMachineLayerInstance* layerInstance) const override; + bool evaluateStateful(const StateMachineInstance* stateMachineInstance, + StateMachineLayerInstance* layerInstance) const; + uint32_t assetId() override { return scriptAssetId(); } + bool addScriptedDirt(ComponentDirt value, bool recurse = false) override + { + return false; + } + ScriptProtocol scriptProtocol() override + { + return ScriptProtocol::transitionCondition; + } + Component* component() override { return nullptr; } + StatusCode import(ImportStack& importStack) override; + Core* clone() const override; + ScriptedObject* cloneScriptedObject() const override; +}; +} // namespace rive + +#endif \ No newline at end of file
diff --git a/include/rive/animation/state_machine.hpp b/include/rive/animation/state_machine.hpp index 258fe61..ff441ca 100644 --- a/include/rive/animation/state_machine.hpp +++ b/include/rive/animation/state_machine.hpp
@@ -10,6 +10,7 @@ class StateMachineInput; class StateMachineListener; class StateMachineImporter; +class ScriptedObject; class DataBind; class StateMachine : public StateMachineBase { @@ -20,6 +21,7 @@ std::vector<std::unique_ptr<StateMachineInput>> m_Inputs; std::vector<std::unique_ptr<StateMachineListener>> m_Listeners; std::vector<std::unique_ptr<DataBind>> m_dataBinds; + std::vector<ScriptedObject*> m_scriptedObjects; void addLayer(std::unique_ptr<StateMachineLayer>); void addInput(std::unique_ptr<StateMachineInput>); @@ -36,6 +38,11 @@ size_t inputCount() const { return m_Inputs.size(); } size_t listenerCount() const { return m_Listeners.size(); } size_t dataBindCount() const { return m_dataBinds.size(); } + void addScriptedObject(ScriptedObject* object); + std::vector<ScriptedObject*> scriptedObjects() const + { + return m_scriptedObjects; + } const StateMachineInput* input(std::string name) const; const StateMachineInput* input(size_t index) const;
diff --git a/include/rive/animation/state_machine_instance.hpp b/include/rive/animation/state_machine_instance.hpp index 641630c..18f3437 100644 --- a/include/rive/animation/state_machine_instance.hpp +++ b/include/rive/animation/state_machine_instance.hpp
@@ -206,7 +206,7 @@ bool hasListeners() { return m_hitComponents.size() > 0; } void clearDataContext(); void internalDataContext(DataContext* dataContext); - ScriptedObject* scriptedObject(const ScriptedObject*); + ScriptedObject* scriptedObject(const ScriptedObject*) const; #ifdef TESTING size_t hitComponentsCount() { return m_hitComponents.size(); }; HitComponent* hitComponent(size_t index)
diff --git a/include/rive/assets/script_asset.hpp b/include/rive/assets/script_asset.hpp index 6de0360..d62f942 100644 --- a/include/rive/assets/script_asset.hpp +++ b/include/rive/assets/script_asset.hpp
@@ -26,7 +26,8 @@ layout, converter, pathEffect, - listenerAction + listenerAction, + transitionCondition }; #ifdef WITH_RIVE_SCRIPTING
diff --git a/include/rive/generated/animation/scripted_transition_condition_base.hpp b/include/rive/generated/animation/scripted_transition_condition_base.hpp new file mode 100644 index 0000000..01d08b5 --- /dev/null +++ b/include/rive/generated/animation/scripted_transition_condition_base.hpp
@@ -0,0 +1,71 @@ +#ifndef _RIVE_SCRIPTED_TRANSITION_CONDITION_BASE_HPP_ +#define _RIVE_SCRIPTED_TRANSITION_CONDITION_BASE_HPP_ +#include "rive/animation/transition_condition.hpp" +#include "rive/core/field_types/core_uint_type.hpp" +namespace rive +{ +class ScriptedTransitionConditionBase : public TransitionCondition +{ +protected: + typedef TransitionCondition Super; + +public: + static const uint16_t typeKey = 647; + + /// Helper to quickly determine if a core object extends another without + /// RTTI at runtime. + bool isTypeOf(uint16_t typeKey) const override + { + switch (typeKey) + { + case ScriptedTransitionConditionBase::typeKey: + case TransitionConditionBase::typeKey: + return true; + default: + return false; + } + } + + uint16_t coreType() const override { return typeKey; } + + static const uint16_t scriptAssetIdPropertyKey = 931; + +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 ScriptedTransitionConditionBase& object) + { + m_ScriptAssetId = object.m_ScriptAssetId; + TransitionCondition::copy(object); + } + + bool deserialize(uint16_t propertyKey, BinaryReader& reader) override + { + switch (propertyKey) + { + case scriptAssetIdPropertyKey: + m_ScriptAssetId = CoreUintType::deserialize(reader); + return true; + } + return TransitionCondition::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 4a541ae..1bd5d13 100644 --- a/include/rive/generated/core_registry.hpp +++ b/include/rive/generated/core_registry.hpp
@@ -51,6 +51,7 @@ #include "rive/animation/nested_state_machine.hpp" #include "rive/animation/nested_trigger.hpp" #include "rive/animation/scripted_listener_action.hpp" +#include "rive/animation/scripted_transition_condition.hpp" #include "rive/animation/state_machine.hpp" #include "rive/animation/state_machine_bool.hpp" #include "rive/animation/state_machine_component.hpp" @@ -508,6 +509,8 @@ return new ListenerBoolChange(); case ListenerAlignTargetBase::typeKey: return new ListenerAlignTarget(); + case ScriptedTransitionConditionBase::typeKey: + return new ScriptedTransitionCondition(); case TransitionNumberConditionBase::typeKey: return new TransitionNumberCondition(); case TransitionValueBooleanComparatorBase::typeKey: @@ -1242,6 +1245,10 @@ case ListenerAlignTargetBase::targetIdPropertyKey: object->as<ListenerAlignTargetBase>()->targetId(value); break; + case ScriptedTransitionConditionBase::scriptAssetIdPropertyKey: + object->as<ScriptedTransitionConditionBase>()->scriptAssetId( + value); + break; case TransitionValueConditionBase::opValuePropertyKey: object->as<TransitionValueConditionBase>()->opValue(value); break; @@ -2775,6 +2782,9 @@ return object->as<ListenerBoolChangeBase>()->value(); case ListenerAlignTargetBase::targetIdPropertyKey: return object->as<ListenerAlignTargetBase>()->targetId(); + case ScriptedTransitionConditionBase::scriptAssetIdPropertyKey: + return object->as<ScriptedTransitionConditionBase>() + ->scriptAssetId(); case TransitionValueConditionBase::opValuePropertyKey: return object->as<TransitionValueConditionBase>()->opValue(); case TransitionViewModelConditionBase::opValuePropertyKey: @@ -3749,6 +3759,7 @@ case KeyFrameIdBase::valuePropertyKey: case ListenerBoolChangeBase::valuePropertyKey: case ListenerAlignTargetBase::targetIdPropertyKey: + case ScriptedTransitionConditionBase::scriptAssetIdPropertyKey: case TransitionValueConditionBase::opValuePropertyKey: case TransitionViewModelConditionBase::opValuePropertyKey: case BlendState1DInputBase::inputIdPropertyKey: @@ -4416,6 +4427,8 @@ return object->is<ListenerBoolChangeBase>(); case ListenerAlignTargetBase::targetIdPropertyKey: return object->is<ListenerAlignTargetBase>(); + case ScriptedTransitionConditionBase::scriptAssetIdPropertyKey: + return object->is<ScriptedTransitionConditionBase>(); case TransitionValueConditionBase::opValuePropertyKey: return object->is<TransitionValueConditionBase>(); case TransitionViewModelConditionBase::opValuePropertyKey:
diff --git a/include/rive/importers/state_machine_importer.hpp b/include/rive/importers/state_machine_importer.hpp index dc08236..2d8c402 100644 --- a/include/rive/importers/state_machine_importer.hpp +++ b/include/rive/importers/state_machine_importer.hpp
@@ -10,6 +10,7 @@ class StateMachineListener; class StateMachine; class DataBind; +class ScriptedObject; class StateMachineImporter : public ImportStackObject { private: @@ -23,6 +24,7 @@ void addInput(std::unique_ptr<StateMachineInput>); void addListener(std::unique_ptr<StateMachineListener>); void addDataBind(std::unique_ptr<DataBind>); + void addScriptedObject(ScriptedObject* object); StatusCode resolve() override; bool readNullObject() override;
diff --git a/include/rive/scripted/scripted_object.hpp b/include/rive/scripted/scripted_object.hpp index c712716..18e9182 100644 --- a/include/rive/scripted/scripted_object.hpp +++ b/include/rive/scripted/scripted_object.hpp
@@ -63,6 +63,7 @@ virtual ScriptProtocol scriptProtocol() = 0; int self() { return m_self; } virtual Component* component() = 0; + virtual ScriptedObject* cloneScriptedObject() const { return nullptr; } }; } // namespace rive
diff --git a/src/animation/scripted_listener_action.cpp b/src/animation/scripted_listener_action.cpp index 4b3b959..db217cc 100644 --- a/src/animation/scripted_listener_action.cpp +++ b/src/animation/scripted_listener_action.cpp
@@ -1,5 +1,6 @@ #include "rive/animation/scripted_listener_action.hpp" #include "rive/animation/state_machine_instance.hpp" +#include "rive/importers/state_machine_importer.hpp" using namespace rive; @@ -66,6 +67,14 @@ { return result; } + + auto stateMachineImporter = + importStack.latest<StateMachineImporter>(StateMachine::typeKey); + if (stateMachineImporter == nullptr) + { + return StatusCode::MissingObject; + } + stateMachineImporter->addScriptedObject(this); return Super::import(importStack); } @@ -83,4 +92,11 @@ twin->addProperty(clonedValue); } return twin; +} + +ScriptedObject* ScriptedListenerAction::cloneScriptedObject() const +{ + auto clonedScriptedObject = clone()->as<ScriptedListenerAction>(); + clonedScriptedObject->reinit(); + return clonedScriptedObject; } \ No newline at end of file
diff --git a/src/animation/scripted_transition_condition.cpp b/src/animation/scripted_transition_condition.cpp new file mode 100644 index 0000000..8ea3eda --- /dev/null +++ b/src/animation/scripted_transition_condition.cpp
@@ -0,0 +1,101 @@ +#include "rive/animation/scripted_transition_condition.hpp" +#include "rive/animation/state_machine_instance.hpp" +#include "rive/importers/state_machine_importer.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. +bool ScriptedTransitionCondition::evaluateStateful( + const StateMachineInstance* stateMachineInstance, + StateMachineLayerInstance* layerInstance) const +{ + bool result = false; +#ifdef WITH_RIVE_SCRIPTING + if (m_state) + { + // Stack: [] + rive_lua_pushRef(m_state, m_self); + // Stack: [self] + lua_getfield(m_state, -1, "evaluate"); + + // Stack: [self, field] + lua_insert(m_state, -2); // Swap self and field + + // Stack: [field, self] + if (static_cast<lua_Status>(rive_lua_pcall(m_state, 1, 1)) == LUA_OK) + { + if (lua_isboolean(m_state, -1)) + { + result = lua_toboolean(m_state, -1); + } + // Stack: [result] + rive_lua_pop(m_state, 1); + } + else + { + // Stack: [status] + rive_lua_pop(m_state, 1); + } + } +#endif + return result; +} + +bool ScriptedTransitionCondition::evaluate( + const StateMachineInstance* stateMachineInstance, + StateMachineLayerInstance* layerInstance) const +{ +#ifdef WITH_RIVE_SCRIPTING + auto scriptedObject = stateMachineInstance->scriptedObject(this); + if (scriptedObject != nullptr) + { + auto statefulListenerAction = + static_cast<ScriptedTransitionCondition*>(scriptedObject); + return statefulListenerAction->evaluateStateful(stateMachineInstance, + layerInstance); + } +#endif + return false; +} + +StatusCode ScriptedTransitionCondition::import(ImportStack& importStack) +{ + auto result = registerReferencer(importStack); + if (result != StatusCode::Ok) + { + return result; + } + auto stateMachineImporter = + importStack.latest<StateMachineImporter>(StateMachine::typeKey); + if (stateMachineImporter == nullptr) + { + return StatusCode::MissingObject; + } + stateMachineImporter->addScriptedObject(this); + return Super::import(importStack); +} + +Core* ScriptedTransitionCondition::clone() const +{ + ScriptedTransitionCondition* twin = ScriptedTransitionConditionBase::clone() + ->as<ScriptedTransitionCondition>(); + if (m_fileAsset != nullptr) + { + twin->setAsset(m_fileAsset); + } + for (auto prop : m_customProperties) + { + auto clonedValue = prop->clone()->as<CustomProperty>(); + twin->addProperty(clonedValue); + } + return twin; +} + +ScriptedObject* ScriptedTransitionCondition::cloneScriptedObject() const +{ + auto clonedScriptedObject = clone()->as<ScriptedTransitionCondition>(); + clonedScriptedObject->reinit(); + return clonedScriptedObject; +} \ No newline at end of file
diff --git a/src/animation/state_machine.cpp b/src/animation/state_machine.cpp index dbea78e..be9cc59 100644 --- a/src/animation/state_machine.cpp +++ b/src/animation/state_machine.cpp
@@ -4,6 +4,7 @@ #include "rive/animation/state_machine_layer.hpp" #include "rive/animation/state_machine_input.hpp" #include "rive/animation/state_machine_listener.hpp" +#include "rive/scripted/scripted_object.hpp" #include "rive/data_bind/data_bind.hpp" using namespace rive; @@ -156,4 +157,9 @@ return m_dataBinds[index].get(); } return nullptr; +} + +void StateMachine::addScriptedObject(ScriptedObject* object) +{ + m_scriptedObjects.push_back(object); } \ No newline at end of file
diff --git a/src/animation/state_machine_instance.cpp b/src/animation/state_machine_instance.cpp index 964b7a2..226bb45 100644 --- a/src/animation/state_machine_instance.cpp +++ b/src/animation/state_machine_instance.cpp
@@ -8,6 +8,7 @@ #include "rive/animation/layer_state_flags.hpp" #include "rive/animation/nested_linear_animation.hpp" #include "rive/animation/nested_state_machine.hpp" +#include "rive/animation/scripted_transition_condition.hpp" #include "rive/animation/state_instance.hpp" #include "rive/animation/state_machine_bool.hpp" #include "rive/animation/state_machine_input_instance.hpp" @@ -1482,32 +1483,18 @@ 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; - } - } + // Initialize local instances of ScriptedObjects + for (auto& scriptedOb : machine->scriptedObjects()) + { + m_scriptedListenerActionsMap[scriptedOb] = + scriptedOb->cloneScriptedObject(); } sortHitComponents(); } ScriptedObject* StateMachineInstance::scriptedObject( - const ScriptedObject* source) + const ScriptedObject* source) const { auto itr = m_scriptedListenerActionsMap.find(source); if (itr != m_scriptedListenerActionsMap.end())
diff --git a/src/assets/script_asset.cpp b/src/assets/script_asset.cpp index 9642b5b..bca0765 100644 --- a/src/assets/script_asset.cpp +++ b/src/assets/script_asset.cpp
@@ -67,7 +67,8 @@ scriptProtocol == ScriptProtocol::layout || scriptProtocol == ScriptProtocol::converter || scriptProtocol == ScriptProtocol::pathEffect || - scriptProtocol == ScriptProtocol::listenerAction) + scriptProtocol == ScriptProtocol::listenerAction || + scriptProtocol == ScriptProtocol::transitionCondition) { if (static_cast<lua_Type>(lua_getfield(state, -1, "update")) == LUA_TFUNCTION)
diff --git a/src/generated/animation/scripted_transition_condition_base.cpp b/src/generated/animation/scripted_transition_condition_base.cpp new file mode 100644 index 0000000..d4bd6d1 --- /dev/null +++ b/src/generated/animation/scripted_transition_condition_base.cpp
@@ -0,0 +1,11 @@ +#include "rive/generated/animation/scripted_transition_condition_base.hpp" +#include "rive/animation/scripted_transition_condition.hpp" + +using namespace rive; + +Core* ScriptedTransitionConditionBase::clone() const +{ + auto cloned = new ScriptedTransitionCondition(); + cloned->copy(*this); + return cloned; +}
diff --git a/src/importers/state_machine_importer.cpp b/src/importers/state_machine_importer.cpp index de29f24..67eac58 100644 --- a/src/importers/state_machine_importer.cpp +++ b/src/importers/state_machine_importer.cpp
@@ -39,4 +39,9 @@ return true; } +void StateMachineImporter::addScriptedObject(ScriptedObject* object) +{ + m_StateMachine->addScriptedObject(object); +} + StatusCode StateMachineImporter::resolve() { return StatusCode::Ok; } \ No newline at end of file
diff --git a/tests/unit_tests/assets/scripted_transition_condition.riv b/tests/unit_tests/assets/scripted_transition_condition.riv new file mode 100644 index 0000000..2dbecd7 --- /dev/null +++ b/tests/unit_tests/assets/scripted_transition_condition.riv Binary files differ
diff --git a/tests/unit_tests/runtime/scripting/scripting_transition_condition_test.cpp b/tests/unit_tests/runtime/scripting/scripting_transition_condition_test.cpp new file mode 100644 index 0000000..985df3b --- /dev/null +++ b/tests/unit_tests/runtime/scripting/scripting_transition_condition_test.cpp
@@ -0,0 +1,48 @@ + +#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/viewmodel/viewmodel_instance_boolean.hpp" +#include "rive_file_reader.hpp" + +using namespace rive; + +TEST_CASE("Scripted transition condition", "[silver]") +{ + rive::SerializingFactory silver; + auto file = + ReadRiveFile("assets/scripted_transition_condition.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 tlBool = + vmi->propertyValue("timelineBool")->as<ViewModelInstanceBoolean>(); + auto anyBool = + vmi->propertyValue("anyStateBool")->as<ViewModelInstanceBoolean>(); + + auto renderer = silver.makeRenderer(); + artboard->draw(renderer.get()); + + silver.addFrame(); + + tlBool->propertyValue(true); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + + silver.addFrame(); + + anyBool->propertyValue(true); + stateMachine->advanceAndApply(0.016f); + artboard->draw(renderer.get()); + + CHECK(silver.matches("scripted_transition_condition")); +} \ No newline at end of file
diff --git a/tests/unit_tests/silvers/scripted_transition_condition.sriv b/tests/unit_tests/silvers/scripted_transition_condition.sriv new file mode 100644 index 0000000..a0ae9bb --- /dev/null +++ b/tests/unit_tests/silvers/scripted_transition_condition.sriv Binary files differ