| #ifdef WITH_RIVE_SCRIPTING |
| #include "rive/lua/rive_lua_libs.hpp" |
| #endif |
| #include "rive/assets/script_asset.hpp" |
| #include "rive/artboard.hpp" |
| #include "rive/file.hpp" |
| #include "rive/script_input_artboard.hpp" |
| #include "rive/animation/scripted_listener_action.hpp" |
| #include "rive/animation/scripted_transition_condition.hpp" |
| #include "rive/scripted/scripted_data_converter.hpp" |
| #include "rive/scripted/scripted_drawable.hpp" |
| #include "rive/scripted/scripted_interpolator.hpp" |
| #include "rive/scripted/scripted_layout.hpp" |
| #include "rive/scripted/scripted_path_effect.hpp" |
| #include "rive/scripted/scripted_object.hpp" |
| #include "rive/data_bind/data_bind.hpp" |
| |
| using namespace rive; |
| |
| ScriptedObject* ScriptedObject::from(Core* object) |
| { |
| switch (object->coreType()) |
| { |
| case ScriptedDataConverter::typeKey: |
| return object->as<ScriptedDataConverter>(); |
| case ScriptedDrawable::typeKey: |
| return object->as<ScriptedDrawable>(); |
| case ScriptedLayout::typeKey: |
| return object->as<ScriptedLayout>(); |
| case ScriptedPathEffect::typeKey: |
| return object->as<ScriptedPathEffect>(); |
| case ScriptedListenerAction::typeKey: |
| return object->as<ScriptedListenerAction>(); |
| case ScriptedTransitionCondition::typeKey: |
| return object->as<ScriptedTransitionCondition>(); |
| case ScriptedInterpolator::typeKey: |
| return object->as<ScriptedInterpolator>(); |
| } |
| return nullptr; |
| } |
| |
| #ifdef WITH_RIVE_SCRIPTING |
| void ScriptedObject::setArtboardInput(std::string name, Artboard* artboard) |
| { |
| lua_State* L = state(); |
| if (L == nullptr || scriptAsset() == nullptr) |
| { |
| return; |
| } |
| rive_lua_pushRef(L, m_self); |
| auto artboardInstance = artboard->instance(); |
| artboardInstance->frameOrigin(false); |
| lua_newrive<ScriptedArtboard>(L, |
| L, |
| scriptAsset()->file(), |
| std::move(artboardInstance), |
| nullptr, |
| dataContext()); |
| lua_setfield(L, -2, name.c_str()); |
| rive_lua_pop(L, 1); |
| addScriptedDirt(ComponentDirt::ScriptUpdate); |
| } |
| |
| void ScriptedObject::setBooleanInput(std::string name, bool value) |
| { |
| lua_State* L = state(); |
| if (L == nullptr) |
| { |
| return; |
| } |
| rive_lua_pushRef(L, m_self); |
| lua_pushboolean(L, value); |
| lua_setfield(L, -2, name.c_str()); |
| rive_lua_pop(L, 1); |
| addScriptedDirt(ComponentDirt::ScriptUpdate); |
| } |
| |
| void ScriptedObject::setNumberInput(std::string name, float value) |
| { |
| lua_State* L = state(); |
| if (L == nullptr) |
| { |
| return; |
| } |
| rive_lua_pushRef(L, m_self); |
| lua_pushnumber(L, value); |
| lua_setfield(L, -2, name.c_str()); |
| rive_lua_pop(L, 1); |
| addScriptedDirt(ComponentDirt::ScriptUpdate); |
| } |
| |
| void ScriptedObject::setIntegerInput(std::string name, int value) |
| { |
| lua_State* L = state(); |
| if (L == nullptr) |
| { |
| return; |
| } |
| rive_lua_pushRef(L, m_self); |
| lua_pushunsigned(L, value); |
| lua_setfield(L, -2, name.c_str()); |
| rive_lua_pop(L, 1); |
| addScriptedDirt(ComponentDirt::ScriptUpdate); |
| } |
| |
| void ScriptedObject::setStringInput(std::string name, std::string value) |
| { |
| lua_State* L = state(); |
| if (L == nullptr) |
| { |
| return; |
| } |
| rive_lua_pushRef(L, m_self); |
| lua_pushstring(L, value.c_str()); |
| lua_setfield(L, -2, name.c_str()); |
| rive_lua_pop(L, 1); |
| addScriptedDirt(ComponentDirt::ScriptUpdate); |
| } |
| |
| void ScriptedObject::setViewModelInput(std::string name, |
| ViewModelInstanceValue* value) |
| { |
| lua_State* L = state(); |
| if (L == nullptr) |
| { |
| return; |
| } |
| rive_lua_pushRef(L, m_self); |
| switch (value->coreType()) |
| { |
| case ViewModelInstanceViewModelBase::typeKey: |
| { |
| auto viewModel = value->as<ViewModelInstanceViewModel>(); |
| auto vmi = viewModel->referenceViewModelInstance(); |
| if (vmi == nullptr) |
| { |
| fprintf(stderr, |
| "riveLuaPushViewModelInstanceValue - passed in a " |
| "ViewModelInstanceViewModel with no associated " |
| "ViewModelInstance.\n"); |
| return; |
| } |
| |
| lua_newrive<ScriptedViewModel>(L, |
| L, |
| ref_rcp(vmi->viewModel()), |
| vmi); |
| break; |
| } |
| default: |
| break; |
| } |
| lua_setfield(L, -2, name.c_str()); |
| rive_lua_pop(L, 1); |
| addScriptedDirt(ComponentDirt::ScriptUpdate); |
| } |
| |
| void ScriptedObject::trigger(std::string name) |
| { |
| lua_State* L = state(); |
| if (L == nullptr) |
| { |
| return; |
| } |
| rive_lua_pushRef(L, m_self); |
| if (static_cast<lua_Type>(lua_getfield(L, -1, name.c_str())) != |
| LUA_TFUNCTION) |
| { |
| rive_lua_pop(L, 2); |
| return; |
| } |
| lua_pushvalue(L, -2); |
| rive_lua_pcall_with_context(L, this, 1, 0); |
| rive_lua_pop(L, 1); |
| addScriptedDirt(ComponentDirt::ScriptUpdate); |
| } |
| |
| bool ScriptedObject::scriptAdvance(float elapsedSeconds) |
| { |
| lua_State* L = state(); |
| if (!advances() || L == nullptr) |
| { |
| return false; |
| } |
| rive_lua_pushRef(L, m_self); |
| // implementedMethods may be assumed for legacy files (all-bits default); if |
| // the field isn't actually a function, treat it as not implemented. |
| if (static_cast<lua_Type>(lua_getfield(L, -1, "advance")) != LUA_TFUNCTION) |
| { |
| rive_lua_pop(L, 2); // non-function field + self |
| return false; |
| } |
| lua_pushvalue(L, -2); |
| lua_pushnumber(L, elapsedSeconds); |
| if (static_cast<lua_Status>(rive_lua_pcall_with_context(L, this, 2, 1)) != |
| LUA_OK) |
| { |
| rive_lua_pop(L, 2); |
| return false; |
| } |
| bool result = lua_toboolean(L, -1); |
| rive_lua_pop(L, 2); |
| return result; |
| } |
| |
| void ScriptedObject::scriptDrawCanvas() |
| { |
| lua_State* L = state(); |
| if (!drawsCanvas() || L == nullptr) |
| { |
| return; |
| } |
| rive_lua_pushRef(L, m_self); |
| if (static_cast<lua_Type>(lua_getfield(L, -1, "drawCanvas")) != |
| LUA_TFUNCTION) |
| { |
| rive_lua_pop(L, 2); // non-function field + self |
| return; |
| } |
| lua_pushvalue(L, -2); |
| if (static_cast<lua_Status>(rive_lua_pcall(L, 1, 0)) != LUA_OK) |
| { |
| rive_lua_pop(L, 1); |
| return; |
| } |
| rive_lua_pop(L, 1); |
| } |
| |
| void ScriptedObject::scriptUpdate() |
| { |
| lua_State* L = state(); |
| if (!updates() || L == nullptr) |
| { |
| return; |
| } |
| // Stack: [] |
| rive_lua_pushRef(L, m_self); |
| // Stack: [self] |
| if (static_cast<lua_Type>(lua_getfield(L, -1, "update")) != LUA_TFUNCTION) |
| { |
| // Not actually implemented (assumed for legacy files); no-op. The |
| // update phase never started, so there's no flag to reset. |
| rive_lua_pop(L, 2); // non-function field + self |
| return; |
| } |
| // Only inside the update phase while the callback actually runs. |
| m_inUpdatePhase = true; |
| // Stack: [self, field] Swap self and field |
| lua_insert(L, -2); |
| // Stack: [field, self] |
| if (static_cast<lua_Status>(rive_lua_pcall_with_context(L, this, 1, 0)) != |
| LUA_OK) |
| { |
| rive_lua_pop(L, 1); |
| } |
| m_inUpdatePhase = false; |
| } |
| |
| bool ScriptedObject::tryLuaUserInit(lua_State* L) |
| { |
| rive_lua_pushRef(L, m_self); |
| // Stack: [self] |
| if (static_cast<lua_Type>(lua_getfield(L, -1, "init")) != LUA_TFUNCTION) |
| { |
| // init is optional and not implemented (assumed for legacy files); |
| // nothing to run — the object is considered initialized. |
| rive_lua_pop(L, 2); // non-function field + self |
| return true; |
| } |
| // Stack: [self, field] |
| lua_pushvalue(L, -2); |
| // Stack: [self, field, self] |
| // Reuse the ScriptedContext created during ensureScriptInitialized so |
| // the generator and init() both see the same Context instance. |
| rive_lua_pushRef(L, m_context); |
| // Stack: [self, field, self, ScriptedContext] |
| auto pCallResult = rive_lua_pcall_with_context(L, this, 2, 1); |
| if (static_cast<lua_Status>(pCallResult) != LUA_OK) |
| { |
| lua_unref(L, m_self); |
| disposeScriptedContext(); |
| // Stack: [self, status] |
| rive_lua_pop(L, 2); |
| if (m_vm != nullptr) |
| { |
| m_vm->unregisterScriptedObject(this); |
| } |
| m_vm = nullptr; |
| m_self = 0; |
| return false; |
| } |
| if (!lua_toboolean(L, -1) || m_contextPtr->missingRequestedData()) |
| { |
| lua_unref(L, m_self); |
| disposeScriptedContext(); |
| rive_lua_pop(L, 2); |
| if (m_vm != nullptr) |
| { |
| m_vm->unregisterScriptedObject(this); |
| } |
| m_vm = nullptr; |
| m_self = 0; |
| return false; |
| } |
| // Stack: [self, result] |
| rive_lua_pop(L, 1); |
| assert(static_cast<lua_Type>(lua_type(L, -1)) == LUA_TTABLE); |
| // Stack: [self] |
| rive_lua_pop(L, 1); |
| return true; |
| } |
| |
| bool ScriptedObject::ensureScriptInitialized(ScriptingVM* vm) |
| { |
| lua_State* L = vm ? vm->state() : nullptr; |
| if (L == nullptr) |
| { |
| return false; |
| } |
| if (m_self != 0 && m_vm == vm) |
| { |
| rive_lua_pop(L, 1); |
| return true; |
| } |
| if (m_vm != nullptr) |
| { |
| lua_State* oldState = state(); |
| if (m_self != 0) |
| { |
| lua_unref(oldState, m_self); |
| m_self = 0; |
| } |
| |
| disposeTrackedProperties(); |
| disposeScriptedContext(); |
| m_vm->unregisterScriptedObject(this); |
| } |
| m_userLuaInitDone = false; |
| |
| for (auto prop : m_customProperties) |
| { |
| auto scriptInput = ScriptInput::from(prop); |
| if (scriptInput && !scriptInput->validateForColdScriptInit()) |
| { |
| rive_lua_pop(L, 1); |
| return false; |
| } |
| } |
| |
| // Create the Context userdata before calling the generator so scripts |
| // can request resources at construction time (e.g. |
| // `return { canvas = context:gpuCanvas() }`). The same Context is |
| // reused in tryLuaUserInit when init(self, context) is called, so a |
| // script only ever sees one Context per scripted-object lifetime. |
| // Stack: [generator] |
| m_contextPtr = lua_newrive<ScriptedContext>(L, this); |
| // Stack: [generator, context] |
| m_context = lua_ref(L, -1); |
| // Stack: [generator, context] (lua_ref does not pop) |
| |
| // m_vm is not yet assigned here, so disposeScriptedContext() cannot |
| // resolve a lua_State via state(). Unref/clear directly on failure. |
| auto disposeContextDirect = [&]() { |
| if (m_contextPtr != nullptr) |
| { |
| m_contextPtr->clearScriptedObject(); |
| m_contextPtr = nullptr; |
| } |
| if (m_context != 0) |
| { |
| lua_unref(L, m_context); |
| m_context = 0; |
| } |
| }; |
| |
| if (static_cast<lua_Status>(rive_lua_pcall_with_context(L, this, 1, 1)) != |
| LUA_OK) |
| { |
| disposeContextDirect(); |
| rive_lua_pop(L, 1); |
| return false; |
| } |
| if (static_cast<lua_Type>(lua_type(L, -1)) != LUA_TTABLE) |
| { |
| disposeContextDirect(); |
| rive_lua_pop(L, 1); |
| return false; |
| } |
| m_self = lua_ref(L, -1); |
| m_vm = vm; |
| if (vm != nullptr) |
| { |
| vm->registerScriptedObject(this); |
| } |
| rive_lua_pop(L, 1); |
| return true; |
| } |
| |
| bool ScriptedObject::hydrateScriptInputs() |
| { |
| lua_State* L = state(); |
| if (L == nullptr || m_self == 0) |
| { |
| return false; |
| } |
| // First validate that all properties are hydratable. |
| // This ensures we don't partially hydrate props and early out on the next |
| // step |
| for (auto prop : m_customProperties) |
| { |
| auto scriptInput = ScriptInput::from(prop); |
| if (scriptInput != nullptr && |
| !scriptInput->validateHydrationPrerequisites()) |
| { |
| return false; |
| } |
| } |
| // Next hydrate all properties |
| for (auto prop : m_customProperties) |
| { |
| auto scriptInput = ScriptInput::from(prop); |
| if (scriptInput != nullptr && !scriptInput->hydrateScriptInput()) |
| { |
| return false; |
| } |
| } |
| // Finally initialize the script if it hasn't been initialized before. |
| if (inits() && !m_userLuaInitDone) |
| { |
| if (!tryLuaUserInit(L)) |
| { |
| return false; |
| } |
| m_userLuaInitDone = true; |
| } |
| didHydrateScriptInputs(); |
| return true; |
| } |
| |
| void ScriptedObject::disposeScriptInputs() |
| { |
| for (auto prop : m_customProperties) |
| { |
| auto scriptInput = ScriptInput::from(prop); |
| if (scriptInput != nullptr) |
| { |
| scriptInput->scriptedObject(nullptr); |
| } |
| } |
| m_customProperties.clear(); |
| } |
| |
| void ScriptedObject::disposeTrackedProperties() |
| { |
| |
| auto trackedProperties = m_trackedScriptedProperties; |
| for (auto* property : trackedProperties) |
| { |
| if (property != nullptr) |
| { |
| property->dispose(); |
| } |
| } |
| } |
| |
| void ScriptedObject::disposeScriptedContext() |
| { |
| if (m_contextPtr != nullptr) |
| { |
| m_contextPtr->clearScriptedObject(); |
| m_contextPtr = nullptr; |
| } |
| if (m_context != 0) |
| { |
| lua_State* L = state(); |
| if (L != nullptr) |
| { |
| lua_unref(L, m_context); |
| } |
| m_context = 0; |
| } |
| } |
| |
| void ScriptedObject::scriptDispose() |
| { |
| disposeScriptInputs(); |
| disposeTrackedProperties(); |
| m_trackedScriptedProperties.clear(); |
| |
| lua_State* L = state(); |
| if (L != nullptr) |
| { |
| lua_unref(L, m_self); |
| disposeScriptedContext(); |
| } |
| if (m_vm != nullptr) |
| { |
| m_vm->unregisterScriptedObject(this); |
| m_vm = nullptr; |
| } |
| m_self = 0; |
| m_userLuaInitDone = false; |
| } |
| #else |
| void ScriptedObject::setArtboardInput(std::string name, Artboard* artboard) {} |
| |
| void ScriptedObject::setBooleanInput(std::string name, bool value) {} |
| |
| void ScriptedObject::setIntegerInput(std::string name, int value) {} |
| |
| void ScriptedObject::setNumberInput(std::string name, float value) {} |
| |
| void ScriptedObject::setStringInput(std::string name, std::string value) {} |
| |
| void ScriptedObject::setViewModelInput(std::string name, |
| ViewModelInstanceValue* value) |
| {} |
| |
| void ScriptedObject::trigger(std::string name) {} |
| |
| bool ScriptedObject::scriptAdvance(float elapsedSeconds) { return false; } |
| |
| void ScriptedObject::scriptDrawCanvas() {} |
| |
| void ScriptedObject::scriptUpdate() {} |
| |
| void ScriptedObject::scriptDispose() {} |
| |
| void ScriptedObject::disposeScriptInputs() {} |
| #endif |
| |
| void ScriptedObject::reinit() |
| { |
| if (scriptAsset() != nullptr) |
| { |
| scriptAsset()->initScriptedObject(this); |
| #ifdef WITH_RIVE_SCRIPTING |
| hydrateScriptInputs(); |
| #endif |
| } |
| } |
| |
| ScriptAsset* ScriptedObject::scriptAsset() const |
| { |
| return (ScriptAsset*)m_fileAsset.get(); |
| } |
| |
| void ScriptedObject::setAsset(rcp<FileAsset> asset) |
| { |
| if (asset != nullptr && asset->is<ScriptAsset>()) |
| { |
| FileAssetReferencer::setAsset(asset); |
| } |
| } |
| |
| void ScriptedObject::markNeedsUpdate() {} |
| |
| void ScriptedObject::cloneProperties(CustomPropertyContainer* twin, |
| DataBindContainer* dataBindContainer) const |
| { |
| |
| for (auto prop : m_customProperties) |
| { |
| auto clonedValue = prop->clone()->as<CustomProperty>(); |
| twin->addProperty(clonedValue); |
| auto input = ScriptInput::from(prop); |
| if (input != nullptr) |
| { |
| auto dataBind = input->dataBind(); |
| if (dataBind) |
| { |
| auto dataBindClone = static_cast<DataBind*>(dataBind->clone()); |
| dataBindClone->file(dataBind->file()); |
| if (dataBind->converter() != nullptr) |
| { |
| dataBindClone->converter( |
| dataBind->converter()->clone()->as<DataConverter>()); |
| } |
| dataBindClone->target(clonedValue); |
| dataBindContainer->addDataBind(dataBindClone); |
| if (auto* clonedInput = ScriptInput::from(clonedValue)) |
| { |
| clonedInput->dataBind(dataBindClone, |
| /*ownsDataBind=*/false); |
| } |
| } |
| } |
| } |
| } |