Lua: Animation bindings

ini_importer_tests
Mads Buvik Sandvei 4 months ago committed by Zackhasacat
parent d1e79028e9
commit a94add741e

@ -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

@ -8,6 +8,7 @@
#include <SDL_events.h>
#include "../mwgui/mode.hpp"
#include "../mwrender/animationpriority.hpp"
#include <components/sdlutil/events.hpp>
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;

@ -9,6 +9,7 @@
#include <vector>
#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;

@ -0,0 +1,365 @@
#include <components/esm3/loadmgef.hpp>
#include <components/esm3/loadstat.hpp>
#include <components/lua/asyncpackage.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/utilpackage.hpp>
#include <components/misc/resourcehelpers.hpp>
#include <components/settings/values.hpp>
#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 <array>
namespace MWLua
{
struct AnimationGroup;
struct TextKeyCallback;
}
namespace sol
{
template <>
struct is_automagical<MWLua::AnimationGroup> : std::false_type
{
};
template <>
struct is_automagical<std::shared_ptr<MWLua::TextKeyCallback>> : 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<ESM::Static>())
return staticOrID.as<const ESM::Static*>();
else
{
ESM::RefId id = ESM::RefId::deserializeText(LuaUtil::cast<std::string_view>(staticOrID));
return MWBase::Environment::get().getWorld()->getStore().get<ESM::Static>().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<sol::optional<Priority>>("priority");
if (asPriorityEnum)
return asPriorityEnum.value();
auto asTable = args.get<sol::optional<sol::table>>("priority");
if (asTable)
{
AnimationPriorities priorities = AnimationPriorities(Priority::Priority_Default);
for (auto entry : asTable.value())
{
if (!entry.first.is<BoneGroup>() || !entry.second.is<Priority>())
throw std::runtime_error("Priority table must consist of BoneGroup-Priority pairs only");
auto group = entry.first.as<BoneGroup>();
auto priority = entry.second.as<Priority>();
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<std::string_view, MWMechanics::Priority>({
{ "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<std::string_view, BlendMask>({
{ "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<std::string_view, BoneGroup>({
{ "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> {
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> {
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> {
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> {
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> {
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<int>::max());
float speed = options.get_or("speed", 1.f);
std::string startKey = options.get_or<std::string>("startkey", "start");
std::string stopKey = options.get_or<std::string>("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<int>::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<std::string>("startkey", "start");
std::string stop = options.get_or<std::string>("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<std::string>("vfxId", ""), loop = options.get_or("loop", false),
bonename = options.get_or<std::string>("bonename", ""),
particleTexture = options.get_or<std::string>("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<std::string>("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;
}
}

@ -0,0 +1,12 @@
#ifndef MWLUA_ANIMATIONBINDINGS_H
#define MWLUA_ANIMATIONBINDINGS_H
#include <sol/forward.hpp>
namespace MWLua
{
sol::table initAnimationPackage(const Context& context);
sol::table initCoreVfxBindings(const Context& context);
}
#endif // MWLUA_ANIMATIONBINDINGS_H

@ -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
{

@ -51,7 +51,14 @@ namespace MWLua
{
MWWorld::CellStore& mCell;
};
using Event = std::variant<OnActive, OnInactive, OnConsume, OnActivate, OnUseItem, OnNewExterior, OnTeleported>;
struct OnAnimationTextKey
{
ESM::RefNum mActor;
std::string mGroupname;
std::string mKey;
};
using Event = std::variant<OnActive, OnInactive, OnConsume, OnActivate, OnUseItem, OnNewExterior, OnTeleported,
OnAnimationTextKey>;
void clear() { mQueue.clear(); }
void addToQueue(Event e) { mQueue.push_back(std::move(e)); }

@ -3,6 +3,8 @@
#include <components/esm3/loadcell.hpp>
#include <components/misc/strings/lower.hpp>
#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)

@ -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" };
};
}

@ -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<std::string>& 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(); }) },

@ -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<MWRender::BoneGroup>(i)] != priority[static_cast<MWRender::BoneGroup>(0)])
priorityAsTable = true;
if (priorityAsTable)
{
sol::table priorityTable = mLua.newTable();
for (uint32_t i = 0; i < MWRender::sNumBlendMasks; i++)
priorityTable[static_cast<MWRender::BoneGroup>(i)] = priority[static_cast<MWRender::BoneGroup>(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.

@ -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 });

@ -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()

@ -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)

@ -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<MWWorld::Ptr>& out) const
{
for (const Actor& actor : mActors)

@ -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<MWWorld::Ptr>& out) const;

@ -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<std::string_view> 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<std::string_view> 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<size_t>::max(), true);
playBlendedAnimation("torch", Priority_Torch, MWRender::BlendMask_LeftArm, false, 1.0f, "start", "stop",
0.0f, std::numeric_limits<size_t>::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<int> 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<std::string_view> 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);
}
}

@ -138,9 +138,13 @@ namespace MWMechanics
float mTime;
bool mLooping;
bool mScripted;
std::string mStartKey;
std::string mStopKey;
float mSpeed;
};
typedef std::deque<AnimationQueueEntry> 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
{

@ -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);

@ -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)

