diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 20a27e5bc3..52f4eaebb1 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -516,9 +516,12 @@ namespace MWBase virtual void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) = 0; virtual void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, - const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true, bool useAmbientLight = true) + const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true, bool useAmbientLight = true, + std::string_view effectId = {}, bool loop = false) = 0; + virtual void removeEffect(std::string_view effectId) = 0; + /// @see MWWorld::WeatherManager::isInStorm virtual bool isInStorm() const = 0; diff --git a/apps/openmw/mwlua/animationbindings.cpp b/apps/openmw/mwlua/animationbindings.cpp index 22ebd7e33a..45022393a2 100644 --- a/apps/openmw/mwlua/animationbindings.cpp +++ b/apps/openmw/mwlua/animationbindings.cpp @@ -312,28 +312,34 @@ namespace MWLua sol::table api(context.mLua->unsafeState(), sol::create); auto world = MWBase::Environment::get().getWorld(); - api["spawn"] - = [world, context](std::string_view model, const osg::Vec3f& worldPos, sol::optional options) { - if (options) - { - bool magicVfx = options->get_or("mwMagicVfx", true); - std::string texture = options->get_or("particleTextureOverride", ""); - float scale = options->get_or("scale", 1.f); - bool useAmbientLight = options->get_or("useAmbientLight", true); - context.mLuaManager->addAction( - [world, model = VFS::Path::Normalized(model), texture = std::move(texture), worldPos, scale, - magicVfx, useAmbientLight]() { - world->spawnEffect(model, texture, worldPos, scale, magicVfx, useAmbientLight); - }, - "openmw.vfx.spawn"); - } - else - { - context.mLuaManager->addAction([world, model = VFS::Path::Normalized(model), - worldPos]() { world->spawnEffect(model, "", worldPos, 1.f); }, - "openmw.vfx.spawn"); - } - }; + api["remove"] = [world, context](std::string_view vfxId) { + context.mLuaManager->addAction([world, vfxId = vfxId] { world->removeEffect(vfxId); }, "openmw.vfx.remove"); + }; + + api["spawn"] = [world, context]( + std::string_view model, const osg::Vec3f& worldPos, sol::optional options) { + if (options) + { + bool magicVfx = options->get_or("mwMagicVfx", true); + std::string texture = options->get_or("particleTextureOverride", ""); + float scale = options->get_or("scale", 1.f); + std::string_view vfxId = options->get_or("vfxId", ""); + bool loop = options->get_or("loop", false); + bool useAmbientLight = options->get_or("useAmbientLight", true); + context.mLuaManager->addAction( + [world, model = VFS::Path::Normalized(model), texture = std::move(texture), worldPos, scale, + magicVfx, useAmbientLight, vfxId, loop]() { + world->spawnEffect(model, texture, worldPos, scale, magicVfx, useAmbientLight, vfxId, loop); + }, + "openmw.vfx.spawn"); + } + else + { + context.mLuaManager->addAction([world, model = VFS::Path::Normalized(model), + worldPos]() { world->spawnEffect(model, "", worldPos, 1.f); }, + "openmw.vfx.spawn"); + } + }; return api; } diff --git a/apps/openmw/mwrender/effectmanager.cpp b/apps/openmw/mwrender/effectmanager.cpp index 927f3f7ad0..7f2ee282b9 100644 --- a/apps/openmw/mwrender/effectmanager.cpp +++ b/apps/openmw/mwrender/effectmanager.cpp @@ -28,7 +28,8 @@ namespace MWRender } void EffectManager::addEffect(VFS::Path::NormalizedView model, std::string_view textureOverride, - const osg::Vec3f& worldPosition, float scale, bool isMagicVFX, bool useAmbientLight) + const osg::Vec3f& worldPosition, float scale, bool isMagicVFX, bool useAmbientLight, std::string_view effectId, + bool loop) { osg::ref_ptr node = mResourceSystem->getSceneManager()->getInstance(model); @@ -36,6 +37,8 @@ namespace MWRender Effect effect; effect.mAnimTime = std::make_shared(); + effect.loop = loop; + effect.effectId = std::string(effectId); SceneUtil::FindMaxControllerLengthVisitor findMaxLengthVisitor; node->accept(findMaxLengthVisitor); @@ -67,17 +70,47 @@ namespace MWRender mResourceSystem->getSceneManager()->setUpNormalsRTForStateSet(node->getOrCreateStateSet(), false); + std::lock_guard lock(mEffectsMutex); mEffects.push_back(std::move(effect)); } + void EffectManager::removeEffect(std::string_view effectId) + { + std::lock_guard lock(mEffectsMutex); + mEffects.erase(std::remove_if(mEffects.begin(), mEffects.end(), + [effectId, this](Effect& effect) { + if (effectId.compare(effect.effectId) == 0) + { + mParentNode->removeChild(effect.mTransform); + return true; + } + + return false; + }), + mEffects.end()); + } + void EffectManager::update(float dt) { + std::lock_guard lock(mEffectsMutex); mEffects.erase(std::remove_if(mEffects.begin(), mEffects.end(), [dt, this](Effect& effect) { + bool remove = false; effect.mAnimTime->addTime(dt); - const auto remove = effect.mAnimTime->getTime() >= effect.mMaxControllerLength; - if (remove) - mParentNode->removeChild(effect.mTransform); + if (effect.mAnimTime->getTime() >= effect.mMaxControllerLength) + { + if (effect.loop) + { + float remainder = effect.mAnimTime->getTime() - effect.mMaxControllerLength; + effect.mAnimTime->resetTime(remainder); + } + else + { + mParentNode->removeChild(effect.mTransform); + remove = true; + } + } + return remove; }), mEffects.end()); @@ -85,6 +118,7 @@ namespace MWRender void EffectManager::clear() { + std::lock_guard lock(mEffectsMutex); for (const auto& effect : mEffects) { mParentNode->removeChild(effect.mTransform); diff --git a/apps/openmw/mwrender/effectmanager.hpp b/apps/openmw/mwrender/effectmanager.hpp index 87b0ce8e33..46caa64dc6 100644 --- a/apps/openmw/mwrender/effectmanager.hpp +++ b/apps/openmw/mwrender/effectmanager.hpp @@ -35,7 +35,10 @@ namespace MWRender /// Add an effect. When it's finished playing, it will be removed automatically. void addEffect(VFS::Path::NormalizedView model, std::string_view textureOverride, - const osg::Vec3f& worldPosition, float scale, bool isMagicVFX = true, bool useAmbientLight = true); + const osg::Vec3f& worldPosition, float scale, bool isMagicVFX = true, bool useAmbientLight = true, + std::string_view effectId = {}, bool loop = false); + + void removeEffect(std::string_view effectId); void update(float dt); @@ -45,11 +48,14 @@ namespace MWRender private: struct Effect { + std::string effectId; float mMaxControllerLength; + bool loop; std::shared_ptr mAnimTime; osg::ref_ptr mTransform; }; + std::mutex mEffectsMutex; std::vector mEffects; osg::ref_ptr mParentNode; diff --git a/apps/openmw/mwrender/renderingmanager.cpp b/apps/openmw/mwrender/renderingmanager.cpp index 6afbfcfe7d..7c7b171757 100644 --- a/apps/openmw/mwrender/renderingmanager.cpp +++ b/apps/openmw/mwrender/renderingmanager.cpp @@ -1253,9 +1253,15 @@ namespace MWRender } void RenderingManager::spawnEffect(VFS::Path::NormalizedView model, std::string_view texture, - const osg::Vec3f& worldPosition, float scale, bool isMagicVFX, bool useAmbientLight) + const osg::Vec3f& worldPosition, float scale, bool isMagicVFX, bool useAmbientLight, std::string_view effectId, + bool loop) { - mEffectManager->addEffect(model, texture, worldPosition, scale, isMagicVFX, useAmbientLight); + mEffectManager->addEffect(model, texture, worldPosition, scale, isMagicVFX, useAmbientLight, effectId, loop); + } + + void RenderingManager::removeEffect(std::string_view effectId) + { + mEffectManager->removeEffect(effectId); } void RenderingManager::notifyWorldSpaceChanged() diff --git a/apps/openmw/mwrender/renderingmanager.hpp b/apps/openmw/mwrender/renderingmanager.hpp index 2e573f8276..808b1ee4c0 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -195,7 +195,10 @@ namespace MWRender SkyManager* getSkyManager(); void spawnEffect(VFS::Path::NormalizedView model, std::string_view texture, const osg::Vec3f& worldPosition, - float scale = 1.f, bool isMagicVFX = true, bool useAmbientLight = true); + float scale = 1.f, bool isMagicVFX = true, bool useAmbientLight = true, std::string_view effectId = {}, + bool loop = false); + + void removeEffect(std::string_view effectId); /// Clear all savegame-specific data void clear(); diff --git a/apps/openmw/mwworld/worldimp.cpp b/apps/openmw/mwworld/worldimp.cpp index 57d794c535..df161ff9a6 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -3689,9 +3689,15 @@ namespace MWWorld } void World::spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, - const osg::Vec3f& worldPos, float scale, bool isMagicVFX, bool useAmbientLight) + const osg::Vec3f& worldPos, float scale, bool isMagicVFX, bool useAmbientLight, std::string_view effectId, + bool loop) { - mRendering->spawnEffect(model, textureOverride, worldPos, scale, isMagicVFX, useAmbientLight); + mRendering->spawnEffect(model, textureOverride, worldPos, scale, isMagicVFX, useAmbientLight, effectId, loop); + } + + void World::removeEffect(std::string_view effectId) + { + mRendering->removeEffect(effectId); } struct ResetActorsVisitor diff --git a/apps/openmw/mwworld/worldimp.hpp b/apps/openmw/mwworld/worldimp.hpp index 983682a98f..38569839a0 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -604,8 +604,10 @@ namespace MWWorld void spawnBloodEffect(const MWWorld::Ptr& ptr, const osg::Vec3f& worldPosition) override; void spawnEffect(VFS::Path::NormalizedView model, const std::string& textureOverride, - const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true, - bool useAmbientLight = true) override; + const osg::Vec3f& worldPos, float scale = 1.f, bool isMagicVFX = true, bool useAmbientLight = true, + std::string_view effectId = {}, bool loop = false) override; + + void removeEffect(std::string_view effectId) override; /// @see MWWorld::WeatherManager::isInStorm bool isInStorm() const override; diff --git a/files/data/scripts/omw/worldeventhandlers.lua b/files/data/scripts/omw/worldeventhandlers.lua index 4e7f96fcb7..78960f33a9 100644 --- a/files/data/scripts/omw/worldeventhandlers.lua +++ b/files/data/scripts/omw/worldeventhandlers.lua @@ -7,5 +7,6 @@ return { SetGameTimeScale = function(scale) world.setGameTimeScale(scale) end, SetSimulationTimeScale = function(scale) world.setSimulationTimeScale(scale) end, SpawnVfx = function(data) world.vfx.spawn(data.model, data.position, data.options) end, + RemoveVfx = function(vfxId) world.vfx.remove(vfxId) end, }, } diff --git a/files/lua_api/openmw/world.lua b/files/lua_api/openmw/world.lua index b72120a8e5..8334f98b16 100644 --- a/files/lua_api/openmw/world.lua +++ b/files/lua_api/openmw/world.lua @@ -196,6 +196,8 @@ -- * `particleTextureOverride` - Name of a particle texture that should override this effect's default texture. (default: "") -- * `scale` - A number that scales the size of the vfx (Default: 1) -- * `useAmbientLighting` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: 1) +-- * `loop` - boolean, if true the effect will loop until removed (default: 0). +-- * `vfxId` - a string ID that can be used to remove the effect later, using #remove. (Default: ""). -- -- @usage -- Spawn a sanctuary effect near the player -- local effect = core.magic.effects.records[core.magic.EFFECT_TYPE.Sanctuary] @@ -204,4 +206,13 @@ -- core.sendGlobalEvent('SpawnVfx', {model = model, position = pos}) -- +--- +-- Remove a VFX with the given vfxId. Best invoked through the RemoveVfx global event +-- @function [parent=#VFX] remove +-- @param #string vfxId the vfxId of the vfx to remove. +-- +-- @usage -- Remove the vfx with vfxId "myvfx" +-- core.sendGlobalEvent('RemoveVfx', "myvfx") +-- + return nil