diff --git a/apps/openmw/CMakeLists.txt b/apps/openmw/CMakeLists.txt index 373de3683d..566aedfff0 100644 --- a/apps/openmw/CMakeLists.txt +++ b/apps/openmw/CMakeLists.txt @@ -24,7 +24,7 @@ add_openmw_dir (mwrender bulletdebugdraw globalmap characterpreview camera localmap water terrainstorage ripplesimulation renderbin actoranimation landmanager navmesh actorspaths recastmesh fogmanager objectpaging groundcover postprocessor pingpongcull luminancecalculator pingpongcanvas transparentpass precipitationocclusion ripples - actorutil distortion + actorutil distortion animationpriority bonegroup blendmask ) add_openmw_dir (mwinput @@ -64,7 +64,7 @@ add_openmw_dir (mwlua context globalscripts localscripts playerscripts luabindings objectbindings cellbindings mwscriptbindings camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings postprocessingbindings stats debugbindings itemdata types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus types/potion types/ingredient types/misc types/repair types/armor types/light types/static types/clothing types/levelledlist types/terminal - worker magicbindings factionbindings classbindings + worker magicbindings factionbindings classbindings animationbindings ) add_openmw_dir (mwsound @@ -109,7 +109,7 @@ add_openmw_dir (mwstate add_openmw_dir (mwbase environment world scriptmanager dialoguemanager journal soundmanager mechanicsmanager - inputmanager windowmanager statemanager + inputmanager windowmanager statemanager luamanager ) # Main executable diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index f3cea83224..0503fcec9e 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -8,6 +8,7 @@ #include #include "../mwgui/mode.hpp" +#include "../mwrender/animationpriority.hpp" #include namespace MWWorld @@ -60,10 +61,14 @@ namespace MWBase virtual void itemConsumed(const MWWorld::Ptr& consumable, const MWWorld::Ptr& actor) = 0; virtual void objectActivated(const MWWorld::Ptr& object, const MWWorld::Ptr& actor) = 0; virtual void useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) = 0; + virtual void animationTextKey(const MWWorld::Ptr& actor, const std::string& key) = 0; + virtual void playAnimation(const MWWorld::Ptr& object, const std::string& groupname, + const MWRender::AnimPriority& priority, int blendMask, bool autodisable, float speedmult, + std::string_view start, std::string_view stop, float startpoint, size_t loops, bool loopfallback) + = 0; virtual void exteriorCreated(MWWorld::CellStore& cell) = 0; virtual void actorDied(const MWWorld::Ptr& actor) = 0; virtual void questUpdated(const ESM::RefId& questId, int stage) = 0; - // `arg` is either forwarded from MWGui::pushGuiMode or empty virtual void uiModeChanged(const MWWorld::Ptr& arg) = 0; diff --git a/apps/openmw/mwbase/mechanicsmanager.hpp b/apps/openmw/mwbase/mechanicsmanager.hpp index 9e99a37ec7..532100af7a 100644 --- a/apps/openmw/mwbase/mechanicsmanager.hpp +++ b/apps/openmw/mwbase/mechanicsmanager.hpp @@ -9,6 +9,7 @@ #include #include "../mwmechanics/greetingstate.hpp" +#include "../mwrender/animationpriority.hpp" #include "../mwworld/ptr.hpp" @@ -170,16 +171,33 @@ namespace MWBase ///< Forces an object to refresh its animation state. virtual bool playAnimationGroup( - const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number = 1, bool persist = false) + const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number = 1, bool scripted = false) = 0; ///< Run animation for a MW-reference. Calls to this function for references that are currently not /// in the scene should be ignored. /// /// \param mode 0 normal, 1 immediate start, 2 immediate loop - /// \param count How many times the animation should be run - /// \param persist Whether the animation state should be stored in saved games - /// and persist after cell unload. + /// \param number How many times the animation should be run + /// \param scripted Whether the animation should be treated as a scripted animation. /// \return Success or error + virtual bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, int loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop) + = 0; + ///< Lua variant of playAnimationGroup. The mode parameter is omitted + /// and forced to 0. modes 1 and 2 can be emulated by doing clearAnimationQueue() and + /// setting the startKey. + /// + /// \param number How many times the animation should be run + /// \param speed How fast to play the animation, where 1.f = normal speed + /// \param startKey Which textkey to start the animation from + /// \param stopKey Which textkey to stop the animation on + /// \param forceLoop Force the animation to be looping, even if it's normally not looping. + /// \param blendMask See MWRender::Animation::BlendMask + /// \param scripted Whether the animation should be treated as as scripted animation + /// \return Success or error + /// + + virtual void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) = 0; virtual void skipAnimation(const MWWorld::Ptr& ptr) = 0; ///< Skip the animation for the given MW-reference for one frame. Calls to this function for @@ -192,6 +210,9 @@ namespace MWBase /// Save the current animation state of managed references to their RefData. virtual void persistAnimationStates() = 0; + /// Clear out the animation queue, and cancel any animation currently playing from the queue + virtual void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) = 0; + /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently /// paused we may want to do it manually (after equipping permanent enchantment) virtual void updateMagicEffects(const MWWorld::Ptr& ptr) = 0; diff --git a/apps/openmw/mwlua/animationbindings.cpp b/apps/openmw/mwlua/animationbindings.cpp new file mode 100644 index 0000000000..272685dc11 --- /dev/null +++ b/apps/openmw/mwlua/animationbindings.cpp @@ -0,0 +1,365 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" +#include "../mwbase/world.hpp" + +#include "../mwmechanics/character.hpp" + +#include "../mwworld/esmstore.hpp" + +#include "context.hpp" +#include "luamanagerimp.hpp" +#include "objectvariant.hpp" + +#include "animationbindings.hpp" +#include + +namespace MWLua +{ + struct AnimationGroup; + struct TextKeyCallback; +} + +namespace sol +{ + template <> + struct is_automagical : std::false_type + { + }; + template <> + struct is_automagical> : std::false_type + { + }; +} + +namespace MWLua +{ + using BlendMask = MWRender::Animation::BlendMask; + using BoneGroup = MWRender::Animation::BoneGroup; + using Priority = MWMechanics::Priority; + using AnimationPriorities = MWRender::Animation::AnimPriority; + + MWWorld::Ptr getMutablePtrOrThrow(const ObjectVariant& variant) + { + if (variant.isLObject()) + throw std::runtime_error("Local scripts can only modify animations of the object they are attached to."); + + MWWorld::Ptr ptr = variant.ptr(); + if (ptr.isEmpty()) + throw std::runtime_error("Invalid object"); + if (!ptr.getRefData().isEnabled()) + throw std::runtime_error("Can't use a disabled object"); + + return ptr; + } + + MWWorld::Ptr getPtrOrThrow(const ObjectVariant& variant) + { + MWWorld::Ptr ptr = variant.ptr(); + if (ptr.isEmpty()) + throw std::runtime_error("Invalid object"); + + return ptr; + } + + MWRender::Animation* getMutableAnimationOrThrow(const ObjectVariant& variant) + { + MWWorld::Ptr ptr = getMutablePtrOrThrow(variant); + auto world = MWBase::Environment::get().getWorld(); + MWRender::Animation* anim = world->getAnimation(ptr); + if (!anim) + throw std::runtime_error("Object has no animation"); + return anim; + } + + const MWRender::Animation* getConstAnimationOrThrow(const ObjectVariant& variant) + { + MWWorld::Ptr ptr = getPtrOrThrow(variant); + auto world = MWBase::Environment::get().getWorld(); + const MWRender::Animation* anim = world->getAnimation(ptr); + if (!anim) + throw std::runtime_error("Object has no animation"); + return anim; + } + + const ESM::Static* getStatic(const sol::object& staticOrID) + { + if (staticOrID.is()) + return staticOrID.as(); + else + { + ESM::RefId id = ESM::RefId::deserializeText(LuaUtil::cast(staticOrID)); + return MWBase::Environment::get().getWorld()->getStore().get().find(id); + } + } + + std::string getStaticModelOrThrow(const sol::object& staticOrID) + { + const ESM::Static* static_ = getStatic(staticOrID); + if (!static_) + throw std::runtime_error("Invalid static"); + + return Misc::ResourceHelpers::correctMeshPath(static_->mModel); + } + + static AnimationPriorities getPriorityArgument(const sol::table& args) + { + auto asPriorityEnum = args.get>("priority"); + if (asPriorityEnum) + return asPriorityEnum.value(); + + auto asTable = args.get>("priority"); + if (asTable) + { + AnimationPriorities priorities = AnimationPriorities(Priority::Priority_Default); + for (auto entry : asTable.value()) + { + if (!entry.first.is() || !entry.second.is()) + throw std::runtime_error("Priority table must consist of BoneGroup-Priority pairs only"); + auto group = entry.first.as(); + auto priority = entry.second.as(); + if (group < 0 || group >= BoneGroup::Num_BoneGroups) + throw std::runtime_error("Invalid bonegroup: " + std::to_string(group)); + priorities[group] = priority; + } + + return priorities; + } + + return Priority::Priority_Default; + } + + sol::table initAnimationPackage(const Context& context) + { + auto* lua = context.mLua; + auto mechanics = MWBase::Environment::get().getMechanicsManager(); + auto world = MWBase::Environment::get().getWorld(); + + sol::table api(lua->sol(), sol::create); + + api["PRIORITY"] + = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ + { "Default", MWMechanics::Priority::Priority_Default }, + { "WeaponLowerBody", MWMechanics::Priority::Priority_WeaponLowerBody }, + { "SneakIdleLowerBody", MWMechanics::Priority::Priority_SneakIdleLowerBody }, + { "SwimIdle", MWMechanics::Priority::Priority_SwimIdle }, + { "Jump", MWMechanics::Priority::Priority_Jump }, + { "Movement", MWMechanics::Priority::Priority_Movement }, + { "Hit", MWMechanics::Priority::Priority_Hit }, + { "Weapon", MWMechanics::Priority::Priority_Weapon }, + { "Block", MWMechanics::Priority::Priority_Block }, + { "Knockdown", MWMechanics::Priority::Priority_Knockdown }, + { "Torch", MWMechanics::Priority::Priority_Torch }, + { "Storm", MWMechanics::Priority::Priority_Storm }, + { "Death", MWMechanics::Priority::Priority_Death }, + { "Scripted", MWMechanics::Priority::Priority_Scripted }, + })); + + api["BLEND_MASK"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ + { "LowerBody", BlendMask::BlendMask_LowerBody }, + { "Torso", BlendMask::BlendMask_Torso }, + { "LeftArm", BlendMask::BlendMask_LeftArm }, + { "RightArm", BlendMask::BlendMask_RightArm }, + { "UpperBody", BlendMask::BlendMask_UpperBody }, + { "All", BlendMask::BlendMask_All }, + })); + + api["BONE_GROUP"] = LuaUtil::makeStrictReadOnly(context.mLua->tableFromPairs({ + { "LowerBody", BoneGroup::BoneGroup_LowerBody }, + { "Torso", BoneGroup::BoneGroup_Torso }, + { "LeftArm", BoneGroup::BoneGroup_LeftArm }, + { "RightArm", BoneGroup::BoneGroup_RightArm }, + })); + + api["hasAnimation"] = [world](const sol::object& object) -> bool { + return world->getAnimation(getPtrOrThrow(ObjectVariant(object))) != nullptr; + }; + + // equivalent to MWScript's SkipAnim + api["skipAnimationThisFrame"] = [mechanics](const sol::object& object) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + // This sets a flag that is only used during the update pass, so + // there's no need to queue + mechanics->skipAnimation(ptr); + }; + + api["getTextKeyTime"] = [](const sol::object& object, std::string_view key) -> sol::optional { + float time = getConstAnimationOrThrow(ObjectVariant(object))->getTextKeyTime(key); + if (time >= 0.f) + return time; + return sol::nullopt; + }; + api["isPlaying"] = [](const sol::object& object, std::string_view groupname) { + return getConstAnimationOrThrow(ObjectVariant(object))->isPlaying(groupname); + }; + api["getCurrentTime"] = [](const sol::object& object, std::string_view groupname) -> sol::optional { + float time = getConstAnimationOrThrow(ObjectVariant(object))->getCurrentTime(groupname); + if (time >= 0.f) + return time; + return sol::nullopt; + }; + api["isLoopingAnimation"] = [](const sol::object& object, std::string_view groupname) { + return getConstAnimationOrThrow(ObjectVariant(object))->isLoopingAnimation(groupname); + }; + api["cancel"] = [](const sol::object& object, std::string_view groupname) { + return getMutableAnimationOrThrow(ObjectVariant(object))->disable(groupname); + }; + api["setLoopingEnabled"] = [](const sol::object& object, std::string_view groupname, bool enabled) { + return getMutableAnimationOrThrow(ObjectVariant(object))->setLoopingEnabled(groupname, enabled); + }; + // MWRender::Animation::getInfo can also return the current speed multiplier, but this is never used. + api["getCompletion"] = [](const sol::object& object, std::string_view groupname) -> sol::optional { + float completion = 0.f; + if (getConstAnimationOrThrow(ObjectVariant(object))->getInfo(groupname, &completion)) + return completion; + return sol::nullopt; + }; + api["getLoopCount"] = [](const sol::object& object, std::string groupname) -> sol::optional { + size_t loops = 0; + if (getConstAnimationOrThrow(ObjectVariant(object))->getInfo(groupname, nullptr, nullptr, &loops)) + return loops; + return sol::nullopt; + }; + api["getSpeed"] = [](const sol::object& object, std::string groupname) -> sol::optional { + float speed = 0.f; + if (getConstAnimationOrThrow(ObjectVariant(object))->getInfo(groupname, nullptr, &speed, nullptr)) + return speed; + return sol::nullopt; + }; + api["setSpeed"] = [](const sol::object& object, std::string groupname, float speed) { + getMutableAnimationOrThrow(ObjectVariant(object))->adjustSpeedMult(groupname, speed); + }; + api["getActiveGroup"] = [](const sol::object& object, MWRender::BoneGroup boneGroup) -> std::string_view { + return getConstAnimationOrThrow(ObjectVariant(object))->getActiveGroup(boneGroup); + }; + + // Clears out the animation queue, and cancel any animation currently playing from the queue + api["clearAnimationQueue"] = [mechanics](const sol::object& object, bool clearScripted) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + mechanics->clearAnimationQueue(ptr, clearScripted); + }; + + // Extended variant of MWScript's PlayGroup and LoopGroup + api["playQueued"] = sol::overload( + [mechanics](const sol::object& object, const std::string& groupname, const sol::table& options) { + int numberOfLoops = options.get_or("loops", std::numeric_limits::max()); + float speed = options.get_or("speed", 1.f); + std::string startKey = options.get_or("startkey", "start"); + std::string stopKey = options.get_or("stopkey", "stop"); + bool forceLoop = options.get_or("forceloop", false); + + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + mechanics->playAnimationGroupLua(ptr, groupname, numberOfLoops, speed, startKey, stopKey, forceLoop); + }, + [mechanics](const sol::object& object, const std::string& groupname) { + MWWorld::Ptr ptr = getMutablePtrOrThrow(ObjectVariant(object)); + mechanics->playAnimationGroupLua( + ptr, groupname, std::numeric_limits::max(), 1, "start", "stop", false); + }); + + api["playBlended"] = [](const sol::object& object, std::string_view groupname, const sol::table& options) { + int loops = options.get_or("loops", 0); + MWRender::Animation::AnimPriority priority = getPriorityArgument(options); + BlendMask blendMask = options.get_or("blendmask", BlendMask::BlendMask_All); + bool autoDisable = options.get_or("autodisable", true); + float speed = options.get_or("speed", 1.0f); + std::string start = options.get_or("startkey", "start"); + std::string stop = options.get_or("stopkey", "stop"); + float startpoint = options.get_or("startpoint", 0.0f); + bool forceLoop = options.get_or("forceloop", false); + + auto animation = getMutableAnimationOrThrow(ObjectVariant(object)); + animation->play(groupname, priority, blendMask, autoDisable, speed, start, stop, startpoint, loops, + forceLoop || animation->isLoopingAnimation(groupname)); + }; + + api["hasGroup"] = [](const sol::object& object, std::string_view groupname) -> bool { + const MWRender::Animation* anim = getConstAnimationOrThrow(ObjectVariant(object)); + return anim->hasAnimation(groupname); + }; + + // Note: This checks the nodemap, and does not read the scene graph itself, and so should be thread safe. + api["hasBone"] = [](const sol::object& object, std::string_view bonename) -> bool { + const MWRender::Animation* anim = getConstAnimationOrThrow(ObjectVariant(object)); + return anim->getNode(bonename) != nullptr; + }; + + api["addVfx"] = sol::overload( + [context](const sol::object& object, const sol::object& staticOrID) { + context.mLuaManager->addAction( + [object = ObjectVariant(object), model = getStaticModelOrThrow(staticOrID)] { + MWRender::Animation* anim = getMutableAnimationOrThrow(object); + anim->addEffect(model, ""); + }, + "addVfxAction"); + }, + [context](const sol::object& object, const sol::object& staticOrID, const sol::table& options) { + context.mLuaManager->addAction( + [object = ObjectVariant(object), model = getStaticModelOrThrow(staticOrID), + effectId = options.get_or("vfxId", ""), loop = options.get_or("loop", false), + bonename = options.get_or("bonename", ""), + particleTexture = options.get_or("particleTextureOverride", "")] { + MWRender::Animation* anim = getMutableAnimationOrThrow(ObjectVariant(object)); + + anim->addEffect(model, effectId, loop, bonename, particleTexture); + }, + "addVfxAction"); + }); + + api["removeVfx"] = [context](const sol::object& object, std::string_view effectId) { + context.mLuaManager->addAction( + [object = ObjectVariant(object), effectId = std::string(effectId)] { + MWRender::Animation* anim = getMutableAnimationOrThrow(object); + anim->removeEffect(effectId); + }, + "removeVfxAction"); + }; + + api["removeAllVfx"] = [context](const sol::object& object) { + context.mLuaManager->addAction( + [object = ObjectVariant(object)] { + MWRender::Animation* anim = getMutableAnimationOrThrow(object); + anim->removeEffects(); + }, + "removeVfxAction"); + }; + + return LuaUtil::makeReadOnly(api); + } + + sol::table initCoreVfxBindings(const Context& context) + { + sol::state_view& lua = context.mLua->sol(); + sol::table api(lua, sol::create); + auto world = MWBase::Environment::get().getWorld(); + + api["spawn"] = sol::overload( + [world, context](const sol::object& staticOrID, const osg::Vec3f& worldPos) { + auto model = getStaticModelOrThrow(staticOrID); + context.mLuaManager->addAction( + [world, model, worldPos]() { world->spawnEffect(model, "", worldPos); }, "openmw.vfx.spawn"); + }, + [world, context](const sol::object& staticOrID, const osg::Vec3f& worldPos, const sol::table& options) { + auto model = getStaticModelOrThrow(staticOrID); + + bool magicVfx = options.get_or("mwMagicVfx", true); + std::string textureOverride = options.get_or("particleTextureOverride", ""); + float scale = options.get_or("scale", 1.f); + + context.mLuaManager->addAction( + [world, model, textureOverride, worldPos, scale, magicVfx]() { + world->spawnEffect(model, textureOverride, worldPos, scale, magicVfx); + }, + "openmw.vfx.spawn"); + }); + + return api; + } +} diff --git a/apps/openmw/mwlua/animationbindings.hpp b/apps/openmw/mwlua/animationbindings.hpp new file mode 100644 index 0000000000..d28dda9208 --- /dev/null +++ b/apps/openmw/mwlua/animationbindings.hpp @@ -0,0 +1,12 @@ +#ifndef MWLUA_ANIMATIONBINDINGS_H +#define MWLUA_ANIMATIONBINDINGS_H + +#include + +namespace MWLua +{ + sol::table initAnimationPackage(const Context& context); + sol::table initCoreVfxBindings(const Context& context); +} + +#endif // MWLUA_ANIMATIONBINDINGS_H diff --git a/apps/openmw/mwlua/engineevents.cpp b/apps/openmw/mwlua/engineevents.cpp index 0fbb13f1cf..43507ff1a5 100644 --- a/apps/openmw/mwlua/engineevents.cpp +++ b/apps/openmw/mwlua/engineevents.cpp @@ -86,6 +86,15 @@ namespace MWLua void operator()(const OnNewExterior& event) const { mGlobalScripts.onNewExterior(GCell{ &event.mCell }); } + void operator()(const OnAnimationTextKey& event) const + { + MWWorld::Ptr actor = getPtr(event.mActor); + if (actor.isEmpty()) + return; + if (auto* scripts = getLocalScripts(actor)) + scripts->onAnimationTextKey(event.mGroupname, event.mKey); + } + private: MWWorld::Ptr getPtr(ESM::RefNum id) const { diff --git a/apps/openmw/mwlua/engineevents.hpp b/apps/openmw/mwlua/engineevents.hpp index 7c706edcd0..bf8d219fd5 100644 --- a/apps/openmw/mwlua/engineevents.hpp +++ b/apps/openmw/mwlua/engineevents.hpp @@ -51,7 +51,14 @@ namespace MWLua { MWWorld::CellStore& mCell; }; - using Event = std::variant; + struct OnAnimationTextKey + { + ESM::RefNum mActor; + std::string mGroupname; + std::string mKey; + }; + using Event = std::variant; void clear() { mQueue.clear(); } void addToQueue(Event e) { mQueue.push_back(std::move(e)); } diff --git a/apps/openmw/mwlua/localscripts.cpp b/apps/openmw/mwlua/localscripts.cpp index 8cf383e985..1d5e710869 100644 --- a/apps/openmw/mwlua/localscripts.cpp +++ b/apps/openmw/mwlua/localscripts.cpp @@ -3,6 +3,8 @@ #include #include +#include "../mwbase/environment.hpp" +#include "../mwbase/mechanicsmanager.hpp" #include "../mwmechanics/aicombat.hpp" #include "../mwmechanics/aiescort.hpp" #include "../mwmechanics/aifollow.hpp" @@ -162,6 +164,10 @@ namespace MWLua MWMechanics::AiSequence& ai = ptr.getClass().getCreatureStats(ptr).getAiSequence(); ai.stack(MWMechanics::AiTravel(target.x(), target.y(), target.z(), false), ptr, cancelOther); }; + selfAPI["_enableLuaAnimations"] = [](SelfObject& self, bool enable) { + const MWWorld::Ptr& ptr = self.ptr(); + MWBase::Environment::get().getMechanicsManager()->enableLuaAnimations(ptr, enable); + }; } LocalScripts::LocalScripts(LuaUtil::LuaState* lua, const LObject& obj) @@ -170,7 +176,7 @@ namespace MWLua { this->addPackage("openmw.self", sol::make_object(lua->sol(), &mData)); registerEngineHandlers({ &mOnActiveHandlers, &mOnInactiveHandlers, &mOnConsumeHandlers, &mOnActivatedHandlers, - &mOnTeleportedHandlers }); + &mOnTeleportedHandlers, &mOnAnimationTextKeyHandlers, &mOnPlayAnimationHandlers }); } void LocalScripts::setActive(bool active) diff --git a/apps/openmw/mwlua/localscripts.hpp b/apps/openmw/mwlua/localscripts.hpp index b87b628a89..230ec93d3c 100644 --- a/apps/openmw/mwlua/localscripts.hpp +++ b/apps/openmw/mwlua/localscripts.hpp @@ -71,6 +71,14 @@ namespace MWLua void onConsume(const LObject& consumable) { callEngineHandlers(mOnConsumeHandlers, consumable); } void onActivated(const LObject& actor) { callEngineHandlers(mOnActivatedHandlers, actor); } void onTeleported() { callEngineHandlers(mOnTeleportedHandlers); } + void onAnimationTextKey(std::string_view groupname, std::string_view key) + { + callEngineHandlers(mOnAnimationTextKeyHandlers, groupname, key); + } + void onPlayAnimation(std::string_view groupname, const sol::table& options) + { + callEngineHandlers(mOnPlayAnimationHandlers, groupname, options); + } void applyStatsCache(); @@ -83,6 +91,8 @@ namespace MWLua EngineHandlerList mOnConsumeHandlers{ "onConsume" }; EngineHandlerList mOnActivatedHandlers{ "onActivated" }; EngineHandlerList mOnTeleportedHandlers{ "onTeleported" }; + EngineHandlerList mOnAnimationTextKeyHandlers{ "_onAnimationTextKey" }; + EngineHandlerList mOnPlayAnimationHandlers{ "_onPlayAnimation" }; }; } diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index 48f2f3e35d..4da34bf9d4 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -35,6 +35,7 @@ #include "mwscriptbindings.hpp" #include "objectlists.hpp" +#include "animationbindings.hpp" #include "camerabindings.hpp" #include "cellbindings.hpp" #include "debugbindings.hpp" @@ -147,6 +148,7 @@ namespace MWLua }; api["contentFiles"] = initContentFilesBindings(lua->sol()); api["sound"] = initCoreSoundBindings(context); + api["vfx"] = initCoreVfxBindings(context); api["getFormId"] = [](std::string_view contentFile, unsigned int index) -> std::string { const std::vector& contentList = MWBase::Environment::get().getWorld()->getContentFiles(); for (size_t i = 0; i < contentList.size(); ++i) @@ -330,6 +332,7 @@ namespace MWLua sol::state_view lua = context.mLua->sol(); MWWorld::DateTimeManager* tm = MWBase::Environment::get().getWorld()->getTimeManager(); return { + { "openmw.animation", initAnimationPackage(context) }, { "openmw.async", LuaUtil::getAsyncPackageInitializer( lua, [tm] { return tm->getSimulationTime(); }, [tm] { return tm->getGameTime(); }) }, diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index 2417e9e340..89402a6008 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -23,6 +23,7 @@ #include "../mwbase/windowmanager.hpp" #include "../mwbase/world.hpp" +#include "../mwrender/bonegroup.hpp" #include "../mwrender/postprocessor.hpp" #include "../mwworld/datetimemanager.hpp" @@ -362,6 +363,49 @@ namespace MWLua mEngineEvents.addToQueue(EngineEvents::OnUseItem{ getId(actor), getId(object), force }); } + void LuaManager::animationTextKey(const MWWorld::Ptr& actor, const std::string& key) + { + auto pos = key.find(": "); + if (pos != std::string::npos) + mEngineEvents.addToQueue( + EngineEvents::OnAnimationTextKey{ getId(actor), key.substr(0, pos), key.substr(pos + 2) }); + } + + void LuaManager::playAnimation(const MWWorld::Ptr& actor, const std::string& groupname, + const MWRender::AnimPriority& priority, int blendMask, bool autodisable, float speedmult, + std::string_view start, std::string_view stop, float startpoint, size_t loops, bool loopfallback) + { + sol::table options = mLua.newTable(); + options["blendmask"] = blendMask; + options["autodisable"] = autodisable; + options["speed"] = speedmult; + options["startkey"] = start; + options["stopkey"] = stop; + options["startpoint"] = startpoint; + options["loops"] = loops; + options["forceloop"] = loopfallback; + + bool priorityAsTable = false; + for (uint32_t i = 1; i < MWRender::sNumBlendMasks; i++) + if (priority[static_cast(i)] != priority[static_cast(0)]) + priorityAsTable = true; + if (priorityAsTable) + { + sol::table priorityTable = mLua.newTable(); + for (uint32_t i = 0; i < MWRender::sNumBlendMasks; i++) + priorityTable[static_cast(i)] = priority[static_cast(i)]; + options["priority"] = priorityTable; + } + else + options["priority"] = priority[MWRender::BoneGroup_LowerBody]; + + // mEngineEvents.addToQueue(event); + // Has to be called immediately, otherwise engine details that depend on animations playing immediately + // break. + if (auto* scripts = actor.getRefData().getLuaScripts()) + scripts->onPlayAnimation(groupname, options); + } + void LuaManager::objectAddedToScene(const MWWorld::Ptr& ptr) { mObjectLists.objectAddedToScene(ptr); // assigns generated RefNum if it is not set yet. diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index 8bd189d8e9..7556abad5d 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -79,6 +79,10 @@ namespace MWLua mEngineEvents.addToQueue(EngineEvents::OnActivate{ getId(actor), getId(object) }); } void useItem(const MWWorld::Ptr& object, const MWWorld::Ptr& actor, bool force) override; + void animationTextKey(const MWWorld::Ptr& actor, const std::string& key) override; + void playAnimation(const MWWorld::Ptr& actor, const std::string& groupname, + const MWRender::AnimPriority& priority, int blendMask, bool autodisable, float speedmult, + std::string_view start, std::string_view stop, float startpoint, size_t loops, bool loopfallback) override; void exteriorCreated(MWWorld::CellStore& cell) override { mEngineEvents.addToQueue(EngineEvents::OnNewExterior{ cell }); diff --git a/apps/openmw/mwlua/magicbindings.cpp b/apps/openmw/mwlua/magicbindings.cpp index 6e35776cba..1e3cb2ab69 100644 --- a/apps/openmw/mwlua/magicbindings.cpp +++ b/apps/openmw/mwlua/magicbindings.cpp @@ -389,6 +389,17 @@ namespace MWLua auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS(); return Misc::ResourceHelpers::correctIconPath(rec.mIcon, vfs); }); + magicEffectT["particle"] + = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view { return rec.mParticle; }); + magicEffectT["continuousVfx"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> bool { + return (rec.mData.mFlags & ESM::MagicEffect::ContinuousVfx) != 0; + }); + magicEffectT["castingStatic"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mCasting.serializeText(); }); + magicEffectT["hitStatic"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mHit.serializeText(); }); + magicEffectT["areaStatic"] = sol::readonly_property( + [](const ESM::MagicEffect& rec) -> std::string { return rec.mArea.serializeText(); }); magicEffectT["name"] = sol::readonly_property([](const ESM::MagicEffect& rec) -> std::string_view { return MWBase::Environment::get() .getWorld() diff --git a/apps/openmw/mwmechanics/activespells.cpp b/apps/openmw/mwmechanics/activespells.cpp index d8e409d9e2..a9c669fce5 100644 --- a/apps/openmw/mwmechanics/activespells.cpp +++ b/apps/openmw/mwmechanics/activespells.cpp @@ -324,7 +324,7 @@ namespace MWMechanics MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(ptr); if (animation && !reflectStatic->mModel.empty()) animation->addEffect(Misc::ResourceHelpers::correctMeshPath(reflectStatic->mModel), - ESM::MagicEffect::Reflect, false); + ESM::MagicEffect::indexToName(ESM::MagicEffect::Reflect), false); caster.getClass().getCreatureStats(caster).getActiveSpells().addSpell(*reflected); } if (removedSpell) diff --git a/apps/openmw/mwmechanics/actors.cpp b/apps/openmw/mwmechanics/actors.cpp index bc3cc3bb6a..92f8a212c9 100644 --- a/apps/openmw/mwmechanics/actors.cpp +++ b/apps/openmw/mwmechanics/actors.cpp @@ -2019,6 +2019,24 @@ namespace MWMechanics return false; } } + + bool Actors::playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, int loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + return iter->second->getCharacterController().playGroupLua( + groupName, speed, startKey, stopKey, loops, forceLoop); + return false; + } + + void Actors::enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->getCharacterController().enableLuaAnimations(enable); + } + void Actors::skipAnimation(const MWWorld::Ptr& ptr) const { const auto iter = mIndex.find(ptr.mRef); @@ -2048,6 +2066,13 @@ namespace MWMechanics actor.getCharacterController().persistAnimationState(); } + void Actors::clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->getCharacterController().clearAnimQueue(clearScripted); + } + void Actors::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const { for (const Actor& actor : mActors) diff --git a/apps/openmw/mwmechanics/actors.hpp b/apps/openmw/mwmechanics/actors.hpp index 7c676ca018..3ead5f069a 100644 --- a/apps/openmw/mwmechanics/actors.hpp +++ b/apps/openmw/mwmechanics/actors.hpp @@ -114,10 +114,14 @@ namespace MWMechanics bool playAnimationGroup( const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool scripted = false) const; + bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, int loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop); + void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable); void skipAnimation(const MWWorld::Ptr& ptr) const; bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) const; bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const; void persistAnimationStates() const; + void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted); void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const; diff --git a/apps/openmw/mwmechanics/character.cpp b/apps/openmw/mwmechanics/character.cpp index f1bec8ce9d..5533cb578b 100644 --- a/apps/openmw/mwmechanics/character.cpp +++ b/apps/openmw/mwmechanics/character.cpp @@ -36,6 +36,7 @@ #include "../mwrender/animation.hpp" #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/mechanicsmanager.hpp" #include "../mwbase/soundmanager.hpp" #include "../mwbase/windowmanager.hpp" @@ -270,7 +271,7 @@ namespace case CharState_IdleSwim: return Priority_SwimIdle; case CharState_IdleSneak: - priority[MWRender::Animation::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody; + priority[MWRender::BoneGroup_LowerBody] = Priority_SneakIdleLowerBody; [[fallthrough]]; default: return priority; @@ -444,8 +445,8 @@ namespace MWMechanics { mHitState = CharState_Block; priority = Priority_Hit; - priority[MWRender::Animation::BoneGroup_LeftArm] = Priority_Block; - priority[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody; + priority[MWRender::BoneGroup_LeftArm] = Priority_Block; + priority[MWRender::BoneGroup_LowerBody] = Priority_WeaponLowerBody; startKey = "block start"; stopKey = "block stop"; } @@ -482,8 +483,7 @@ namespace MWMechanics return; } - mAnimation->play( - mCurrentHit, priority, MWRender::Animation::BlendMask_All, true, 1, startKey, stopKey, 0.0f, ~0ul); + playBlendedAnimation(mCurrentHit, priority, MWRender::BlendMask_All, true, 1, startKey, stopKey, 0.0f, ~0ul); } void CharacterController::refreshJumpAnims(JumpingState jump, bool force) @@ -502,7 +502,7 @@ namespace MWMechanics std::string_view weapShortGroup = getWeaponShortGroup(mWeaponType); std::string jumpAnimName = "jump"; jumpAnimName += weapShortGroup; - MWRender::Animation::BlendMask jumpmask = MWRender::Animation::BlendMask_All; + MWRender::Animation::BlendMask jumpmask = MWRender::BlendMask_All; if (!weapShortGroup.empty() && !mAnimation->hasAnimation(jumpAnimName)) jumpAnimName = fallbackShortWeaponGroup("jump", &jumpmask); @@ -520,10 +520,10 @@ namespace MWMechanics mCurrentJump = jumpAnimName; if (mJumpState == JumpState_InAir) - mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, false, 1.0f, startAtLoop ? "loop start" : "start", - "stop", 0.f, ~0ul); + playBlendedAnimation(jumpAnimName, Priority_Jump, jumpmask, false, 1.0f, + startAtLoop ? "loop start" : "start", "stop", 0.f, ~0ul); else if (mJumpState == JumpState_Landing) - mAnimation->play(jumpAnimName, Priority_Jump, jumpmask, true, 1.0f, "loop stop", "stop", 0.0f, 0); + playBlendedAnimation(jumpAnimName, Priority_Jump, jumpmask, true, 1.0f, "loop stop", "stop", 0.0f, 0); } bool CharacterController::onOpen() const @@ -539,8 +539,8 @@ namespace MWMechanics if (mAnimation->isPlaying("containerclose")) return false; - mAnimation->play("containeropen", Priority_Scripted, MWRender::Animation::BlendMask_All, false, 1.0f, - "start", "stop", 0.f, 0); + mAnimation->play( + "containeropen", Priority_Scripted, MWRender::BlendMask_All, false, 1.0f, "start", "stop", 0.f, 0); if (mAnimation->isPlaying("containeropen")) return false; } @@ -560,8 +560,8 @@ namespace MWMechanics if (animPlaying) startPoint = 1.f - complete; - mAnimation->play("containerclose", Priority_Scripted, MWRender::Animation::BlendMask_All, false, 1.0f, - "start", "stop", startPoint, 0); + mAnimation->play("containerclose", Priority_Scripted, MWRender::BlendMask_All, false, 1.0f, "start", "stop", + startPoint, 0); } } @@ -600,7 +600,7 @@ namespace MWMechanics if (!isRealWeapon(mWeaponType)) { if (blendMask != nullptr) - *blendMask = MWRender::Animation::BlendMask_LowerBody; + *blendMask = MWRender::BlendMask_LowerBody; return baseGroupName; } @@ -619,13 +619,13 @@ namespace MWMechanics // Special case for crossbows - we should apply 1h animations a fallback only for lower body if (mWeaponType == ESM::Weapon::MarksmanCrossbow && blendMask != nullptr) - *blendMask = MWRender::Animation::BlendMask_LowerBody; + *blendMask = MWRender::BlendMask_LowerBody; if (!mAnimation->hasAnimation(groupName)) { groupName = baseGroupName; if (blendMask != nullptr) - *blendMask = MWRender::Animation::BlendMask_LowerBody; + *blendMask = MWRender::BlendMask_LowerBody; } return groupName; @@ -658,7 +658,7 @@ namespace MWMechanics } } - MWRender::Animation::BlendMask movemask = MWRender::Animation::BlendMask_All; + MWRender::Animation::BlendMask movemask = MWRender::BlendMask_All; std::string_view weapShortGroup = getWeaponShortGroup(mWeaponType); @@ -749,7 +749,7 @@ namespace MWMechanics } } - mAnimation->play( + playBlendedAnimation( mCurrentMovement, Priority_Movement, movemask, false, 1.f, "start", "stop", startpoint, ~0ul, true); } @@ -821,8 +821,8 @@ namespace MWMechanics clearStateAnimation(mCurrentIdle); mCurrentIdle = std::move(idleGroup); - mAnimation->play(mCurrentIdle, priority, MWRender::Animation::BlendMask_All, false, 1.0f, "start", "stop", - startPoint, numLoops, true); + playBlendedAnimation( + mCurrentIdle, priority, MWRender::BlendMask_All, false, 1.0f, "start", "stop", startPoint, numLoops, true); } void CharacterController::refreshCurrentAnims( @@ -855,8 +855,8 @@ namespace MWMechanics resetCurrentIdleState(); resetCurrentJumpState(); - mAnimation->play(mCurrentDeath, Priority_Death, MWRender::Animation::BlendMask_All, false, 1.0f, "start", - "stop", startpoint, 0); + playBlendedAnimation( + mCurrentDeath, Priority_Death, MWRender::BlendMask_All, false, 1.0f, "start", "stop", startpoint, 0); } CharacterState CharacterController::chooseRandomDeathState() const @@ -998,6 +998,8 @@ namespace MWMechanics { std::string_view evt = key->second; + MWBase::Environment::get().getLuaManager()->animationTextKey(mPtr, key->second); + if (evt.substr(0, 7) == "sound: ") { MWBase::SoundManager* sndMgr = MWBase::Environment::get().getSoundManager(); @@ -1189,8 +1191,9 @@ namespace MWMechanics { if (!animPlaying) { - int mask = MWRender::Animation::BlendMask_Torso | MWRender::Animation::BlendMask_RightArm; - mAnimation->play("idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f, ~0ul, true); + int mask = MWRender::BlendMask_Torso | MWRender::BlendMask_RightArm; + playBlendedAnimation( + "idlestorm", Priority_Storm, mask, true, 1.0f, "start", "stop", 0.0f, ~0ul, true); } else { @@ -1247,41 +1250,6 @@ namespace MWMechanics } } - bool CharacterController::isLoopingAnimation(std::string_view group) const - { - // In Morrowind, a some animation groups are always considered looping, regardless - // of loop start/stop keys. - // To be match vanilla behavior we probably only need to check this list, but we don't - // want to prevent modded animations with custom group names from looping either. - static const std::unordered_set loopingAnimations = { "walkforward", "walkback", "walkleft", - "walkright", "swimwalkforward", "swimwalkback", "swimwalkleft", "swimwalkright", "runforward", "runback", - "runleft", "runright", "swimrunforward", "swimrunback", "swimrunleft", "swimrunright", "sneakforward", - "sneakback", "sneakleft", "sneakright", "turnleft", "turnright", "swimturnleft", "swimturnright", - "spellturnleft", "spellturnright", "torch", "idle", "idle2", "idle3", "idle4", "idle5", "idle6", "idle7", - "idle8", "idle9", "idlesneak", "idlestorm", "idleswim", "jump", "inventoryhandtohand", - "inventoryweapononehand", "inventoryweapontwohand", "inventoryweapontwowide" }; - static const std::vector shortGroups = getAllWeaponTypeShortGroups(); - - if (mAnimation && mAnimation->getTextKeyTime(std::string(group) + ": loop start") >= 0) - return true; - - // Most looping animations have variants for each weapon type shortgroup. - // Just remove the shortgroup instead of enumerating all of the possible animation groupnames. - // Make sure we pick the longest shortgroup so e.g. "bow" doesn't get picked over "crossbow" - // when the shortgroup is crossbow. - std::size_t suffixLength = 0; - for (std::string_view suffix : shortGroups) - { - if (suffix.length() > suffixLength && group.ends_with(suffix)) - { - suffixLength = suffix.length(); - } - } - group.remove_suffix(suffixLength); - - return loopingAnimations.count(group) > 0; - } - bool CharacterController::updateWeaponState() { // If the current animation is scripted, we can't do anything here. @@ -1357,8 +1325,8 @@ namespace MWMechanics if (mAnimation->isPlaying("shield")) mAnimation->disable("shield"); - mAnimation->play("torch", Priority_Torch, MWRender::Animation::BlendMask_LeftArm, false, 1.0f, "start", - "stop", 0.0f, std::numeric_limits::max(), true); + playBlendedAnimation("torch", Priority_Torch, MWRender::BlendMask_LeftArm, false, 1.0f, "start", "stop", + 0.0f, std::numeric_limits::max(), true); } else if (mAnimation->isPlaying("torch")) { @@ -1369,7 +1337,7 @@ namespace MWMechanics // For biped actors, blend weapon animations with lower body animations with higher priority MWRender::Animation::AnimPriority priorityWeapon(Priority_Weapon); if (cls.isBipedal(mPtr)) - priorityWeapon[MWRender::Animation::BoneGroup_LowerBody] = Priority_WeaponLowerBody; + priorityWeapon[MWRender::BoneGroup_LowerBody] = Priority_WeaponLowerBody; bool forcestateupdate = false; @@ -1400,19 +1368,19 @@ namespace MWMechanics { // Note: we do not disable unequipping animation automatically to avoid body desync weapgroup = getWeaponAnimation(mWeaponType); - int unequipMask = MWRender::Animation::BlendMask_All; + int unequipMask = MWRender::BlendMask_All; bool useShieldAnims = mAnimation->useShieldAnimations(); if (useShieldAnims && mWeaponType != ESM::Weapon::HandToHand && mWeaponType != ESM::Weapon::Spell && !(mWeaponType == ESM::Weapon::None && weaptype == ESM::Weapon::Spell)) { - unequipMask = unequipMask | ~MWRender::Animation::BlendMask_LeftArm; - mAnimation->play("shield", Priority_Block, MWRender::Animation::BlendMask_LeftArm, true, 1.0f, + unequipMask = unequipMask | ~MWRender::BlendMask_LeftArm; + playBlendedAnimation("shield", Priority_Block, MWRender::BlendMask_LeftArm, true, 1.0f, "unequip start", "unequip stop", 0.0f, 0); } else if (mWeaponType == ESM::Weapon::HandToHand) mAnimation->showCarriedLeft(false); - mAnimation->play( + playBlendedAnimation( weapgroup, priorityWeapon, unequipMask, false, 1.0f, "unequip start", "unequip stop", 0.0f, 0); mUpperBodyState = UpperBodyState::Unequipping; @@ -1458,15 +1426,15 @@ namespace MWMechanics if (weaptype != ESM::Weapon::None) { mAnimation->showWeapons(false); - int equipMask = MWRender::Animation::BlendMask_All; + int equipMask = MWRender::BlendMask_All; if (useShieldAnims && weaptype != ESM::Weapon::Spell) { - equipMask = equipMask | ~MWRender::Animation::BlendMask_LeftArm; - mAnimation->play("shield", Priority_Block, MWRender::Animation::BlendMask_LeftArm, true, - 1.0f, "equip start", "equip stop", 0.0f, 0); + equipMask = equipMask | ~MWRender::BlendMask_LeftArm; + playBlendedAnimation("shield", Priority_Block, MWRender::BlendMask_LeftArm, true, 1.0f, + "equip start", "equip stop", 0.0f, 0); } - mAnimation->play( + playBlendedAnimation( weapgroup, priorityWeapon, equipMask, true, 1.0f, "equip start", "equip stop", 0.0f, 0); mUpperBodyState = UpperBodyState::Equipping; @@ -1617,11 +1585,11 @@ namespace MWMechanics if (mAnimation->getNode("Bip01 L Hand")) mAnimation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), - -1, false, "Bip01 L Hand", effect->mParticle); + "", false, "Bip01 L Hand", effect->mParticle); if (mAnimation->getNode("Bip01 R Hand")) mAnimation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), - -1, false, "Bip01 R Hand", effect->mParticle); + "", false, "Bip01 R Hand", effect->mParticle); } // first effect used for casting animation const ESM::ENAMstruct& firstEffect = effects->front(); @@ -1656,9 +1624,8 @@ namespace MWMechanics startKey = mAttackType + " start"; stopKey = mAttackType + " stop"; } - - mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, - 1, startKey, stopKey, 0.0f, 0); + playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, 1, + startKey, stopKey, 0.0f, 0); mUpperBodyState = UpperBodyState::Casting; } } @@ -1709,8 +1676,8 @@ namespace MWMechanics mAttackVictim = MWWorld::Ptr(); mAttackHitPos = osg::Vec3f(); - mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, - weapSpeed, startKey, stopKey, 0.0f, 0); + playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, weapSpeed, + startKey, stopKey, 0.0f, 0); } } @@ -1783,7 +1750,7 @@ namespace MWMechanics } mAnimation->disable(mCurrentWeapon); - mAnimation->play(mCurrentWeapon, priorityWeapon, MWRender::Animation::BlendMask_All, false, weapSpeed, + playBlendedAnimation(mCurrentWeapon, priorityWeapon, MWRender::BlendMask_All, false, weapSpeed, mAttackType + " max attack", mAttackType + ' ' + hit, startPoint, 0); } @@ -1813,7 +1780,7 @@ namespace MWMechanics // Follow animations have lower priority than movement for non-biped creatures, logic be damned if (!cls.isBipedal(mPtr)) priorityFollow = Priority_Default; - mAnimation->play(mCurrentWeapon, priorityFollow, MWRender::Animation::BlendMask_All, false, weapSpeed, + playBlendedAnimation(mCurrentWeapon, priorityFollow, MWRender::BlendMask_All, false, weapSpeed, mAttackType + ' ' + start, mAttackType + ' ' + stop, 0.0f, 0); mUpperBodyState = UpperBodyState::AttackEnd; @@ -1935,9 +1902,16 @@ namespace MWMechanics mIdleState = CharState_SpecialIdle; auto priority = mAnimQueue.front().mScripted ? Priority_Scripted : Priority_Default; mAnimation->setPlayScriptedOnly(mAnimQueue.front().mScripted); - mAnimation->play(mAnimQueue.front().mGroup, priority, MWRender::Animation::BlendMask_All, false, 1.0f, - (loopStart ? "loop start" : "start"), "stop", mAnimQueue.front().mTime, mAnimQueue.front().mLoopCount, - mAnimQueue.front().mLooping); + if (mAnimQueue.front().mScripted) + mAnimation->play(mAnimQueue.front().mGroup, priority, MWRender::BlendMask_All, false, + mAnimQueue.front().mSpeed, (loopStart ? "loop start" : mAnimQueue.front().mStartKey), + mAnimQueue.front().mStopKey, mAnimQueue.front().mTime, mAnimQueue.front().mLoopCount, + mAnimQueue.front().mLooping); + else + playBlendedAnimation(mAnimQueue.front().mGroup, priority, MWRender::BlendMask_All, false, + mAnimQueue.front().mSpeed, (loopStart ? "loop start" : mAnimQueue.front().mStartKey), + mAnimQueue.front().mStopKey, mAnimQueue.front().mTime, mAnimQueue.front().mLoopCount, + mAnimQueue.front().mLooping); } } @@ -2504,6 +2478,7 @@ namespace MWMechanics state.mScriptedAnims.clear(); for (AnimationQueue::const_iterator iter = mAnimQueue.begin(); iter != mAnimQueue.end(); ++iter) { + // TODO: Probably want to presist lua animations too if (!iter->mScripted) continue; @@ -2541,8 +2516,10 @@ namespace MWMechanics AnimationQueueEntry entry; entry.mGroup = iter->mGroup; entry.mLoopCount = iter->mLoopCount; - entry.mScripted = true; - entry.mLooping = isLoopingAnimation(entry.mGroup); + entry.mLooping = mAnimation->isLoopingAnimation(entry.mGroup); + entry.mStartKey = "start"; + entry.mStopKey = "stop"; + entry.mSpeed = 1.f; entry.mTime = iter->mTime; if (iter->mAbsolute) { @@ -2559,6 +2536,18 @@ namespace MWMechanics } } + void CharacterController::playBlendedAnimation(const std::string& groupname, const MWRender::AnimPriority& priority, + int blendMask, bool autodisable, float speedmult, std::string_view start, std::string_view stop, + float startpoint, size_t loops, bool loopfallback) const + { + if (mLuaAnimations) + MWBase::Environment::get().getLuaManager()->playAnimation(mPtr, groupname, priority, blendMask, autodisable, + speedmult, start, stop, startpoint, loops, loopfallback); + else + mAnimation->play( + groupname, priority, blendMask, autodisable, speedmult, start, stop, startpoint, loops, loopfallback); + } + bool CharacterController::playGroup(std::string_view groupname, int mode, int count, bool scripted) { if (!mAnimation || !mAnimation->hasAnimation(groupname)) @@ -2568,7 +2557,7 @@ namespace MWMechanics if (isScriptedAnimPlaying() && !scripted) return true; - bool looping = isLoopingAnimation(groupname); + bool looping = mAnimation->isLoopingAnimation(groupname); // If this animation is a looped animation that is already playing // and has not yet reached the end of the loop, allow it to continue animating with its existing loop count @@ -2602,8 +2591,12 @@ namespace MWMechanics entry.mGroup = groupname; entry.mLoopCount = count; entry.mTime = 0.f; - entry.mScripted = scripted; + // "PlayGroup idle" is a special case, used to remove to stop scripted animations playing + entry.mScripted = (scripted && groupname != "idle"); entry.mLooping = looping; + entry.mSpeed = 1.f; + entry.mStartKey = ((mode == 2) ? "loop start" : "start"); + entry.mStopKey = "stop"; bool playImmediately = false; @@ -2618,10 +2611,6 @@ namespace MWMechanics mAnimQueue.resize(1); } - // "PlayGroup idle" is a special case, used to stop and remove scripted animations playing - if (groupname == "idle") - entry.mScripted = false; - mAnimQueue.push_back(entry); if (playImmediately) @@ -2630,6 +2619,42 @@ namespace MWMechanics return true; } + bool CharacterController::playGroupLua(std::string_view groupname, float speed, std::string_view startKey, + std::string_view stopKey, int loops, bool forceLoop) + { + // Note: In mwscript, "idle" is a special case used to clear the anim queue. + // In lua we offer an explicit clear method instead so this method does not treat "idle" special. + + if (!mAnimation || !mAnimation->hasAnimation(groupname)) + return false; + + AnimationQueueEntry entry; + entry.mGroup = groupname; + // Note: MWScript gives one less loop to actors than non-actors. + // But this is the Lua version. We don't need to reproduce this weirdness here. + entry.mLoopCount = std::max(loops, 0); + entry.mStartKey = startKey; + entry.mStopKey = stopKey; + entry.mLooping = mAnimation->isLoopingAnimation(groupname) || forceLoop; + entry.mScripted = true; + entry.mSpeed = speed; + entry.mTime = 0; + + if (mAnimQueue.size() > 1) + mAnimQueue.resize(1); + mAnimQueue.push_back(entry); + + if (mAnimQueue.size() == 1) + playAnimQueue(); + + return true; + } + + void CharacterController::enableLuaAnimations(bool enable) + { + mLuaAnimations = enable; + } + void CharacterController::skipAnim() { mSkipAnim = true; @@ -2745,18 +2770,20 @@ namespace MWMechanics // as it's extremely spread out (ActiveSpells, Spells, InventoryStore effects, etc...) so we do it here. // Stop any effects that are no longer active - std::vector effects; - mAnimation->getLoopingEffects(effects); - - for (int effectId : effects) - { - if (mPtr.getClass().getCreatureStats(mPtr).isDeathAnimationFinished() - || mPtr.getClass() - .getCreatureStats(mPtr) - .getMagicEffects() - .getOrDefault(MWMechanics::EffectKey(effectId)) - .getMagnitude() - <= 0) + std::vector effects = mAnimation->getLoopingEffects(); + + for (std::string_view effectId : effects) + { + auto index = ESM::MagicEffect::indexNameToIndex(effectId); + + if (index >= 0 + && (mPtr.getClass().getCreatureStats(mPtr).isDeathAnimationFinished() + || mPtr.getClass() + .getCreatureStats(mPtr) + .getMagicEffects() + .getOrDefault(MWMechanics::EffectKey(index)) + .getMagnitude() + <= 0)) mAnimation->removeEffect(effectId); } } diff --git a/apps/openmw/mwmechanics/character.hpp b/apps/openmw/mwmechanics/character.hpp index dc551900b5..a507c73743 100644 --- a/apps/openmw/mwmechanics/character.hpp +++ b/apps/openmw/mwmechanics/character.hpp @@ -138,9 +138,13 @@ namespace MWMechanics float mTime; bool mLooping; bool mScripted; + std::string mStartKey; + std::string mStopKey; + float mSpeed; }; typedef std::deque AnimationQueue; AnimationQueue mAnimQueue; + bool mLuaAnimations{ false }; CharacterState mIdleState{ CharState_None }; std::string mCurrentIdle; @@ -209,8 +213,6 @@ namespace MWMechanics void refreshMovementAnims(CharacterState movement, bool force = false); void refreshIdleAnims(CharacterState idle, bool force = false); - void clearAnimQueue(bool clearScriptedAnims = false); - bool updateWeaponState(); void updateIdleStormState(bool inwater) const; @@ -247,8 +249,6 @@ namespace MWMechanics void prepareHit(); - bool isLoopingAnimation(std::string_view group) const; - public: CharacterController(const MWWorld::Ptr& ptr, MWRender::Animation* anim); virtual ~CharacterController(); @@ -274,10 +274,17 @@ namespace MWMechanics void persistAnimationState() const; void unpersistAnimationState(); + void playBlendedAnimation(const std::string& groupname, const MWRender::AnimPriority& priority, int blendMask, + bool autodisable, float speedmult, std::string_view start, std::string_view stop, float startpoint, + size_t loops, bool loopfallback = false) const; bool playGroup(std::string_view groupname, int mode, int count, bool scripted = false); + bool playGroupLua(std::string_view groupname, float speed, std::string_view startKey, std::string_view stopKey, + int loops, bool forceLoop); + void enableLuaAnimations(bool enable); void skipAnim(); bool isAnimPlaying(std::string_view groupName) const; bool isScriptedAnimPlaying() const; + void clearAnimQueue(bool clearScriptedAnims = false); enum KillResult { diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp index c9e8e8f322..5323f7e65c 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.cpp @@ -756,6 +756,21 @@ namespace MWMechanics else return mObjects.playAnimationGroup(ptr, groupName, mode, number, scripted); } + bool MechanicsManager::playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, int loops, + float speed, std::string_view startKey, std::string_view stopKey, bool forceLoop) + { + if (ptr.getClass().isActor()) + return mActors.playAnimationGroupLua(ptr, groupName, loops, speed, startKey, stopKey, forceLoop); + else + return mObjects.playAnimationGroupLua(ptr, groupName, loops, speed, startKey, stopKey, forceLoop); + } + void MechanicsManager::enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) + { + if (ptr.getClass().isActor()) + mActors.enableLuaAnimations(ptr, enable); + else + mObjects.enableLuaAnimations(ptr, enable); + } void MechanicsManager::skipAnimation(const MWWorld::Ptr& ptr) { if (ptr.getClass().isActor()) @@ -799,6 +814,14 @@ namespace MWMechanics mObjects.persistAnimationStates(); } + void MechanicsManager::clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) + { + if (ptr.getClass().isActor()) + mActors.clearAnimationQueue(ptr, clearScripted); + else + mObjects.clearAnimationQueue(ptr, clearScripted); + } + void MechanicsManager::updateMagicEffects(const MWWorld::Ptr& ptr) { mActors.updateMagicEffects(ptr); diff --git a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp index f2483396fe..93c1fa3dc2 100644 --- a/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp +++ b/apps/openmw/mwmechanics/mechanicsmanagerimp.hpp @@ -143,10 +143,14 @@ namespace MWMechanics /// @return Success or error bool playAnimationGroup( const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool scripted = false) override; + bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, int loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop) override; + void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) override; void skipAnimation(const MWWorld::Ptr& ptr) override; bool checkAnimationPlaying(const MWWorld::Ptr& ptr, const std::string& groupName) override; bool checkScriptedAnimationPlaying(const MWWorld::Ptr& ptr) const override; void persistAnimationStates() override; + void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) override; /// Update magic effects for an actor. Usually done automatically once per frame, but if we're currently /// paused we may want to do it manually (after equipping permanent enchantment) diff --git a/apps/openmw/mwmechanics/objects.cpp b/apps/openmw/mwmechanics/objects.cpp index 5bdfc91ac7..32d484df2f 100644 --- a/apps/openmw/mwmechanics/objects.cpp +++ b/apps/openmw/mwmechanics/objects.cpp @@ -113,6 +113,23 @@ namespace MWMechanics return false; } } + + bool Objects::playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, int loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + return iter->second->playGroupLua(groupName, speed, startKey, stopKey, loops, forceLoop); + return false; + } + + void Objects::enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->enableLuaAnimations(enable); + } + void Objects::skipAnimation(const MWWorld::Ptr& ptr) { const auto iter = mIndex.find(ptr.mRef); @@ -126,6 +143,13 @@ namespace MWMechanics object.persistAnimationState(); } + void Objects::clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted) + { + const auto iter = mIndex.find(ptr.mRef); + if (iter != mIndex.end()) + iter->second->clearAnimQueue(clearScripted); + } + void Objects::getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const { for (const CharacterController& object : mObjects) diff --git a/apps/openmw/mwmechanics/objects.hpp b/apps/openmw/mwmechanics/objects.hpp index 296f454e4f..1fe43530b0 100644 --- a/apps/openmw/mwmechanics/objects.hpp +++ b/apps/openmw/mwmechanics/objects.hpp @@ -47,8 +47,12 @@ namespace MWMechanics bool playAnimationGroup( const MWWorld::Ptr& ptr, std::string_view groupName, int mode, int number, bool scripted = false); + bool playAnimationGroupLua(const MWWorld::Ptr& ptr, std::string_view groupName, int loops, float speed, + std::string_view startKey, std::string_view stopKey, bool forceLoop); + void enableLuaAnimations(const MWWorld::Ptr& ptr, bool enable); void skipAnimation(const MWWorld::Ptr& ptr); void persistAnimationStates(); + void clearAnimationQueue(const MWWorld::Ptr& ptr, bool clearScripted); void getObjectsInRange(const osg::Vec3f& position, float radius, std::vector& out) const; diff --git a/apps/openmw/mwmechanics/spellcasting.cpp b/apps/openmw/mwmechanics/spellcasting.cpp index e4e07b162f..0496033c70 100644 --- a/apps/openmw/mwmechanics/spellcasting.cpp +++ b/apps/openmw/mwmechanics/spellcasting.cpp @@ -552,8 +552,8 @@ namespace MWMechanics MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(mCaster); if (animation) { - animation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), effect->mIndex, false, - {}, effect->mParticle); + animation->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), + ESM::MagicEffect::indexToName(effect->mIndex), false, {}, effect->mParticle); } else { @@ -626,8 +626,8 @@ namespace MWMechanics { // Don't play particle VFX unless the effect is new or it should be looping. if (playNonLooping || loop) - anim->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), magicEffect.mIndex, loop, - {}, magicEffect.mParticle); + anim->addEffect(Misc::ResourceHelpers::correctMeshPath(castStatic->mModel), + ESM::MagicEffect::indexToName(magicEffect.mIndex), loop, {}, magicEffect.mParticle); } } } diff --git a/apps/openmw/mwmechanics/spelleffects.cpp b/apps/openmw/mwmechanics/spelleffects.cpp index e7146f3e7a..ebf9933cfc 100644 --- a/apps/openmw/mwmechanics/spelleffects.cpp +++ b/apps/openmw/mwmechanics/spelleffects.cpp @@ -285,8 +285,8 @@ namespace const ESM::Static* absorbStatic = esmStore.get().find(ESM::RefId::stringRefId("VFX_Absorb")); MWRender::Animation* animation = MWBase::Environment::get().getWorld()->getAnimation(target); if (animation && !absorbStatic->mModel.empty()) - animation->addEffect( - Misc::ResourceHelpers::correctMeshPath(absorbStatic->mModel), ESM::MagicEffect::SpellAbsorption, false); + animation->addEffect(Misc::ResourceHelpers::correctMeshPath(absorbStatic->mModel), + ESM::MagicEffect::indexToName(ESM::MagicEffect::SpellAbsorption), false); const ESM::Spell* spell = esmStore.get().search(spellId); int spellCost = 0; if (spell) @@ -455,11 +455,11 @@ namespace MWMechanics if (!caster.isEmpty()) { MWRender::Animation* anim = world->getAnimation(caster); - anim->removeEffect(effect.mEffectId); + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); const ESM::Static* fx = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Summon_end")); if (fx) - anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), -1); + anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), ""); } } else if (caster == getPlayer()) @@ -490,7 +490,7 @@ namespace MWMechanics if (!caster.isEmpty()) { MWRender::Animation* anim = world->getAnimation(caster); - anim->removeEffect(effect.mEffectId); + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); } } } @@ -1045,7 +1045,7 @@ namespace MWMechanics effect.mFlags |= ESM::ActiveEffect::Flag_Remove; auto anim = world->getAnimation(target); if (anim) - anim->removeEffect(effect.mEffectId); + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); } else effect.mFlags |= ESM::ActiveEffect::Flag_Applied | ESM::ActiveEffect::Flag_Remove; @@ -1287,7 +1287,7 @@ namespace MWMechanics { auto anim = MWBase::Environment::get().getWorld()->getAnimation(target); if (anim) - anim->removeEffect(effect.mEffectId); + anim->removeEffect(ESM::MagicEffect::indexToName(effect.mEffectId)); } } diff --git a/apps/openmw/mwmechanics/summoning.cpp b/apps/openmw/mwmechanics/summoning.cpp index 85a8d971a9..e4b9e953aa 100644 --- a/apps/openmw/mwmechanics/summoning.cpp +++ b/apps/openmw/mwmechanics/summoning.cpp @@ -105,7 +105,7 @@ namespace MWMechanics const ESM::Static* fx = world->getStore().get().search(ESM::RefId::stringRefId("VFX_Summon_Start")); if (fx) - anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), -1, false); + anim->addEffect(Misc::ResourceHelpers::correctMeshPath(fx->mModel), "", false); } } catch (std::exception& e) diff --git a/apps/openmw/mwrender/animation.cpp b/apps/openmw/mwrender/animation.cpp index feed9719b6..581d2843ab 100644 --- a/apps/openmw/mwrender/animation.cpp +++ b/apps/openmw/mwrender/animation.cpp @@ -46,6 +46,7 @@ #include #include "../mwbase/environment.hpp" +#include "../mwbase/luamanager.hpp" #include "../mwbase/world.hpp" #include "../mwworld/cellstore.hpp" #include "../mwworld/class.hpp" @@ -53,6 +54,7 @@ #include "../mwworld/esmstore.hpp" #include "../mwmechanics/character.hpp" // FIXME: for MWMechanics::Priority +#include "../mwmechanics/weapontype.hpp" #include "actorutil.hpp" #include "rotatecontroller.hpp" @@ -301,11 +303,10 @@ namespace RemoveCallbackVisitor() : RemoveVisitor() , mHasMagicEffects(false) - , mEffectId(-1) { } - RemoveCallbackVisitor(int effectId) + RemoveCallbackVisitor(std::string_view effectId) : RemoveVisitor() , mHasMagicEffects(false) , mEffectId(effectId) @@ -324,7 +325,7 @@ namespace MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); if (vfxCallback) { - bool toRemove = mEffectId < 0 || vfxCallback->mParams.mEffectId == mEffectId; + bool toRemove = mEffectId == "" || vfxCallback->mParams.mEffectId == mEffectId; if (toRemove) mToRemove.emplace_back(group.asNode(), group.getParent(0)); else @@ -338,7 +339,7 @@ namespace void apply(osg::Geometry&) override {} private: - int mEffectId; + std::string_view mEffectId; }; class FindVfxCallbacksVisitor : public osg::NodeVisitor @@ -348,11 +349,10 @@ namespace FindVfxCallbacksVisitor() : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) - , mEffectId(-1) { } - FindVfxCallbacksVisitor(int effectId) + FindVfxCallbacksVisitor(std::string_view effectId) : osg::NodeVisitor(TRAVERSE_ALL_CHILDREN) , mEffectId(effectId) { @@ -368,7 +368,7 @@ namespace MWRender::UpdateVfxCallback* vfxCallback = dynamic_cast(callback); if (vfxCallback) { - if (mEffectId < 0 || vfxCallback->mParams.mEffectId == mEffectId) + if (mEffectId == "" || vfxCallback->mParams.mEffectId == mEffectId) { mCallbacks.push_back(vfxCallback); } @@ -382,7 +382,7 @@ namespace void apply(osg::Geometry&) override {} private: - int mEffectId; + std::string_view mEffectId; }; osg::ref_ptr getVFXLightModelInstance() @@ -447,7 +447,7 @@ namespace MWRender typedef std::map> ControllerMap; - ControllerMap mControllerMap[Animation::sNumBlendMasks]; + ControllerMap mControllerMap[sNumBlendMasks]; const SceneUtil::TextKeyMap& getTextKeys() const; }; @@ -715,6 +715,41 @@ namespace MWRender return mSupportedAnimations.find(anim) != mSupportedAnimations.end(); } + bool Animation::isLoopingAnimation(std::string_view group) const + { + // In Morrowind, a some animation groups are always considered looping, regardless + // of loop start/stop keys. + // To be match vanilla behavior we probably only need to check this list, but we don't + // want to prevent modded animations with custom group names from looping either. + static const std::unordered_set loopingAnimations = { "walkforward", "walkback", "walkleft", + "walkright", "swimwalkforward", "swimwalkback", "swimwalkleft", "swimwalkright", "runforward", "runback", + "runleft", "runright", "swimrunforward", "swimrunback", "swimrunleft", "swimrunright", "sneakforward", + "sneakback", "sneakleft", "sneakright", "turnleft", "turnright", "swimturnleft", "swimturnright", + "spellturnleft", "spellturnright", "torch", "idle", "idle2", "idle3", "idle4", "idle5", "idle6", "idle7", + "idle8", "idle9", "idlesneak", "idlestorm", "idleswim", "jump", "inventoryhandtohand", + "inventoryweapononehand", "inventoryweapontwohand", "inventoryweapontwowide" }; + static const std::vector shortGroups = MWMechanics::getAllWeaponTypeShortGroups(); + + if (getTextKeyTime(std::string(group) + ": loop start") >= 0) + return true; + + // Most looping animations have variants for each weapon type shortgroup. + // Just remove the shortgroup instead of enumerating all of the possible animation groupnames. + // Make sure we pick the longest shortgroup so e.g. "bow" doesn't get picked over "crossbow" + // when the shortgroup is crossbow. + std::size_t suffixLength = 0; + for (std::string_view suffix : shortGroups) + { + if (suffix.length() > suffixLength && group.ends_with(suffix)) + { + suffixLength = suffix.length(); + } + } + group.remove_suffix(suffixLength); + + return loopingAnimations.count(group) > 0; + } + float Animation::getStartTime(const std::string& groupname) const { for (AnimSourceList::const_reverse_iterator iter(mAnimSources.rbegin()); iter != mAnimSources.rend(); ++iter) @@ -758,16 +793,14 @@ namespace MWRender state.mLoopStopTime = key->first; } - if (mTextKeyListener) + try { - try - { + if (mTextKeyListener != nullptr) mTextKeyListener->handleTextKey(groupname, key, map); - } - catch (std::exception& e) - { - Log(Debug::Error) << "Error handling text key " << evt << ": " << e.what(); - } + } + catch (std::exception& e) + { + Log(Debug::Error) << "Error handling text key " << evt << ": " << e.what(); } } @@ -923,7 +956,7 @@ namespace MWRender return true; } - void Animation::setTextKeyListener(Animation::TextKeyListener* listener) + void Animation::setTextKeyListener(TextKeyListener* listener) { mTextKeyListener = listener; } @@ -1052,7 +1085,16 @@ namespace MWRender return true; } - float Animation::getCurrentTime(const std::string& groupname) const + std::string_view Animation::getActiveGroup(BoneGroup boneGroup) const + { + if (auto timePtr = mAnimationTimePtr[boneGroup]->getTimePtr()) + for (auto& state : mStates) + if (state.second.mTime == timePtr) + return state.first; + return ""; + } + + float Animation::getCurrentTime(std::string_view groupname) const { AnimStateMap::const_iterator iter = mStates.find(groupname); if (iter == mStates.end()) @@ -1496,8 +1538,8 @@ namespace MWRender mExtraLightSource->setActorFade(mAlpha); } - void Animation::addEffect( - const std::string& model, int effectId, bool loop, std::string_view bonename, std::string_view texture) + void Animation::addEffect(std::string_view model, std::string_view effectId, bool loop, std::string_view bonename, + std::string_view texture) { if (!mObjectRoot.get()) return; @@ -1579,7 +1621,7 @@ namespace MWRender overrideFirstRootTexture(texture, mResourceSystem, *node); } - void Animation::removeEffect(int effectId) + void Animation::removeEffect(std::string_view effectId) { RemoveCallbackVisitor visitor(effectId); mInsert->accept(visitor); @@ -1589,17 +1631,19 @@ namespace MWRender void Animation::removeEffects() { - removeEffect(-1); + removeEffect(""); } - void Animation::getLoopingEffects(std::vector& out) const + std::vector Animation::getLoopingEffects() const { if (!mHasMagicEffects) - return; + return {}; FindVfxCallbacksVisitor visitor; mInsert->accept(visitor); + std::vector out; + for (std::vector::iterator it = visitor.mCallbacks.begin(); it != visitor.mCallbacks.end(); ++it) { @@ -1608,6 +1652,7 @@ namespace MWRender if (callback->mParams.mLoop && !callback->mFinished) out.push_back(callback->mParams.mEffectId); } + return out; } void Animation::updateEffects() diff --git a/apps/openmw/mwrender/animation.hpp b/apps/openmw/mwrender/animation.hpp index 24366889c4..dae81592b3 100644 --- a/apps/openmw/mwrender/animation.hpp +++ b/apps/openmw/mwrender/animation.hpp @@ -1,6 +1,10 @@ #ifndef GAME_RENDER_ANIMATION_H #define GAME_RENDER_ANIMATION_H +#include "animationpriority.hpp" +#include "blendmask.hpp" +#include "bonegroup.hpp" + #include "../mwworld/movementdirection.hpp" #include "../mwworld/ptr.hpp" @@ -84,7 +88,7 @@ namespace MWRender std::string mModelName; // Just here so we don't add the same effect twice std::shared_ptr mAnimTime; float mMaxControllerLength; - int mEffectId; + std::string mEffectId; bool mLoop; std::string mBoneName; }; @@ -92,60 +96,9 @@ namespace MWRender class Animation : public osg::Referenced { public: - enum BoneGroup - { - BoneGroup_LowerBody = 0, - BoneGroup_Torso, - BoneGroup_LeftArm, - BoneGroup_RightArm - }; - - enum BlendMask - { - BlendMask_LowerBody = 1 << 0, - BlendMask_Torso = 1 << 1, - BlendMask_LeftArm = 1 << 2, - BlendMask_RightArm = 1 << 3, - - BlendMask_UpperBody = BlendMask_Torso | BlendMask_LeftArm | BlendMask_RightArm, - - BlendMask_All = BlendMask_LowerBody | BlendMask_UpperBody - }; - /* This is the number of *discrete* blend masks. */ - static constexpr size_t sNumBlendMasks = 4; - - /// Holds an animation priority value for each BoneGroup. - struct AnimPriority - { - /// Convenience constructor, initialises all priorities to the same value. - AnimPriority(int priority) - { - for (unsigned int i = 0; i < sNumBlendMasks; ++i) - mPriority[i] = priority; - } - - bool operator==(const AnimPriority& other) const - { - for (unsigned int i = 0; i < sNumBlendMasks; ++i) - if (other.mPriority[i] != mPriority[i]) - return false; - return true; - } - - int& operator[](BoneGroup n) { return mPriority[n]; } - - const int& operator[](BoneGroup n) const { return mPriority[n]; } - - bool contains(int priority) const - { - for (unsigned int i = 0; i < sNumBlendMasks; ++i) - if (priority == mPriority[i]) - return true; - return false; - } - - int mPriority[sNumBlendMasks]; - }; + using BlendMask = MWRender::BlendMask; + using BoneGroup = MWRender::BoneGroup; + using AnimPriority = MWRender::AnimPriority; class TextKeyListener { @@ -384,11 +337,11 @@ namespace MWRender * @param texture override the texture specified in the model's materials - if empty, do not override * @note Will not add an effect twice. */ - void addEffect(const std::string& model, int effectId, bool loop = false, std::string_view bonename = {}, - std::string_view texture = {}); - void removeEffect(int effectId); + void addEffect(std::string_view model, std::string_view effectId, bool loop = false, + std::string_view bonename = {}, std::string_view texture = {}); + void removeEffect(std::string_view effectId); void removeEffects(); - void getLoopingEffects(std::vector& out) const; + std::vector getLoopingEffects() const; // Add a spell casting glow to an object. From measuring video taken from the original engine, // the glow seems to be about 1.5 seconds except for telekinesis, which is 1 second. @@ -398,6 +351,8 @@ namespace MWRender bool hasAnimation(std::string_view anim) const; + bool isLoopingAnimation(std::string_view group) const; + // Specifies the axis' to accumulate on. Non-accumulated axis will just // move visually, but not affect the actual movement. Each x/y/z value // should be on the scale of 0 to 1. @@ -446,6 +401,9 @@ namespace MWRender bool getInfo(std::string_view groupname, float* complete = nullptr, float* speedmult = nullptr, size_t* loopcount = nullptr) const; + /// Returns the group name of the animation currently active on that bone group. + std::string_view getActiveGroup(BoneGroup boneGroup) const; + /// Get the absolute position in the animation track of the first text key with the given group. float getStartTime(const std::string& groupname) const; @@ -454,7 +412,7 @@ namespace MWRender /// Get the current absolute position in the animation track for the animation that is currently playing from /// the given group. - float getCurrentTime(const std::string& groupname) const; + float getCurrentTime(std::string_view groupname) const; /** Disables the specified animation group; * \param groupname Animation group to disable. diff --git a/apps/openmw/mwrender/animationpriority.hpp b/apps/openmw/mwrender/animationpriority.hpp new file mode 100644 index 0000000000..048d29901e --- /dev/null +++ b/apps/openmw/mwrender/animationpriority.hpp @@ -0,0 +1,42 @@ +#ifndef GAME_RENDER_ANIMATIONPRIORITY_H +#define GAME_RENDER_ANIMATIONPRIORITY_H + +#include "blendmask.hpp" +#include "bonegroup.hpp" + +namespace MWRender +{ + /// Holds an animation priority value for each BoneGroup. + struct AnimPriority + { + /// Convenience constructor, initialises all priorities to the same value. + AnimPriority(int priority) + { + for (unsigned int i = 0; i < sNumBlendMasks; ++i) + mPriority[i] = priority; + } + + bool operator==(const AnimPriority& other) const + { + for (unsigned int i = 0; i < sNumBlendMasks; ++i) + if (other.mPriority[i] != mPriority[i]) + return false; + return true; + } + + int& operator[](BoneGroup n) { return mPriority[n]; } + + const int& operator[](BoneGroup n) const { return mPriority[n]; } + + bool contains(int priority) const + { + for (unsigned int i = 0; i < sNumBlendMasks; ++i) + if (priority == mPriority[i]) + return true; + return false; + } + + int mPriority[sNumBlendMasks]; + }; +} +#endif diff --git a/apps/openmw/mwrender/blendmask.hpp b/apps/openmw/mwrender/blendmask.hpp new file mode 100644 index 0000000000..f140814d8d --- /dev/null +++ b/apps/openmw/mwrender/blendmask.hpp @@ -0,0 +1,22 @@ +#ifndef GAME_RENDER_BLENDMASK_H +#define GAME_RENDER_BLENDMASK_H + +#include + +namespace MWRender +{ + enum BlendMask + { + BlendMask_LowerBody = 1 << 0, + BlendMask_Torso = 1 << 1, + BlendMask_LeftArm = 1 << 2, + BlendMask_RightArm = 1 << 3, + + BlendMask_UpperBody = BlendMask_Torso | BlendMask_LeftArm | BlendMask_RightArm, + + BlendMask_All = BlendMask_LowerBody | BlendMask_UpperBody + }; + /* This is the number of *discrete* blend masks. */ + static constexpr size_t sNumBlendMasks = 4; +} +#endif diff --git a/apps/openmw/mwrender/bonegroup.hpp b/apps/openmw/mwrender/bonegroup.hpp new file mode 100644 index 0000000000..2afedade86 --- /dev/null +++ b/apps/openmw/mwrender/bonegroup.hpp @@ -0,0 +1,16 @@ +#ifndef GAME_RENDER_BONEGROUP_H +#define GAME_RENDER_BONEGROUP_H + +namespace MWRender +{ + enum BoneGroup + { + BoneGroup_LowerBody = 0, + BoneGroup_Torso, + BoneGroup_LeftArm, + BoneGroup_RightArm, + + Num_BoneGroups + }; +} +#endif diff --git a/apps/openmw/mwrender/characterpreview.cpp b/apps/openmw/mwrender/characterpreview.cpp index 269f7cab75..aa6b5eb4dd 100644 --- a/apps/openmw/mwrender/characterpreview.cpp +++ b/apps/openmw/mwrender/characterpreview.cpp @@ -457,14 +457,14 @@ namespace MWRender mAnimation->showCarriedLeft(showCarriedLeft); mCurrentAnimGroup = std::move(groupname); - mAnimation->play(mCurrentAnimGroup, 1, Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); + mAnimation->play(mCurrentAnimGroup, 1, BlendMask::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); MWWorld::ConstContainerStoreIterator torch = inv.getSlot(MWWorld::InventoryStore::Slot_CarriedLeft); if (torch != inv.end() && torch->getType() == ESM::Light::sRecordId && showCarriedLeft) { if (!mAnimation->getInfo("torch")) mAnimation->play( - "torch", 2, Animation::BlendMask_LeftArm, false, 1.0f, "start", "stop", 0.0f, ~0ul, true); + "torch", 2, BlendMask::BlendMask_LeftArm, false, 1.0f, "start", "stop", 0.0f, ~0ul, true); } else if (mAnimation->getInfo("torch")) mAnimation->disable("torch"); @@ -591,7 +591,7 @@ namespace MWRender void RaceSelectionPreview::onSetup() { CharacterPreview::onSetup(); - mAnimation->play("idle", 1, Animation::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); + mAnimation->play("idle", 1, BlendMask::BlendMask_All, false, 1.0f, "start", "stop", 0.0f, 0); mAnimation->runAnimation(0.f); // attach camera to follow the head node diff --git a/apps/openmw/mwrender/sky.cpp b/apps/openmw/mwrender/sky.cpp index 060b6ee5de..57c3e902d8 100644 --- a/apps/openmw/mwrender/sky.cpp +++ b/apps/openmw/mwrender/sky.cpp @@ -306,7 +306,7 @@ namespace MWRender bool forceShaders = mSceneManager->getForceShaders(); - mAtmosphereDay = mSceneManager->getInstance(Settings::models().mSkyatmosphere, mEarlyRenderBinRoot); + mAtmosphereDay = mSceneManager->getInstance(Settings::models().mSkyatmosphere.get(), mEarlyRenderBinRoot); ModVertexAlphaVisitor modAtmosphere(ModVertexAlphaVisitor::Atmosphere); mAtmosphereDay->accept(modAtmosphere); @@ -319,9 +319,9 @@ namespace MWRender osg::ref_ptr atmosphereNight; if (mSceneManager->getVFS()->exists(Settings::models().mSkynight02.get())) - atmosphereNight = mSceneManager->getInstance(Settings::models().mSkynight02, mAtmosphereNightNode); + atmosphereNight = mSceneManager->getInstance(Settings::models().mSkynight02.get(), mAtmosphereNightNode); else - atmosphereNight = mSceneManager->getInstance(Settings::models().mSkynight01, mAtmosphereNightNode); + atmosphereNight = mSceneManager->getInstance(Settings::models().mSkynight01.get(), mAtmosphereNightNode); atmosphereNight->getOrCreateStateSet()->setAttributeAndModes( createAlphaTrackingUnlitMaterial(), osg::StateAttribute::ON | osg::StateAttribute::OVERRIDE); @@ -341,7 +341,8 @@ namespace MWRender mEarlyRenderBinRoot->addChild(mCloudNode); mCloudMesh = new osg::PositionAttitudeTransform; - osg::ref_ptr cloudMeshChild = mSceneManager->getInstance(Settings::models().mSkyclouds, mCloudMesh); + osg::ref_ptr cloudMeshChild + = mSceneManager->getInstance(Settings::models().mSkyclouds.get(), mCloudMesh); mCloudUpdater = new CloudUpdater(forceShaders); mCloudUpdater->setOpacity(1.f); cloudMeshChild->addUpdateCallback(mCloudUpdater); @@ -349,7 +350,7 @@ namespace MWRender mNextCloudMesh = new osg::PositionAttitudeTransform; osg::ref_ptr nextCloudMeshChild - = mSceneManager->getInstance(Settings::models().mSkyclouds, mNextCloudMesh); + = mSceneManager->getInstance(Settings::models().mSkyclouds.get(), mNextCloudMesh); mNextCloudUpdater = new CloudUpdater(forceShaders); mNextCloudUpdater->setOpacity(0.f); nextCloudMeshChild->addUpdateCallback(mNextCloudUpdater); diff --git a/components/esm3/loadmgef.cpp b/components/esm3/loadmgef.cpp index 686afbc34a..8d5b99b0c3 100644 --- a/components/esm3/loadmgef.cpp +++ b/components/esm3/loadmgef.cpp @@ -631,7 +631,7 @@ namespace ESM { auto name = sIndexNameToIndexMap.find(effect); if (name == sIndexNameToIndexMap.end()) - throw std::runtime_error("Unimplemented effect " + std::string(effect)); + return -1; return name->second; } diff --git a/components/resource/scenemanager.cpp b/components/resource/scenemanager.cpp index 25abcfd0d8..787f2e8441 100644 --- a/components/resource/scenemanager.cpp +++ b/components/resource/scenemanager.cpp @@ -867,7 +867,7 @@ namespace Resource return static_cast(mErrorMarker->clone(osg::CopyOp::DEEP_COPY_ALL)); } - osg::ref_ptr SceneManager::getTemplate(const std::string& name, bool compile) + osg::ref_ptr SceneManager::getTemplate(std::string_view name, bool compile) { std::string normalized = VFS::Path::normalizeFilename(name); @@ -927,7 +927,7 @@ namespace Resource } } - osg::ref_ptr SceneManager::getInstance(const std::string& name) + osg::ref_ptr SceneManager::getInstance(std::string_view name) { osg::ref_ptr scene = getTemplate(name); return getInstance(scene); @@ -968,7 +968,7 @@ namespace Resource return cloned; } - osg::ref_ptr SceneManager::getInstance(const std::string& name, osg::Group* parentNode) + osg::ref_ptr SceneManager::getInstance(std::string_view name, osg::Group* parentNode) { osg::ref_ptr cloned = getInstance(name); attachTo(cloned, parentNode); diff --git a/components/resource/scenemanager.hpp b/components/resource/scenemanager.hpp index c7663a4d91..12900441de 100644 --- a/components/resource/scenemanager.hpp +++ b/components/resource/scenemanager.hpp @@ -157,7 +157,7 @@ namespace Resource /// @note If the given filename does not exist or fails to load, an error marker mesh will be used instead. /// If even the error marker mesh can not be found, an exception is thrown. /// @note Thread safe. - osg::ref_ptr getTemplate(const std::string& name, bool compile = true); + osg::ref_ptr getTemplate(std::string_view name, bool compile = true); /// Clone osg::Node safely. /// @note Thread safe. @@ -172,12 +172,12 @@ namespace Resource /// Instance the given scene template. /// @see getTemplate /// @note Thread safe. - osg::ref_ptr getInstance(const std::string& name); + osg::ref_ptr getInstance(std::string_view name); /// Instance the given scene template and immediately attach it to a parent node /// @see getTemplate /// @note Not thread safe, unless parentNode is not part of the main scene graph yet. - osg::ref_ptr getInstance(const std::string& name, osg::Group* parentNode); + osg::ref_ptr getInstance(std::string_view name, osg::Group* parentNode); /// Attach the given scene instance to the given parent node /// @note You should have the parentNode in its intended position before calling this method, diff --git a/docs/source/luadoc_data_paths.sh b/docs/source/luadoc_data_paths.sh index 7bcda5110c..02b03cbd69 100755 --- a/docs/source/luadoc_data_paths.sh +++ b/docs/source/luadoc_data_paths.sh @@ -2,6 +2,7 @@ paths=( openmw_aux/*lua scripts/omw/activationhandlers.lua scripts/omw/ai.lua + scripts/omw/mechanics/animationcontroller.lua scripts/omw/playercontrols.lua scripts/omw/camera/camera.lua scripts/omw/mwui/init.lua diff --git a/docs/source/reference/lua-scripting/api.rst b/docs/source/reference/lua-scripting/api.rst index 6d27db0515..1bb7e0b6e9 100644 --- a/docs/source/reference/lua-scripting/api.rst +++ b/docs/source/reference/lua-scripting/api.rst @@ -16,6 +16,7 @@ Lua API reference openmw_storage openmw_core openmw_types + openmw_animation openmw_async openmw_vfs openmw_world @@ -33,6 +34,7 @@ Lua API reference openmw_aux_ui interface_activation interface_ai + interface_animation interface_camera interface_controls interface_item_usage diff --git a/docs/source/reference/lua-scripting/interface_animation.rst b/docs/source/reference/lua-scripting/interface_animation.rst new file mode 100644 index 0000000000..5bde11775e --- /dev/null +++ b/docs/source/reference/lua-scripting/interface_animation.rst @@ -0,0 +1,8 @@ +Interface AnimationController +============================= + +.. include:: version.rst + +.. raw:: html + :file: generated_html/scripts_omw_mechanics_animationcontroller.html + diff --git a/docs/source/reference/lua-scripting/openmw_animation.rst b/docs/source/reference/lua-scripting/openmw_animation.rst new file mode 100644 index 0000000000..35ac26ecec --- /dev/null +++ b/docs/source/reference/lua-scripting/openmw_animation.rst @@ -0,0 +1,7 @@ +Package openmw.animation +======================== + +.. include:: version.rst + +.. raw:: html + :file: generated_html/openmw_animation.html diff --git a/docs/source/reference/lua-scripting/tables/interfaces.rst b/docs/source/reference/lua-scripting/tables/interfaces.rst index e05eb642f0..5029baf0a3 100644 --- a/docs/source/reference/lua-scripting/tables/interfaces.rst +++ b/docs/source/reference/lua-scripting/tables/interfaces.rst @@ -10,6 +10,9 @@ * - :ref:`AI ` - by local scripts - Control basic AI of NPCs and creatures. + * - :ref:`AnimationController ` + - by local scripts + - Control animations of NPCs and creatures. * - :ref:`Camera ` - by player scripts - | Allows to alter behavior of the built-in camera script diff --git a/docs/source/reference/lua-scripting/tables/packages.rst b/docs/source/reference/lua-scripting/tables/packages.rst index 67709bbf7b..9a73334b84 100644 --- a/docs/source/reference/lua-scripting/tables/packages.rst +++ b/docs/source/reference/lua-scripting/tables/packages.rst @@ -13,6 +13,8 @@ +------------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.types ` | everywhere | | Functions for specific types of game objects. | +------------------------------------------------------------+--------------------+---------------------------------------------------------------+ +|:ref:`openmw.animation ` | everywhere | | Animation controls | ++------------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.async ` | everywhere | | Timers and callbacks. | +------------------------------------------------------------+--------------------+---------------------------------------------------------------+ |:ref:`openmw.vfs ` | everywhere | | Read-only access to data directories via VFS. | diff --git a/files/data/CMakeLists.txt b/files/data/CMakeLists.txt index 0e91a0b495..3ab30c87ff 100644 --- a/files/data/CMakeLists.txt +++ b/files/data/CMakeLists.txt @@ -75,6 +75,7 @@ set(BUILTIN_DATA_FILES scripts/omw/console/player.lua scripts/omw/console/global.lua scripts/omw/console/local.lua + scripts/omw/mechanics/animationcontroller.lua scripts/omw/mechanics/playercontroller.lua scripts/omw/playercontrols.lua scripts/omw/settings/player.lua diff --git a/files/data/builtin.omwscripts b/files/data/builtin.omwscripts index e4338df533..a6f4ca5f33 100644 --- a/files/data/builtin.omwscripts +++ b/files/data/builtin.omwscripts @@ -10,6 +10,7 @@ GLOBAL: scripts/omw/activationhandlers.lua GLOBAL: scripts/omw/cellhandlers.lua GLOBAL: scripts/omw/usehandlers.lua GLOBAL: scripts/omw/worldeventhandlers.lua +CREATURE, NPC, PLAYER: scripts/omw/mechanics/animationcontroller.lua PLAYER: scripts/omw/mechanics/playercontroller.lua PLAYER: scripts/omw/playercontrols.lua PLAYER: scripts/omw/camera/camera.lua diff --git a/files/data/scripts/omw/mechanics/animationcontroller.lua b/files/data/scripts/omw/mechanics/animationcontroller.lua new file mode 100644 index 0000000000..3293668387 --- /dev/null +++ b/files/data/scripts/omw/mechanics/animationcontroller.lua @@ -0,0 +1,145 @@ +local anim = require('openmw.animation') +local self = require('openmw.self') + +local playBlendedHandlers = {} +local function onPlayBlendedAnimation(groupname, options) + for i = #playBlendedHandlers, 1, -1 do + if playBlendedHandlers[i](groupname, options) == false then + return + end + end +end + +local function playBlendedAnimation(groupname, options) + onPlayBlendedAnimation(groupname, options) + if options.skip then + return + end + anim.playBlended(self, groupname, options) +end + +local textKeyHandlers = {} +local function onAnimationTextKey(groupname, key) + local handlers = textKeyHandlers[groupname] + if handlers then + for i = #handlers, 1, -1 do + if handlers[i](groupname, key) == false then + return + end + end + end + handlers = textKeyHandlers[''] + if handlers then + for i = #handlers, 1, -1 do + if handlers[i](groupname, key) == false then + return + end + end + end +end + +local initialized = false + +local function onUpdate(dt) + -- The script is loaded before the actor's CharacterController object is initialized, therefore + -- we have to delay this initialization step or the call won't have any effect. + if not initialized then + self:_enableLuaAnimations(true) + initialized = true + end +end + +return { + engineHandlers = { + _onPlayAnimation = playBlendedAnimation, + _onAnimationTextKey = onAnimationTextKey, + onUpdate = onUpdate, + }, + + interfaceName = 'AnimationController', + --- + -- Animation controller interface + -- @module AnimationController + -- @usage local anim = require('openmw.animation') + -- local I = require('openmw.interfaces') + -- + -- -- play spellcast animation + -- I.AnimationController.playBlendedAnimation('spellcast', { startkey = 'self start', stopkey = 'self stop', priority = { + -- [anim.BONE_GROUP.RightArm] = anim.PRIORITY.Weapon, + -- [anim.BONE_GROUP.LeftArm] = anim.PRIORITY.Weapon, + -- [anim.BONE_GROUP.Torso] = anim.PRIORITY.Weapon, + -- [anim.BONE_GROUP.LowerBody] = anim.PRIORITY.WeaponLowerBody + -- } }) + -- + -- @usage -- react to the spellcast release textkey + -- I.AnimationController.addTextKeyHandler('spellcast', function(groupname, key) + -- -- Note, Lua is 1-indexed so have to subtract 1 less than the length of 'release' + -- if key.sub(key, #key - 6) == 'release' then + -- print('Abra kadabra!') + -- end + -- end) + -- + -- @usage -- Add a text key handler that will react to all keys + -- I.AnimationController.addTextKeyHandler('', function(groupname, key) + -- if key.sub(key, #key - 2) == 'hit' and not key.sub(key, #key - 7) == ' min hit' then + -- print('Hit!') + -- end + -- end) + -- + -- @usage -- Make a handler that changes player attack speed based on current fatigue + -- I.AnimationController.addPlayBlendedAnimationHandler(function (groupname, options) + -- local stop = options.stopkey + -- if #stop > 10 and stop.sub(stop, #stop - 10) == ' max attack' then + -- -- This is an attack wind up animation, scale its speed by attack + -- local fatigue = Actor.stats.dynamic.fatigue(self) + -- local factor = 1 - fatigue.current / fatigue.base + -- speed = 1 - factor * 0.8 + -- options.speed = speed + -- end + -- end) + -- + + interface = { + --- Interface version + -- @field [parent=#AnimationController] #number version + version = 0, + + --- AnimationController Package + -- @type Package + + --- Make this actor play an animation. Makes a call to @{openmw.animation#playBlended}, after invoking handlers added through addPlayBlendedAnimationHandler + -- @function [parent=#AnimationController] playBlendedAnimation + -- @param #string groupname The animation group to be played + -- @param #table options The table of play options that will be passed to @{openmw.animation#playBlended} + playBlendedAnimation = playBlendedAnimation, + + --- Add new playBlendedAnimation handler for this actor + -- If `handler(groupname, options)` returns false, other handlers for + -- the call will be skipped. + -- @function [parent=#AnimationController] addPlayBlendedAnimationHandler + -- @param #function handler The handler. + addPlayBlendedAnimationHandler = function(handler) + playBlendedHandlers[#playBlendedHandlers + 1] = handler + end, + + --- Add new text key handler for this actor + -- While playing, some animations emit text key events. Register a handle to listen for all + -- text key events associated with this actor's animations. + -- If `handler(groupname, key)` returns false, other handlers for + -- the call will be skipped. + -- @function [parent=#AnimationController] addTextKeyHandler + -- @param #string groupname Name of the animation group to listen to keys for. If the empty string or nil, all keys will be received + -- @param #function handler The handler. + addTextKeyHandler = function(groupname, handler) + if not groupname then + groupname = "" + end + local handlers = textKeyHandlers[groupname] + if handlers == nil then + handlers = {} + textKeyHandlers[groupname] = handlers + end + handlers[#handlers + 1] = handler + end, + } +} \ No newline at end of file diff --git a/files/lua_api/openmw/animation.lua b/files/lua_api/openmw/animation.lua new file mode 100644 index 0000000000..bb5a0594df --- /dev/null +++ b/files/lua_api/openmw/animation.lua @@ -0,0 +1,255 @@ +--- +-- `openmw.animation` defines functions that allow control of character animations +-- Note that for some methods, such as @{openmw.animation#playBlended} you should use the associated methods on the +-- [AnimationController](interface_animation.html) interface rather than invoking this API directly. +-- @module animation +-- @usage local anim = require('openmw.animation') + +--- Possible @{#Priority} values +-- @field [parent=#animation] #Priority PRIORITY + +--- `animation.PRIORITY` +-- @type Priority +-- @field #number Default "0" +-- @field #number WeaponLowerBody "1" +-- @field #number SneakIdleLowerBody "2" +-- @field #number SwimIdle "3" +-- @field #number Jump "4" +-- @field #number Movement "5" +-- @field #number Hit "6" +-- @field #number Weapon "7" +-- @field #number Block "8" +-- @field #number Knockdown "9" +-- @field #number Torch "10" +-- @field #number Storm "11" +-- @field #number Death "12" +-- @field #number Scripted "13" Special priority used by scripted animations. When any animation with this priority is present, all animations without this priority are paused. + +--- Possible @{#BlendMask} values +-- @field [parent=#animation] #BlendMask BLEND_MASK + +--- `animation.BLEND_MASK` +-- @type BlendMask +-- @field #number LowerBody "1" All bones from 'Bip01 pelvis' and below +-- @field #number Torso "2" All bones from 'Bip01 Spine1' and up, excluding arms +-- @field #number LeftArm "4" All bones from 'Bip01 L Clavicle' and out +-- @field #number RightArm "8" All bones from 'Bip01 R Clavicle' and out +-- @field #number UpperBody "14" All bones from 'Bip01 Spine1' and up, including arms +-- @field #number All "15" All bones + +--- Possible @{#BoneGroup} values +-- @field [parent=#animation] #BoneGroup BONE_GROUP + +--- `animation.BONE_GROUP` +-- @type BoneGroup +-- @field #number LowerBody "1" All bones from 'Bip01 pelvis' and below +-- @field #number Torso "2" All bones from 'Bip01 Spine1' and up, excluding arms +-- @field #number LeftArm "3" All bones from 'Bip01 L Clavicle' and out +-- @field #number RightArm "4" All bones from 'Bip01 R Clavicle' and out + + +--- +-- Check if the object has an animation object or not +-- @function [parent=#animation] hasAnimation +-- @param openmw.core#GameObject actor +-- @return #boolean + +--- +-- Skips animations for one frame, equivalent to mwscript's SkipAnim +-- Can be used only in local scripts on self. +-- @function [parent=#animation] skipAnimationThisFrame +-- @param openmw.core#GameObject actor + +--- +-- Get the absolute position within the animation track of the given text key +-- @function [parent=#animation] getTextKeyTime +-- @param openmw.core#GameObject actor +-- @param #string text key +-- @return #number + +--- +-- Check if the given animation group is currently playing +-- @function [parent=#animation] isPlaying +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @return #boolean + +--- +-- Get the current absolute time of the given animation group if it is playing, or -1 if it is not playing. +-- @function [parent=#animation] getCurrentTime +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @return #number + +--- +-- Check whether the animation is a looping animation or not. This is determined by a combination +-- of groupname, some of which are hardcoded to be looping, and the presence of loop start/stop keys. +-- The groupnames that are hardcoded as looping are the following, as well as per-weapon-type suffixed variants of each. +-- "walkforward", "walkback", "walkleft", "walkright", "swimwalkforward", "swimwalkback", "swimwalkleft", "swimwalkright", +-- "runforward", "runback", "runleft", "runright", "swimrunforward", "swimrunback", "swimrunleft", "swimrunright", +-- "sneakforward", "sneakback", "sneakleft", "sneakright", "turnleft", "turnright", "swimturnleft", "swimturnright", +-- "spellturnleft", "spellturnright", "torch", "idle", "idle2", "idle3", "idle4", "idle5", "idle6", "idle7", "idle8", +-- "idle9", "idlesneak", "idlestorm", "idleswim", "jump", "inventoryhandtohand", "inventoryweapononehand", +-- "inventoryweapontwohand", "inventoryweapontwowide" +-- @function [parent=#animation] isLoopingAnimation +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @return #boolean + + +--- +-- Cancels and removes the animation group from the list of active animations +-- Can be used only in local scripts on self. +-- @function [parent=#animation] cancel +-- @param openmw.core#GameObject actor +-- @param #string groupname + +--- +-- Enables or disables looping for the given animation group. Looping is enabled by default. +-- Can be used only in local scripts on self. +-- @function [parent=#animation] setLoopingEnabled +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @param #boolean enabled + +--- +-- Returns the completion of the animation, or nil if the animation group is not active. +-- @function [parent=#animation] getCompletion +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @return #number, #nil + +--- +-- Returns the remaining number of loops, not counting the current loop, or nil if the animation group is not active. +-- @function [parent=#animation] getLoopCount +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @return #number, #nil + +--- +-- Get the current playback speed of an animation group, or nil if the animation group is not active. +-- @function [parent=#animation] getSpeed +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @return #number, #nil + +--- +-- Modifies the playback speed of an animation group. +-- Note that this is not sticky and only affects the speed until the currently playing sequence ends. +-- Can be used only in local scripts on self. +-- @function [parent=#animation] setSpeed +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @param #number speed The new animation speed, where speed=1 is normal speed. + +--- +-- Clears all animations currently in the animation queue. This affects animations played by mwscript, @{openmw.animation#playQueued}, and ai packages, but does not affect animations played using @{openmw.animation#playBlended}. +-- Can be used only in local scripts on self. +-- @function [parent=#animation] clearAnimationQueue +-- @param openmw.core#GameObject actor +-- @param #boolean clearScripted whether to keep animation with priority Scripted or not. + +--- +-- Acts as a slightly extended version of MWScript's LoopGroup. Plays this animation exclusively +-- until it ends, or the queue is cleared using #clearAnimationQueue. Use #clearAnimationQueue and the `startkey` option +-- to imitate the behavior of LoopGroup's play modes. +-- Can be used only in local scripts on self. +-- @function [parent=#animation] playQueued +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @param #table options A table of play options. Can contain: +-- +-- * `loops` - a number >= 0, the number of times the animation should loop after the first play (default: infinite). +-- * `speed` - a floating point number >= 0, the speed at which the animation should play (default: 1); +-- * `startkey` - the animation key at which the animation should start (default: "start") +-- * `stopkey` - the animation key at which the animation should end (default: "stop") +-- * `forceloop` - a boolean, to set if the animation should loop even if it's not a looping animation (default: false) +-- +-- @usage -- Play death1 without waiting. Equivalent to playgroup, death1, 1 +-- anim.clearAnimationQueue(self, false) +-- anim.playQueued(self, 'death1') +-- +-- @usage -- Play an animation group with custom start/stop keys +-- anim.clearAnimationQueue(self, false) +-- anim.playQueued(self, 'spellcast', { startkey = 'self start', stopkey = 'self stop' }) +-- + +--- +-- Play an animation directly. You probably want to use the [AnimationController](interface_animation.html) interface, which will trigger relevant handlers, +-- instead of calling this directly. Note that the still hardcoded character controller may at any time and for any reason alter +-- or cancel currently playing animations, so making your own calls to this function either directly or through the [AnimationController](interface_animation.html) +-- interface may be of limited utility. For now, use openmw.animation#playQueued to script your own animations. +-- Can be used only in local scripts on self. +-- @function [parent=#animation] playBlended +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @param #table options A table of play options. Can contain: +-- +-- * `loops` - a number >= 0, the number of times the animation should loop after the first play (default: 0). +-- * `priority` - Either a single #Priority value that will be assigned to all bone groups. Or a table mapping bone groups to its priority (default: PRIORITY.Default). +-- * `blendMask` - A mask of which bone groups to include in the animation (Default: BLEND_MASK.All. +-- * `autodisable` - If true, the animation will be immediately removed upon finishing, which means information will not be possible to query once completed. (Default: true) +-- * `speed` - a floating point number >= 0, the speed at which the animation should play (default: 1) +-- * `startkey` - the animation key at which the animation should start (default: "start") +-- * `stopkey` - the animation key at which the animation should end (default: "stop") +-- * `startpoint` - a floating point number 0 <= value <= 1, starting completion of the animation (default: 0) +-- * `forceloop` - a boolean, to set if the animation should loop even if it's not a looping animation (default: false) + +--- +-- Check if the actor's animation has the given animation group or not. +-- @function [parent=#animation] hasGroup +-- @param openmw.core#GameObject actor +-- @param #string groupname +-- @return #boolean + +--- +-- Check if the actor's skeleton has the given bone or not +-- @function [parent=#animation] hasBone +-- @param openmw.core#GameObject actor +-- @param #string bonename +-- @return #boolean + +--- +-- Get the current active animation for a bone group +-- @function [parent=#animation] getActiveGroup +-- @param openmw.core#GameObject actor +-- @param #number bonegroup Bone group enum, see @{openmw.animation#BONE_GROUP} +-- @return #string + +--- +-- Plays a VFX on the actor. +-- Can be used only in local scripts on self. +-- @function [parent=#animation] addVfx +-- @param openmw.core#GameObject actor +-- @param #any static @{openmw.core#StaticRecord} or #string ID +-- @param #table options optional table of parameters. Can contain: +-- +-- * `loop` - boolean, if true the effect will loop until removed (default: 0). +-- * `bonename` - name of the bone to attach the vfx to. (default: "") +-- * `particle` - name of the particle texture to use. (default: "") +-- * `vfxId` - a string ID that can be used to remove the effect later, using #removeVfx, and to avoid duplicate effects. The default value of "" can have duplicates. To avoid interaction with the engine, use unique identifiers unrelated to magic effect IDs. The engine uses this identifier to add and remove magic effects based on what effects are active on the actor. If this is set equal to the @{openmw.core#MagicEffectId} identifier of the magic effect being added, for example core.magic.EFFECT_TYPE.FireDamage, then the engine will remove it once the fire damage effect on the actor reaches 0. (Default: ""). +-- +-- @usage local mgef = core.magic.effects[myEffectName] +-- anim.addVfx(self, 'VFX_Hands', {bonename = 'Bip01 L Hand', particle = mgef.particle, loop = mgef.continuousVfx, vfxId = mgef.id..'_myuniquenamehere'}) +-- -- later: +-- anim.removeVfx(self, mgef.id..'_myuniquenamehere') +-- + +--- +-- Removes a specific VFX +-- Can be used only in local scripts on self. +-- @function [parent=#animation] removeVfx +-- @param openmw.core#GameObject actor +-- @param #number vfxId an integer ID that uniquely identifies the VFX to remove + +--- +-- Removes all vfx from the actor +-- Can be used only in local scripts on self. +-- @function [parent=#animation] removeAllVfx +-- @param openmw.core#GameObject actor + + + + +return nil + diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index dae5fc0594..94d63312f1 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -668,6 +668,11 @@ -- @field #number baseCost -- @field openmw.util#Color color -- @field #boolean harmful +-- @field #boolean continuousVfx Whether the magic effect's vfx should loop or not +-- @field #string particle Identifier of the particle texture +-- @field #string castingStatic Identifier of the vfx static used for casting +-- @field #string hitStatic Identifier of the vfx static used on hit +-- @field #string areaStatic Identifier of the vfx static used for AOE spells --- -- @type MagicEffectWithParams @@ -899,4 +904,24 @@ -- @field #number favouredSkillValue Secondary skill value required to get this rank. -- @field #number factionReaction Reaction of faction members if player is in this faction. +--- @{#VFX}: Visual effects +-- @field [parent=#core] #VFX vfx + +--- +-- Spawn a VFX at the given location in the world +-- @function [parent=#VFX] spawn +-- @param #any static openmw.core#StaticRecord or #string ID +-- @param openmw.util#Vector3 location +-- @param #table options optional table of parameters. Can contain: +-- +-- * `mwMagicVfx` - Boolean that if true causes the textureOverride parameter to only affect nodes with the Nif::RC_NiTexturingProperty property set. (default: true). +-- * `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) +-- +-- @usage -- Spawn a sanctuary effect near the player +-- local effect = core.magic.effects[core.magic.EFFECT_TYPE.Sanctuary] +-- pos = self.position + util.vector3(0, 100, 0) +-- core.vfx.spawn(effect.castingStatic, pos) +-- + return nil diff --git a/files/lua_api/openmw/interfaces.lua b/files/lua_api/openmw/interfaces.lua index d4a290aa47..57103768d2 100644 --- a/files/lua_api/openmw/interfaces.lua +++ b/files/lua_api/openmw/interfaces.lua @@ -5,6 +5,9 @@ --- -- @field [parent=#interfaces] scripts.omw.activationhandlers#scripts.omw.activationhandlers Activation +--- +-- @field [parent=#interfaces] scripts.omw.mechanics.animationcontroller#scripts.omw.mechanics.animationcontroller AnimationController + --- -- @field [parent=#interfaces] scripts.omw.ai#scripts.omw.ai AI