@ -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<MWWorld::Ptr>& out) const
{
for (const CharacterController& object : mObjects)

@ -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<MWWorld::Ptr>& out) const;

@ -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);
}
}
}

@ -285,8 +285,8 @@ namespace
const ESM::Static* absorbStatic = esmStore.get<ESM::Static>().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<ESM::Spell>().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<ESM::Static>().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));
}
}

@ -105,7 +105,7 @@ namespace MWMechanics
const ESM::Static* fx
= world->getStore().get<ESM::Static>().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)

@ -46,6 +46,7 @@
#include <components/settings/values.hpp>
#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<MWRender::UpdateVfxCallback*>(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<MWRender::UpdateVfxCallback*>(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<osg::LightModel> getVFXLightModelInstance()
@ -447,7 +447,7 @@ namespace MWRender
typedef std::map<std::string, osg::ref_ptr<SceneUtil::KeyframeController>> 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<std::string_view> 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<std::string_view> 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<int>& out) const
std::vector<std::string_view> Animation::getLoopingEffects() const
{
if (!mHasMagicEffects)
return;
return {};
FindVfxCallbacksVisitor visitor;
mInsert->accept(visitor);
std::vector<std::string_view> out;
for (std::vector<UpdateVfxCallback*>::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()

@ -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<EffectAnimationTime> 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<int>& out) const;
std::vector<std::string_view> 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.

@ -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

@ -0,0 +1,22 @@
#ifndef GAME_RENDER_BLENDMASK_H
#define GAME_RENDER_BLENDMASK_H
#include <cstddef>
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

@ -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

@ -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

@ -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<osg::Node> 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<osg::Node> cloudMeshChild = mSceneManager->getInstance(Settings::models().mSkyclouds, mCloudMesh);
osg::ref_ptr<osg::Node> 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<osg::Node> 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);

@ -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;
}

@ -867,7 +867,7 @@ namespace Resource
return static_cast<osg::Node*>(mErrorMarker->clone(osg::CopyOp::DEEP_COPY_ALL));
}
osg::ref_ptr<const osg::Node> SceneManager::getTemplate(const std::string& name, bool compile)
osg::ref_ptr<const osg::Node> SceneManager::getTemplate(std::string_view name, bool compile)
{
std::string normalized = VFS::Path::normalizeFilename(name);
@ -927,7 +927,7 @@ namespace Resource
}
}
osg::ref_ptr<osg::Node> SceneManager::getInstance(const std::string& name)
osg::ref_ptr<osg::Node> SceneManager::getInstance(std::string_view name)
{
osg::ref_ptr<const osg::Node> scene = getTemplate(name);
return getInstance(scene);
@ -968,7 +968,7 @@ namespace Resource
return cloned;
}
osg::ref_ptr<osg::Node> SceneManager::getInstance(const std::string& name, osg::Group* parentNode)
osg::ref_ptr<osg::Node> SceneManager::getInstance(std::string_view name, osg::Group* parentNode)
{
osg::ref_ptr<osg::Node> cloned = getInstance(name);
attachTo(cloned, parentNode);

@ -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<const osg::Node> getTemplate(const std::string& name, bool compile = true);
osg::ref_ptr<const osg::Node> 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<osg::Node> getInstance(const std::string& name);
osg::ref_ptr<osg::Node> 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<osg::Node> getInstance(const std::string& name, osg::Group* parentNode);
osg::ref_ptr<osg::Node> 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,

@ -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

@ -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

@ -0,0 +1,8 @@
Interface AnimationController
=============================
.. include:: version.rst
.. raw:: html
:file: generated_html/scripts_omw_mechanics_animationcontroller.html

@ -0,0 +1,7 @@
Package openmw.animation
========================
.. include:: version.rst
.. raw:: html
:file: generated_html/openmw_animation.html

@ -10,6 +10,9 @@
* - :ref:`AI <Interface AI>`
- by local scripts
- Control basic AI of NPCs and creatures.
* - :ref:`AnimationController <Interface AnimationController>`
- by local scripts
- Control animations of NPCs and creatures.
* - :ref:`Camera <Interface Camera>`
- by player scripts
- | Allows to alter behavior of the built-in camera script

@ -13,6 +13,8 @@
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.types <Package openmw.types>` | everywhere | | Functions for specific types of game objects. |
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.animation <Package openmw.animation>` | everywhere | | Animation controls |
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.async <Package openmw.async>` | everywhere | | Timers and callbacks. |
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.vfs <Package openmw.vfs>` | everywhere | | Read-only access to data directories via VFS. |

@ -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

@ -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

@ -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,
}
}

@ -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

@ -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

@ -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

Loading…
Cancel
Save