diff --git a/apps/openmw/mwbase/world.hpp b/apps/openmw/mwbase/world.hpp index 157c12af23..a9dc9bd8fe 100644 --- a/apps/openmw/mwbase/world.hpp +++ b/apps/openmw/mwbase/world.hpp @@ -527,9 +527,12 @@ namespace MWBase virtual void spawnRandomCreature(const ESM::RefId& creatureList) = 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 1af7acea36..cce00d49d3 100644 --- a/apps/openmw/mwlua/animationbindings.cpp +++ b/apps/openmw/mwlua/animationbindings.cpp @@ -300,6 +300,16 @@ namespace MWLua { sol::table api(context.mLua->unsafeState(), sol::create); + api["remove"] = [context](std::string vfxId) { + context.mLuaManager->addAction( + [vfxId = vfxId] { + if (vfxId.empty()) + throw std::runtime_error("vfxId is empty"); + MWBase::Environment::get().getWorld()->removeEffect(vfxId); + }, + "openmw.vfx.remove"); + }; + api["spawn"] = [context](std::string_view model, const osg::Vec3f& worldPos, sol::optional options) { if (options) @@ -307,12 +317,14 @@ namespace MWLua bool magicVfx = options->get_or("mwMagicVfx", true); std::string texture = options->get_or("particleTextureOverride", ""); float scale = options->get_or("scale", 1.f); + std::string vfxId = options->get_or("vfxId", ""); + bool loop = options->get_or("loop", false); bool useAmbientLight = options->get_or("useAmbientLight", true); context.mLuaManager->addAction( [model = VFS::Path::Normalized(model), texture = std::move(texture), worldPos, scale, - magicVfx, useAmbientLight]() { + magicVfx, useAmbientLight, vfxId, loop]() { MWBase::Environment::get().getWorld()->spawnEffect( - model, texture, worldPos, scale, magicVfx, useAmbientLight); + model, texture, worldPos, scale, magicVfx, useAmbientLight, vfxId, loop); }, "openmw.vfx.spawn"); } diff --git a/apps/openmw/mwrender/effectmanager.cpp b/apps/openmw/mwrender/effectmanager.cpp index 927f3f7ad0..5844d7c3c9 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.mLoop = loop; + effect.mEffectId = 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 == effect.mEffectId) + { + 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.mLoop) + { + 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..e51bae5f99 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 mEffectId; float mMaxControllerLength; + bool mLoop; 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 0698e8c4ae..0a82374e34 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 4723c59b8a..f9ff9f5945 100644 --- a/apps/openmw/mwrender/renderingmanager.hpp +++ b/apps/openmw/mwrender/renderingmanager.hpp @@ -196,7 +196,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 c07f5b9161..e4cc417b50 100644 --- a/apps/openmw/mwworld/worldimp.cpp +++ b/apps/openmw/mwworld/worldimp.cpp @@ -3707,9 +3707,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 16f91177a1..6938ca4eea 100644 --- a/apps/openmw/mwworld/worldimp.hpp +++ b/apps/openmw/mwworld/worldimp.hpp @@ -609,8 +609,10 @@ namespace MWWorld void spawnRandomCreature(const ESM::RefId& creatureList) 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 22126ce8f4..41d424301c 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) -- * `useAmbientLight` - boolean, vfx get a white ambient light attached in Morrowind. If false don't attach this. (default: true) +-- * `loop` - boolean, if true the effect will loop until removed (default: false). +-- * `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