mirror of
https://github.com/FirestormViewer/phoenix-firestorm.git
synced 2026-08-14 08:53:53 +00:00
4015 lines
124 KiB
C++
4015 lines
124 KiB
C++
/**
|
|
* @file aoengine.cpp
|
|
* @brief The core Animation Overrider engine
|
|
*
|
|
* $LicenseInfo:firstyear=2001&license=viewerlgpl$
|
|
* Second Life Viewer Source Code
|
|
* Copyright (C) 2011, Zi Ree @ Second Life
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation;
|
|
* version 2.1 of the License only.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
* $/LicenseInfo$
|
|
*/
|
|
|
|
#include "llviewerprecompiledheaders.h"
|
|
|
|
|
|
#include "roles_constants.h"
|
|
|
|
#include "aoengine.h"
|
|
#include "aoset.h"
|
|
|
|
#include "llagent.h"
|
|
#include "llagentcamera.h"
|
|
#include "llanimationstates.h"
|
|
#include "llassetstorage.h"
|
|
#include "llfilesystem.h"
|
|
#include "llinventoryfunctions.h" // for ROOT_FIRESTORM_FOLDER
|
|
#include "llinventorymodel.h"
|
|
#include "llnotificationsutil.h"
|
|
#include "llstring.h"
|
|
#include "llviewercontrol.h"
|
|
#include "llxorcipher.h"
|
|
|
|
#define ROOT_AO_FOLDER "#AO"
|
|
|
|
static const LLUUID ENCRYPTION_MAGIC_ID("4b552ff5-fd63-408c-8288-cd09429852ba");
|
|
constexpr F32 INVENTORY_POLLING_INTERVAL = 5.0f;
|
|
|
|
AOEngine::AOEngine() :
|
|
LLSingleton<AOEngine>(),
|
|
mCurrentSet(nullptr),
|
|
mDefaultSet(nullptr),
|
|
mEnabled(false),
|
|
mEnabledStands(false),
|
|
mInMouselook(false),
|
|
mUnderWater(false),
|
|
mImportSet(nullptr),
|
|
mAOFolder(LLUUID::null),
|
|
mLastMotion(ANIM_AGENT_STAND),
|
|
mLastOverriddenMotion(ANIM_AGENT_STAND)
|
|
{
|
|
gSavedPerAccountSettings.getControl("UseAO")->getCommitSignal()->connect(boost::bind(&AOEngine::onToggleAOControl, this));
|
|
gSavedPerAccountSettings.getControl("UseAOStands")->getCommitSignal()->connect(boost::bind(&AOEngine::onToggleAOStandsControl, this));
|
|
gSavedPerAccountSettings.getControl("PauseAO")->getCommitSignal()->connect(boost::bind(&AOEngine::onPauseAO, this));
|
|
|
|
mRegionChangeConnection = gAgent.addRegionChangedCallback(boost::bind(&AOEngine::onRegionChange, this));
|
|
}
|
|
|
|
AOEngine::~AOEngine()
|
|
{
|
|
LLSD currentState = LLSD::emptyMap();
|
|
if (mCurrentSet)
|
|
{
|
|
currentState = LLSD().with("CurrentSet", mCurrentSet->getName());
|
|
LLSD currentAnimations;
|
|
for (S32 index = 0; index < AOSet::AOSTATES_MAX; ++index)
|
|
{
|
|
// store the active step's first member asset as the shadow id
|
|
if (auto state = mCurrentSet->getState(index); state && state->mCurrentAnimation < state->mSteps.size() && !state->mSteps[state->mCurrentAnimation].mMembers.empty())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Storing current animation for state " << index << ": Step index " << state->mCurrentAnimation << LL_ENDL;
|
|
LLUUID shadow_id{ state->mSteps[state->mCurrentAnimation].mMembers.front().mAssetUUID };
|
|
LLXORCipher cipher(ENCRYPTION_MAGIC_ID.mData, UUID_BYTES);
|
|
cipher.encrypt(shadow_id.mData, UUID_BYTES);
|
|
currentAnimations.insert(state->mName, shadow_id);
|
|
}
|
|
else
|
|
{
|
|
LL_DEBUGS("AOEngine") << "No state " << index << " or no animations defined for this state" << LL_ENDL;
|
|
}
|
|
}
|
|
currentState.insert("CurrentStateAnimations", currentAnimations);
|
|
}
|
|
LL_DEBUGS("AOEngine") << "Stored AO state: " << currentState << LL_ENDL;
|
|
gSavedPerAccountSettings.setLLSD("FSCurrentAOState", currentState);
|
|
|
|
clear(false);
|
|
|
|
if (mRegionChangeConnection.connected())
|
|
{
|
|
mRegionChangeConnection.disconnect();
|
|
}
|
|
}
|
|
|
|
void AOEngine::init()
|
|
{
|
|
bool do_enable = gSavedPerAccountSettings.getBOOL("UseAO");
|
|
bool do_enable_stands = gSavedPerAccountSettings.getBOOL("UseAOStands");
|
|
if (do_enable)
|
|
{
|
|
// enable_stands() calls enable(), but we need to set the
|
|
// mEnabled variable properly
|
|
mEnabled = true;
|
|
// Enabling the AO always enables stands to start with
|
|
enableStands(true);
|
|
}
|
|
else
|
|
{
|
|
enableStands(do_enable_stands);
|
|
enable(false);
|
|
}
|
|
}
|
|
|
|
// static
|
|
void AOEngine::onLoginComplete()
|
|
{
|
|
AOEngine::instance().init();
|
|
}
|
|
|
|
void AOEngine::onToggleAOControl()
|
|
{
|
|
enable(gSavedPerAccountSettings.getBOOL("UseAO"));
|
|
if (mEnabled)
|
|
{
|
|
// Enabling the AO always enables stands to start with
|
|
gSavedPerAccountSettings.setBOOL("UseAOStands", true);
|
|
}
|
|
}
|
|
|
|
void AOEngine::onToggleAOStandsControl()
|
|
{
|
|
enableStands(gSavedPerAccountSettings.getBOOL("UseAOStands"));
|
|
}
|
|
|
|
void AOEngine::onPauseAO()
|
|
{
|
|
// can't use mEnabled here as that gets switched over by enable()
|
|
if (gSavedPerAccountSettings.getBOOL("UseAO"))
|
|
{
|
|
enable(!gSavedPerAccountSettings.getBOOL("PauseAO"));
|
|
}
|
|
}
|
|
|
|
void AOEngine::clear(bool fromTimer)
|
|
{
|
|
stopActiveTracks();
|
|
stopAlwaysAnimations();
|
|
|
|
std::move(mSets.begin(), mSets.end(), std::back_inserter(mOldSets));
|
|
mSets.clear();
|
|
|
|
mCurrentSet = nullptr;
|
|
|
|
//<ND/> FIRE-3801; We cannot delete any AOSet object if we're called from a timer tick. AOSet is derived from LLEventTimer and destruction will
|
|
// fail in ~LLInstanceTracker when a destructor runs during iteration.
|
|
if (!fromTimer)
|
|
{
|
|
std::for_each(mOldSets.begin(), mOldSets.end(), DeletePointer());
|
|
mOldSets.clear();
|
|
|
|
std::for_each(mOldImportSets.begin(), mOldImportSets.end(), DeletePointer());
|
|
mOldImportSets.clear();
|
|
}
|
|
}
|
|
|
|
void AOEngine::stopAllStandVariants()
|
|
{
|
|
LL_DEBUGS("AOEngine") << "stopping all STAND variants." << LL_ENDL;
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_STAND_1, ANIM_REQUEST_STOP);
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_STAND_2, ANIM_REQUEST_STOP);
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_STAND_3, ANIM_REQUEST_STOP);
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_STAND_4, ANIM_REQUEST_STOP);
|
|
gAgentAvatarp->LLCharacter::stopMotion(ANIM_AGENT_STAND_1);
|
|
gAgentAvatarp->LLCharacter::stopMotion(ANIM_AGENT_STAND_2);
|
|
gAgentAvatarp->LLCharacter::stopMotion(ANIM_AGENT_STAND_3);
|
|
gAgentAvatarp->LLCharacter::stopMotion(ANIM_AGENT_STAND_4);
|
|
}
|
|
|
|
void AOEngine::stopAllSitVariants()
|
|
{
|
|
LL_DEBUGS("AOEngine") << "stopping all SIT variants." << LL_ENDL;
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_SIT_FEMALE, ANIM_REQUEST_STOP);
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_SIT_GENERIC, ANIM_REQUEST_STOP);
|
|
gAgentAvatarp->LLCharacter::stopMotion(ANIM_AGENT_SIT_FEMALE);
|
|
gAgentAvatarp->LLCharacter::stopMotion(ANIM_AGENT_SIT_GENERIC);
|
|
|
|
// scripted seats that use ground_sit as animation need special treatment
|
|
const LLViewerObject* agentRoot = dynamic_cast<LLViewerObject*>(gAgentAvatarp->getRoot());
|
|
if (agentRoot && agentRoot->getID() != gAgentID)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Not stopping ground sit animations while sitting on a prim." << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_SIT_GROUND, ANIM_REQUEST_STOP);
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_SIT_GROUND_CONSTRAINED, ANIM_REQUEST_STOP);
|
|
|
|
gAgentAvatarp->LLCharacter::stopMotion(ANIM_AGENT_SIT_GROUND);
|
|
gAgentAvatarp->LLCharacter::stopMotion(ANIM_AGENT_SIT_GROUND_CONSTRAINED);
|
|
}
|
|
|
|
void AOEngine::stopCurrentAnimations(AOSet::AOState* state, bool skipFirst)
|
|
{
|
|
for (size_t index = skipFirst ? 1 : 0; index < state->mCurrentAnimationIDs.size(); ++index)
|
|
{
|
|
const LLUUID& animation = state->mCurrentAnimationIDs[index];
|
|
if (animation.notNull())
|
|
{
|
|
gAgent.sendAnimationRequest(animation, ANIM_REQUEST_STOP);
|
|
if (isAgentAvatarValid())
|
|
{
|
|
gAgentAvatarp->LLCharacter::stopMotion(animation);
|
|
}
|
|
}
|
|
}
|
|
state->mCurrentAnimationIDs.clear();
|
|
}
|
|
|
|
bool AOEngine::swapStateAnimations(AOSet::AOState* state, const uuid_vec_t& animations)
|
|
{
|
|
uuid_vec_t oldAnimations = state->mCurrentAnimationIDs;
|
|
if (animations == oldAnimations)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
state->mCurrentAnimationIDs = animations;
|
|
|
|
for (const LLUUID& animation : animations)
|
|
{
|
|
if (std::find(oldAnimations.begin(), oldAnimations.end(), animation) == oldAnimations.end())
|
|
{
|
|
gAgent.sendAnimationRequest(animation, ANIM_REQUEST_START);
|
|
}
|
|
}
|
|
|
|
for (const LLUUID& oldAnimation : oldAnimations)
|
|
{
|
|
if (std::find(animations.begin(), animations.end(), oldAnimation) == animations.end())
|
|
{
|
|
gAgent.sendAnimationRequest(oldAnimation, ANIM_REQUEST_STOP);
|
|
if (isAgentAvatarValid())
|
|
{
|
|
gAgentAvatarp->LLCharacter::stopMotion(oldAnimation);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void AOEngine::startStateTracks(AOSet::AOState* state)
|
|
{
|
|
stopActiveTracks();
|
|
|
|
if (!state || !mEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (size_t index = 0; index < state->mTracks.size(); ++index)
|
|
{
|
|
AOSet::AOTrack& track = state->mTracks[index];
|
|
if (track.mAnimations.empty())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
track.mCurrentAnimationID = mCurrentSet ? mCurrentSet->getAnimationForTrack(track) : LLUUID::null;
|
|
if (track.mCurrentAnimationID.isNull())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
gAgent.sendAnimationRequest(track.mCurrentAnimationID, ANIM_REQUEST_START);
|
|
|
|
// tracks with only one animation and no cycling don't need a timer at all
|
|
if (track.mCycle && track.mCycleTime > 0.0f && track.mAnimations.size() > 1)
|
|
{
|
|
mActiveTrackTimers.emplace_back(std::make_unique<AOTrackTimer>(state->mNum, static_cast<S32>(index), track.mCycleTime));
|
|
}
|
|
}
|
|
|
|
mActiveTrackState = state;
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
}
|
|
|
|
void AOEngine::stopActiveTracks()
|
|
{
|
|
mActiveTrackTimers.clear();
|
|
|
|
if (!mActiveTrackState)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (AOSet::AOTrack& track : mActiveTrackState->mTracks)
|
|
{
|
|
if (track.mCurrentAnimationID.notNull())
|
|
{
|
|
gAgent.sendAnimationRequest(track.mCurrentAnimationID, ANIM_REQUEST_STOP);
|
|
if (isAgentAvatarValid())
|
|
{
|
|
gAgentAvatarp->LLCharacter::stopMotion(track.mCurrentAnimationID);
|
|
}
|
|
track.mCurrentAnimationID.setNull();
|
|
}
|
|
}
|
|
|
|
mActiveTrackState = nullptr;
|
|
}
|
|
|
|
void AOEngine::startAlwaysAnimations()
|
|
{
|
|
AOSet::AOState* state = (mEnabled && mCurrentSet) ? mCurrentSet->getState(AOSet::AlwaysPlay) : nullptr;
|
|
|
|
if (state && state == mActiveAlwaysState)
|
|
{
|
|
return;
|
|
}
|
|
|
|
stopAlwaysAnimations();
|
|
|
|
if (!state || (state->mSteps.empty() && state->mTracks.empty()))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!state->mSteps.empty())
|
|
{
|
|
uuid_vec_t animations;
|
|
mCurrentSet->getAnimationsForState(state, animations);
|
|
state->mCurrentAnimationIDs = animations;
|
|
for (const LLUUID& animation : animations)
|
|
{
|
|
gAgent.sendAnimationRequest(animation, ANIM_REQUEST_START);
|
|
}
|
|
}
|
|
|
|
for (AOSet::AOTrack& track : state->mTracks)
|
|
{
|
|
if (track.mAnimations.empty())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
track.mCurrentAnimationID = mCurrentSet->getAnimationForTrack(track);
|
|
if (track.mCurrentAnimationID.notNull())
|
|
{
|
|
gAgent.sendAnimationRequest(track.mCurrentAnimationID, ANIM_REQUEST_START);
|
|
}
|
|
}
|
|
|
|
mActiveAlwaysState = state;
|
|
refreshAlwaysTimers();
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
}
|
|
|
|
void AOEngine::stopAlwaysAnimations()
|
|
{
|
|
mAlwaysTimers.clear();
|
|
|
|
if (!mActiveAlwaysState)
|
|
{
|
|
return;
|
|
}
|
|
|
|
stopCurrentAnimations(mActiveAlwaysState);
|
|
|
|
for (AOSet::AOTrack& track : mActiveAlwaysState->mTracks)
|
|
{
|
|
if (track.mCurrentAnimationID.notNull())
|
|
{
|
|
gAgent.sendAnimationRequest(track.mCurrentAnimationID, ANIM_REQUEST_STOP);
|
|
if (isAgentAvatarValid())
|
|
{
|
|
gAgentAvatarp->LLCharacter::stopMotion(track.mCurrentAnimationID);
|
|
}
|
|
track.mCurrentAnimationID.setNull();
|
|
}
|
|
}
|
|
|
|
mActiveAlwaysState = nullptr;
|
|
}
|
|
|
|
void AOEngine::refreshAlwaysTimers()
|
|
{
|
|
mAlwaysTimers.clear();
|
|
|
|
AOSet::AOState* state = mActiveAlwaysState;
|
|
if (!state)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (state->mCycle && state->mCycleTime > 0.0f && state->mSteps.size() > 1)
|
|
{
|
|
mAlwaysTimers.emplace_back(std::make_unique<AOTrackTimer>(state->mNum, AO_TRACK_INDEX_STEPS, state->mCycleTime));
|
|
}
|
|
|
|
for (size_t index = 0; index < state->mTracks.size(); ++index)
|
|
{
|
|
const AOSet::AOTrack& track = state->mTracks[index];
|
|
if (track.mCycle && track.mCycleTime > 0.0f && track.mAnimations.size() > 1)
|
|
{
|
|
mAlwaysTimers.emplace_back(std::make_unique<AOTrackTimer>(state->mNum, static_cast<S32>(index), track.mCycleTime));
|
|
}
|
|
}
|
|
}
|
|
|
|
void AOEngine::cycleAlwaysStep()
|
|
{
|
|
AOSet::AOState* state = mActiveAlwaysState;
|
|
if (!mEnabled || !mCurrentSet || !state || state->mSteps.empty())
|
|
{
|
|
return;
|
|
}
|
|
|
|
uuid_vec_t animations;
|
|
mCurrentSet->getAnimationsForState(state, animations);
|
|
|
|
if (swapStateAnimations(state, animations))
|
|
{
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
}
|
|
}
|
|
|
|
void AOEngine::reassertAlwaysAnimations(bool onlyIfMissing)
|
|
{
|
|
if (!mEnabled || !mActiveAlwaysState || (onlyIfMissing && !isAgentAvatarValid()))
|
|
{
|
|
return;
|
|
}
|
|
|
|
const auto needsRestart = [&](const LLUUID& id) -> bool
|
|
{
|
|
if (id.isNull())
|
|
{
|
|
return false;
|
|
}
|
|
if (!onlyIfMissing)
|
|
{
|
|
return true;
|
|
}
|
|
return gAgentAvatarp->mSignaledAnimations.find(id) == gAgentAvatarp->mSignaledAnimations.end();
|
|
};
|
|
|
|
for (const LLUUID& animation : mActiveAlwaysState->mCurrentAnimationIDs)
|
|
{
|
|
if (needsRestart(animation))
|
|
{
|
|
gAgent.sendAnimationRequest(animation, ANIM_REQUEST_START);
|
|
}
|
|
}
|
|
|
|
for (const AOSet::AOTrack& track : mActiveAlwaysState->mTracks)
|
|
{
|
|
if (needsRestart(track.mCurrentAnimationID))
|
|
{
|
|
gAgent.sendAnimationRequest(track.mCurrentAnimationID, ANIM_REQUEST_START);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AOEngine::trackTimeout(S32 stateNum, S32 trackIndex)
|
|
{
|
|
if (!mEnabled || !mCurrentSet)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AOSet::AOState* state = nullptr;
|
|
if (mActiveTrackState && mActiveTrackState->mNum == stateNum)
|
|
{
|
|
state = mActiveTrackState;
|
|
}
|
|
else if (mActiveAlwaysState && mActiveAlwaysState->mNum == stateNum)
|
|
{
|
|
state = mActiveAlwaysState;
|
|
}
|
|
|
|
if (!state)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (trackIndex == AO_TRACK_INDEX_STEPS)
|
|
{
|
|
if (state == mActiveAlwaysState)
|
|
{
|
|
cycleAlwaysStep();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (trackIndex < 0 || trackIndex >= (S32)state->mTracks.size())
|
|
{
|
|
return;
|
|
}
|
|
|
|
AOSet::AOTrack& track = state->mTracks[trackIndex];
|
|
|
|
LLUUID oldAnimation = track.mCurrentAnimationID;
|
|
LLUUID animation = mCurrentSet->getAnimationForTrack(track);
|
|
|
|
if (animation == oldAnimation)
|
|
{
|
|
return;
|
|
}
|
|
|
|
track.mCurrentAnimationID = animation;
|
|
if (animation.notNull())
|
|
{
|
|
gAgent.sendAnimationRequest(animation, ANIM_REQUEST_START);
|
|
}
|
|
|
|
if (oldAnimation.notNull())
|
|
{
|
|
gAgent.sendAnimationRequest(oldAnimation, ANIM_REQUEST_STOP);
|
|
if (isAgentAvatarValid())
|
|
{
|
|
gAgentAvatarp->LLCharacter::stopMotion(oldAnimation);
|
|
}
|
|
}
|
|
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
}
|
|
|
|
void AOEngine::setLastMotion(const LLUUID& motion)
|
|
{
|
|
if (motion != ANIM_AGENT_TYPE)
|
|
{
|
|
mLastMotion = motion;
|
|
}
|
|
}
|
|
|
|
void AOEngine::setLastOverriddenMotion(const LLUUID& motion)
|
|
{
|
|
if (motion != ANIM_AGENT_TYPE)
|
|
{
|
|
mLastOverriddenMotion = motion;
|
|
}
|
|
}
|
|
|
|
bool AOEngine::foreignAnimations() const
|
|
{
|
|
// checking foreign animations only makes sense when smart sit is enabled
|
|
if (!mCurrentSet->getSmart())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// get the seat the avatar is sitting on
|
|
const LLViewerObject* agentRoot = dynamic_cast<LLViewerObject*>(gAgentAvatarp->getRoot());
|
|
if (!agentRoot)
|
|
{
|
|
// this should not happen, ever
|
|
return false;
|
|
}
|
|
|
|
const LLUUID& seat = agentRoot->getID();
|
|
if (seat == gAgentID)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Not checking for foreign animation when not sitting." << LL_ENDL;
|
|
return false;
|
|
}
|
|
|
|
LL_DEBUGS("AOEngine") << "Checking for foreign animation on seat " << seat << LL_ENDL;
|
|
|
|
for (const auto& [source_id, animation_id] : gAgentAvatarp->mAnimationSources)
|
|
{
|
|
// skip animations run by the avatar itself
|
|
if (source_id != gAgentID)
|
|
{
|
|
// find the source object where the animation came from
|
|
LLViewerObject* source = gObjectList.findObject(source_id);
|
|
|
|
// proceed if it's not an attachment
|
|
if (source && !source->isAttachment())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Source " << source_id << " is running animation " << animation_id << LL_ENDL;
|
|
|
|
// get the source's root prim
|
|
LLViewerObject* sourceRoot = dynamic_cast<LLViewerObject*>(source->getRoot());
|
|
|
|
// if the root prim is the same as the animation source, report back as true
|
|
if (sourceRoot && sourceRoot->getID() == seat)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "foreign animation " << animation_id << " found on seat." << LL_ENDL;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// map motion to underwater state, return nullptr if not applicable
|
|
AOSet::AOState* AOEngine::mapSwimming(const LLUUID& motion) const
|
|
{
|
|
S32 stateNum;
|
|
|
|
if (motion == ANIM_AGENT_HOVER)
|
|
{
|
|
stateNum = AOSet::Floating;
|
|
}
|
|
else if (motion == ANIM_AGENT_FLY)
|
|
{
|
|
stateNum = AOSet::SwimmingForward;
|
|
}
|
|
else if (motion == ANIM_AGENT_HOVER_UP)
|
|
{
|
|
stateNum = AOSet::SwimmingUp;
|
|
}
|
|
else if (motion == ANIM_AGENT_HOVER_DOWN)
|
|
{
|
|
stateNum = AOSet::SwimmingDown;
|
|
}
|
|
else
|
|
{
|
|
// motion not applicable for underwater mapping
|
|
return nullptr;
|
|
}
|
|
|
|
return mCurrentSet->getState(stateNum);
|
|
}
|
|
|
|
// switch between swimming and flying on transition in and out of Linden region water
|
|
void AOEngine::checkBelowWater(bool check_underwater)
|
|
{
|
|
// there was no transition, do nothing
|
|
if (mUnderWater == check_underwater)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// only applies to motions that actually change underwater and have animations inside
|
|
AOSet::AOState* mapped = mapSwimming(mLastMotion);
|
|
if (!mapped || mapped->mSteps.empty())
|
|
{
|
|
// set underwater status but do nothing else
|
|
mUnderWater = check_underwater;
|
|
return;
|
|
}
|
|
|
|
// find animation id to stop when transitioning
|
|
LLUUID id = override(mLastMotion, false);
|
|
if (id.isNull())
|
|
{
|
|
// no animation in overrider for this state, use Linden Lab motion
|
|
id = mLastMotion;
|
|
}
|
|
|
|
// stop currently running animation
|
|
gAgent.sendAnimationRequest(id, ANIM_REQUEST_STOP);
|
|
|
|
if (!mUnderWater)
|
|
{
|
|
// remember which animation we stopped while going under water, to catch the stop
|
|
// request later in the overrider - this prevents the overrider from triggering itself
|
|
// after the region comes back with the stop request for Linden Lab motion ids
|
|
mTransitionId = id;
|
|
}
|
|
|
|
// set requested underwater status for overrider
|
|
mUnderWater = check_underwater;
|
|
|
|
// find animation id to start when transitioning
|
|
id = override(mLastMotion, true);
|
|
if (id.isNull())
|
|
{
|
|
// no animation in overrider for this state, use Linden Lab motion
|
|
id = mLastMotion;
|
|
}
|
|
|
|
// start new animation
|
|
gAgent.sendAnimationRequest(id, ANIM_REQUEST_START);
|
|
}
|
|
|
|
// find the correct animation state for the requested motion, mapping flying to
|
|
// swimming where necessary
|
|
AOSet::AOState* AOEngine::getStateForMotion(const LLUUID& motion) const
|
|
{
|
|
// get default state for this motion
|
|
AOSet::AOState* defaultState = mCurrentSet->getStateByRemapID(motion);
|
|
if (!mUnderWater)
|
|
{
|
|
return defaultState;
|
|
}
|
|
|
|
// get state for underwater motion
|
|
AOSet::AOState* mapped = mapSwimming(motion);
|
|
if (!mapped)
|
|
{
|
|
// not applicable for underwater motion, so use default state
|
|
return defaultState;
|
|
}
|
|
|
|
// check if the underwater state has any animations to play
|
|
if (mapped->mSteps.empty())
|
|
{
|
|
// no animations in underwater state, return default
|
|
return defaultState;
|
|
}
|
|
return mapped;
|
|
}
|
|
|
|
void AOEngine::enableStands(bool enable_stands)
|
|
{
|
|
mEnabledStands = enable_stands;
|
|
// let the main enable routine decide if we need to change animations
|
|
// but don't actually change the state of the enabled flag
|
|
enable(mEnabled);
|
|
}
|
|
|
|
void AOEngine::enable(bool enable)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "using " << gAnimLibrary.animationName(mLastMotion) << " enable " << enable << LL_ENDL;
|
|
mEnabled = enable;
|
|
|
|
if (!mCurrentSet)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "enable(" << enable << ") without animation set loaded." << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
AOSet::AOState* state = mCurrentSet->getStateByRemapID(mLastMotion);
|
|
if (mEnabled)
|
|
{
|
|
startAlwaysAnimations();
|
|
if (state && !state->mSteps.empty())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Enabling animation state " << state->mName << LL_ENDL;
|
|
|
|
// do not stop underlying sit animations when re-enabling the AO
|
|
if (mLastOverriddenMotion != ANIM_AGENT_SIT_GROUND &&
|
|
mLastOverriddenMotion != ANIM_AGENT_SIT_GROUND_CONSTRAINED &&
|
|
mLastOverriddenMotion != ANIM_AGENT_SIT)
|
|
{
|
|
gAgent.sendAnimationRequest(mLastOverriddenMotion, ANIM_REQUEST_STOP);
|
|
}
|
|
|
|
LLUUID animation = override(mLastMotion, true);
|
|
if (animation.isNull())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (mLastMotion == ANIM_AGENT_STAND)
|
|
{
|
|
if (!mEnabledStands)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Last motion was a STAND, but disabled for stands, ignoring." << LL_ENDL;
|
|
return;
|
|
}
|
|
stopAllStandVariants();
|
|
}
|
|
else if (mLastMotion == ANIM_AGENT_WALK)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Last motion was a WALK, stopping all variants." << LL_ENDL;
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_WALK_NEW, ANIM_REQUEST_STOP);
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_FEMALE_WALK, ANIM_REQUEST_STOP);
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_FEMALE_WALK_NEW, ANIM_REQUEST_STOP);
|
|
gAgentAvatarp->LLCharacter::stopMotion(ANIM_AGENT_WALK_NEW);
|
|
gAgentAvatarp->LLCharacter::stopMotion(ANIM_AGENT_FEMALE_WALK);
|
|
gAgentAvatarp->LLCharacter::stopMotion(ANIM_AGENT_FEMALE_WALK_NEW);
|
|
}
|
|
else if (mLastMotion == ANIM_AGENT_RUN)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Last motion was a RUN, stopping all variants." << LL_ENDL;
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_RUN_NEW, ANIM_REQUEST_STOP);
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_FEMALE_RUN_NEW, ANIM_REQUEST_STOP);
|
|
gAgentAvatarp->LLCharacter::stopMotion(ANIM_AGENT_RUN_NEW);
|
|
gAgentAvatarp->LLCharacter::stopMotion(ANIM_AGENT_FEMALE_RUN_NEW);
|
|
}
|
|
else if (mLastMotion == ANIM_AGENT_SIT)
|
|
{
|
|
stopAllSitVariants();
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_SIT_GENERIC, ANIM_REQUEST_START);
|
|
}
|
|
else
|
|
{
|
|
LL_WARNS("AOEngine") << "Unhandled last motion id " << gAnimLibrary.animationName(mLastMotion) << LL_ENDL;
|
|
}
|
|
|
|
gAgent.sendAnimationRequest(animation, ANIM_REQUEST_START);
|
|
if (state->mCurrentAnimation < state->mSteps.size())
|
|
{
|
|
mAnimationChangedSignal(state->mSteps[state->mCurrentAnimation].mInventoryUUID);
|
|
}
|
|
|
|
// remember to ignore this motion once in the overrider so stopping the Linden motion
|
|
// will not trigger a stop of the override animation
|
|
mIgnoreMotionStopOnce = mLastMotion;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (mLastOverriddenMotion == ANIM_AGENT_SIT)
|
|
{
|
|
// remove sit cycle cover up
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_SIT_GENERIC, ANIM_REQUEST_STOP);
|
|
}
|
|
|
|
stopActiveTracks();
|
|
stopAlwaysAnimations();
|
|
|
|
// stop all overriders, catch leftovers
|
|
for (S32 index = 0; index < AOSet::AOSTATES_MAX; ++index)
|
|
{
|
|
if (auto state = mCurrentSet->getState(index))
|
|
{
|
|
if (!state->mCurrentAnimationIDs.empty())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Stopping leftover animation(s) from state " << state->mName << LL_ENDL;
|
|
stopCurrentAnimations(state);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LL_DEBUGS("AOEngine") << "state "<< index <<" returned NULL." << LL_ENDL;
|
|
}
|
|
}
|
|
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
|
|
// restore Linden animation if applicable
|
|
if (mLastOverriddenMotion != ANIM_AGENT_SIT || !foreignAnimations())
|
|
{
|
|
gAgent.sendAnimationRequest(mLastMotion, ANIM_REQUEST_START);
|
|
}
|
|
|
|
mCurrentSet->stopTimer();
|
|
}
|
|
}
|
|
|
|
void AOEngine::setStateCycleTimer(const AOSet::AOState* state)
|
|
{
|
|
F32 timeout = state->mCycleTime;
|
|
LL_DEBUGS("AOEngine") << "Setting cycle timeout for state " << state->mName << " of " << timeout << LL_ENDL;
|
|
if (timeout > 0.0f)
|
|
{
|
|
mCurrentSet->startTimer(timeout);
|
|
}
|
|
}
|
|
|
|
LLUUID AOEngine::override(const LLUUID& motion, bool start)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "override(" << gAnimLibrary.animationName(motion) << "," << start << ")" << LL_ENDL;
|
|
|
|
if (!mEnabled)
|
|
{
|
|
if (start && mCurrentSet)
|
|
{
|
|
const AOSet::AOState* state = mCurrentSet->getStateByRemapID(motion);
|
|
if (state)
|
|
{
|
|
setLastMotion(motion);
|
|
LL_DEBUGS("AOEngine") << "(disabled AO) setting last motion id to " << gAnimLibrary.animationName(mLastMotion) << LL_ENDL;
|
|
if (!state->mSteps.empty())
|
|
{
|
|
setLastOverriddenMotion(motion);
|
|
LL_DEBUGS("AOEngine") << "(disabled AO) setting last overridden motion id to " << gAnimLibrary.animationName(mLastOverriddenMotion) << LL_ENDL;
|
|
}
|
|
}
|
|
}
|
|
return LLUUID::null;
|
|
}
|
|
|
|
if (mSets.empty())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "No sets loaded. Skipping overrider." << LL_ENDL;
|
|
return LLUUID::null;
|
|
}
|
|
|
|
if (!mCurrentSet)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "No current AO set chosen. Skipping overrider." << LL_ENDL;
|
|
return LLUUID::null;
|
|
}
|
|
|
|
// ignore stopping this motion once so we can stop the Linden animation
|
|
// without killing our overrider when logging in or re-enabling
|
|
if (!start && motion == mIgnoreMotionStopOnce)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Not stop-overriding motion " << gAnimLibrary.animationName(motion)
|
|
<< " within same state." << LL_ENDL;
|
|
|
|
mIgnoreMotionStopOnce = LLUUID::null;
|
|
|
|
// when stopping a sit motion make sure to stop the cycle point cover-up animation
|
|
if (motion == ANIM_AGENT_SIT)
|
|
{
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_SIT_GENERIC, ANIM_REQUEST_STOP);
|
|
}
|
|
|
|
return LLUUID::null;
|
|
}
|
|
|
|
// map the requested motion to an animation state, taking underwater
|
|
// swimming into account where applicable
|
|
AOSet::AOState* state = getStateForMotion(motion);
|
|
if (!state)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "No current AO state for motion " << gAnimLibrary.animationName(motion) << LL_ENDL;
|
|
// This part of the code was added to capture an edge case where animations got stuck
|
|
// However, it seems it isn't needed anymore and breaks other, more important cases.
|
|
// So we disable this code for now, unless bad things happen and the stuck animations
|
|
// come back again. -Zi
|
|
// if (!gAnimLibrary.animStateToString(motion) && !start)
|
|
// {
|
|
// state = mCurrentSet->getStateByRemapID(mLastOverriddenMotion);
|
|
// if (state && state->mCurrentAnimationID == motion)
|
|
// {
|
|
// LL_DEBUGS("AOEngine") << "Stop requested for current overridden animation UUID " << motion << " - Skipping." << LL_ENDL;
|
|
// }
|
|
// else
|
|
// {
|
|
// LL_DEBUGS("AOEngine") << "Stop requested for unknown UUID " << motion << " - Stopping it just in case." << LL_ENDL;
|
|
// gAgent.sendAnimationRequest(motion, ANIM_REQUEST_STOP);
|
|
// gAgentAvatarp->LLCharacter::stopMotion(motion);
|
|
// }
|
|
// }
|
|
return LLUUID::null;
|
|
}
|
|
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
|
|
// clean up stray animations as an additional safety measure
|
|
// unless current motion is ANIM_AGENT_TYPE, which is a special
|
|
// case, as it plays at the same time as other motions
|
|
if (motion != ANIM_AGENT_TYPE)
|
|
{
|
|
constexpr S32 cleanupStates[] =
|
|
{
|
|
AOSet::Standing,
|
|
AOSet::Walking,
|
|
AOSet::Running,
|
|
AOSet::Sitting,
|
|
AOSet::SittingOnGround,
|
|
AOSet::Crouching,
|
|
AOSet::CrouchWalking,
|
|
AOSet::Falling,
|
|
AOSet::FlyingDown,
|
|
AOSet::FlyingUp,
|
|
AOSet::Flying,
|
|
AOSet::FlyingSlow,
|
|
AOSet::Hovering,
|
|
AOSet::Jumping,
|
|
AOSet::TurningRight,
|
|
AOSet::TurningLeft,
|
|
AOSet::Floating,
|
|
AOSet::SwimmingForward,
|
|
AOSet::SwimmingUp,
|
|
AOSet::SwimmingDown,
|
|
AOSet::AOSTATES_MAX // end marker, guaranteed to be different from any other entry
|
|
};
|
|
|
|
S32 index = 0;
|
|
S32 stateNum;
|
|
|
|
// loop through the list of states
|
|
while ((stateNum = cleanupStates[index]) != AOSet::AOSTATES_MAX)
|
|
{
|
|
// check if the next state is the one we are currently animating and skip that
|
|
if (AOSet::AOState* stateToCheck = mCurrentSet->getState(stateNum); stateToCheck != state)
|
|
{
|
|
// check if there are animations left over for that state
|
|
if (!stateToCheck->mCurrentAnimationIDs.empty())
|
|
{
|
|
LL_WARNS() << "cleaning up animation(s) in state " << stateToCheck->mName << LL_ENDL;
|
|
|
|
// stop the leftover animations locally and in the region for everyone
|
|
stopCurrentAnimations(stateToCheck);
|
|
}
|
|
}
|
|
index++;
|
|
}
|
|
}
|
|
|
|
LLUUID animation;
|
|
|
|
mCurrentSet->stopTimer();
|
|
if (start)
|
|
{
|
|
if (motion != ANIM_AGENT_TYPE)
|
|
{
|
|
stopActiveTracks();
|
|
}
|
|
|
|
setLastMotion(motion);
|
|
LL_DEBUGS("AOEngine") << "(enabled AO) setting last motion id to " << gAnimLibrary.animationName(mLastMotion) << LL_ENDL;
|
|
|
|
// Disable start stands in Mouselook
|
|
if (mCurrentSet->getMouselookStandDisable() &&
|
|
motion == ANIM_AGENT_STAND &&
|
|
mInMouselook)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "(enabled AO, mouselook stand stopped) setting last motion id to " << gAnimLibrary.animationName(mLastMotion) << LL_ENDL;
|
|
return LLUUID::null;
|
|
}
|
|
|
|
// Don't override start and turning stands if stand override is disabled
|
|
if (!mEnabledStands &&
|
|
(motion == ANIM_AGENT_STAND || motion == ANIM_AGENT_TURNRIGHT || motion == ANIM_AGENT_TURNLEFT))
|
|
{
|
|
LL_DEBUGS("AOEngine") << "(enabled AO, stands disabled) setting last motion id to " << gAnimLibrary.animationName(mLastMotion) << LL_ENDL;
|
|
return LLUUID::null;
|
|
}
|
|
|
|
// Do not start override sits if not selected
|
|
if (!mCurrentSet->getSitOverride() && motion == ANIM_AGENT_SIT)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "(enabled AO, sit override stopped) setting last motion id to " << gAnimLibrary.animationName(mLastMotion) << LL_ENDL;
|
|
return LLUUID::null;
|
|
}
|
|
|
|
// scripted seats that use ground_sit as animation need special treatment
|
|
if (motion == ANIM_AGENT_SIT_GROUND || motion == ANIM_AGENT_SIT_GROUND_CONSTRAINED)
|
|
{
|
|
const LLViewerObject* agentRoot = dynamic_cast<LLViewerObject*>(gAgentAvatarp->getRoot());
|
|
if (agentRoot && agentRoot->getID() != gAgentID)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Ground sit animation playing but sitting on a prim - disabling overrider." << LL_ENDL;
|
|
return LLUUID::null;
|
|
}
|
|
}
|
|
|
|
if (!state->mSteps.empty())
|
|
{
|
|
setLastOverriddenMotion(motion);
|
|
LL_DEBUGS("AOEngine") << "(enabled AO) setting last overridden motion id to " << gAnimLibrary.animationName(mLastOverriddenMotion) << LL_ENDL;
|
|
}
|
|
|
|
// do not remember typing as set-wide motion
|
|
if (motion != ANIM_AGENT_TYPE)
|
|
{
|
|
mCurrentSet->setMotion(motion);
|
|
}
|
|
|
|
uuid_vec_t animations;
|
|
mCurrentSet->getAnimationsForState(state, animations);
|
|
|
|
if (!state->mCurrentAnimationIDs.empty())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Previous animation(s) for state "
|
|
<< gAnimLibrary.animationName(motion)
|
|
<< " were not stopped, but we were asked to start a new one. Killing old animations." << LL_ENDL;
|
|
stopCurrentAnimations(state);
|
|
}
|
|
|
|
state->mCurrentAnimationIDs = animations;
|
|
animation = animations.empty() ? LLUUID::null : animations.front();
|
|
LL_DEBUGS("AOEngine") << "overriding " << gAnimLibrary.animationName(motion)
|
|
<< " with " << animation
|
|
<< " (" << animations.size() << " animation(s))"
|
|
<< " in state " << state->mName
|
|
<< " of set " << mCurrentSet->getName()
|
|
<< " (" << mCurrentSet << ")" << LL_ENDL;
|
|
|
|
if (animation.notNull() && state->mCurrentAnimation < state->mSteps.size())
|
|
{
|
|
mAnimationChangedSignal(state->mSteps[state->mCurrentAnimation].mInventoryUUID);
|
|
}
|
|
|
|
setStateCycleTimer(state);
|
|
|
|
if (motion == ANIM_AGENT_SIT)
|
|
{
|
|
// Use ANIM_AGENT_SIT_GENERIC, so we don't create an overrider loop with ANIM_AGENT_SIT
|
|
// while still having a base sitting pose to cover up cycle points
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_SIT_GENERIC, ANIM_REQUEST_START);
|
|
if (mCurrentSet->getSmart())
|
|
{
|
|
mSitCancelTimer.oneShot();
|
|
}
|
|
}
|
|
// special treatment for "transient animations" because the viewer needs the Linden animation to know the agent's state
|
|
else if (motion == ANIM_AGENT_SIT_GROUND ||
|
|
motion == ANIM_AGENT_SIT_GROUND_CONSTRAINED ||
|
|
motion == ANIM_AGENT_PRE_JUMP ||
|
|
motion == ANIM_AGENT_STANDUP ||
|
|
motion == ANIM_AGENT_LAND ||
|
|
motion == ANIM_AGENT_MEDIUM_LAND)
|
|
{
|
|
for (const LLUUID& animationId : animations)
|
|
{
|
|
gAgent.sendAnimationRequest(animationId, ANIM_REQUEST_START);
|
|
}
|
|
return LLUUID::null;
|
|
}
|
|
|
|
for (size_t index = 1; index < animations.size(); ++index)
|
|
{
|
|
gAgent.sendAnimationRequest(animations[index], ANIM_REQUEST_START);
|
|
}
|
|
if (motion != ANIM_AGENT_TYPE)
|
|
{
|
|
startStateTracks(state);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// check for previously remembered transition motion from/to underwater movement and don't
|
|
// allow the overrider to stop it, or it will cancel the transition to underwater motion
|
|
// immediately after starting it
|
|
if (motion == mTransitionId)
|
|
{
|
|
// clear transition motion id and return a null UUID to allow the stock Linden animation
|
|
// system to take over
|
|
mTransitionId.setNull();
|
|
return LLUUID::null;
|
|
}
|
|
|
|
// clear transition motion id here as well, to make sure there is no stray id left behind
|
|
mTransitionId.setNull();
|
|
|
|
animation = state->mCurrentAnimationIDs.empty() ? LLUUID::null : state->mCurrentAnimationIDs.front();
|
|
|
|
// for typing animaiton, just return the stored animation, reset the state timer, and don't memorize anything else
|
|
if (motion == ANIM_AGENT_TYPE)
|
|
{
|
|
stopCurrentAnimations(state, true);
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
AOSet::AOState* previousState = mCurrentSet->getStateByRemapID(mLastMotion);
|
|
if (previousState)
|
|
{
|
|
setStateCycleTimer(previousState);
|
|
}
|
|
return animation;
|
|
}
|
|
|
|
// stop the underlying Linden Lab motion, in case it's still running.
|
|
// frequently happens with sits, so we keep it only for those currently.
|
|
if (motion == ANIM_AGENT_SIT)
|
|
{
|
|
stopAllSitVariants();
|
|
}
|
|
|
|
if (motion != mCurrentSet->getMotion())
|
|
{
|
|
LL_WARNS("AOEngine") << "trying to stop-override motion " << gAnimLibrary.animationName(motion)
|
|
<< " but the current set has motion " << gAnimLibrary.animationName(mCurrentSet->getMotion()) << LL_ENDL;
|
|
stopCurrentAnimations(state, true);
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
return animation;
|
|
}
|
|
|
|
mCurrentSet->setMotion(LLUUID::null);
|
|
stopActiveTracks();
|
|
|
|
// again, special treatment for "transient" animations to make sure our own animation gets stopped properly
|
|
if (motion == ANIM_AGENT_SIT_GROUND ||
|
|
motion == ANIM_AGENT_SIT_GROUND_CONSTRAINED ||
|
|
motion == ANIM_AGENT_PRE_JUMP ||
|
|
motion == ANIM_AGENT_STANDUP ||
|
|
motion == ANIM_AGENT_LAND ||
|
|
motion == ANIM_AGENT_MEDIUM_LAND)
|
|
{
|
|
stopCurrentAnimations(state);
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
setStateCycleTimer(state);
|
|
return LLUUID::null;
|
|
}
|
|
|
|
stopCurrentAnimations(state, true);
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
}
|
|
|
|
return animation;
|
|
}
|
|
|
|
void AOEngine::checkSitCancel()
|
|
{
|
|
if (!foreignAnimations())
|
|
return;
|
|
|
|
if (AOSet::AOState* aoState = mCurrentSet->getStateByRemapID(ANIM_AGENT_SIT))
|
|
{
|
|
if (!aoState->mCurrentAnimationIDs.empty())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Stopping sit animation due to foreign animations running" << LL_ENDL;
|
|
stopCurrentAnimations(aoState);
|
|
// remove cycle point cover-up
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_SIT_GENERIC, ANIM_REQUEST_STOP);
|
|
mSitCancelTimer.stop();
|
|
// stop cycle tiemr
|
|
mCurrentSet->stopTimer();
|
|
// sitting tracks make no sense when the sit override backs off
|
|
stopActiveTracks();
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AOEngine::cycleTimeout(const AOSet* set)
|
|
{
|
|
if (!mEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (set != mCurrentSet)
|
|
{
|
|
LL_WARNS("AOEngine") << "cycleTimeout for set " << set->getName() << " but current set is " << mCurrentSet->getName() << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
cycle(CycleAny);
|
|
}
|
|
|
|
void AOEngine::cycle(eCycleMode cycleMode, bool resetTimer)
|
|
{
|
|
if (!mEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!mCurrentSet)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "cycle without set." << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
// do not cycle if we're sitting and sit-override is off
|
|
if (mLastMotion == ANIM_AGENT_SIT && !mCurrentSet->getSitOverride())
|
|
{
|
|
return;
|
|
}
|
|
// do not cycle if we're standing and mouselook stand override is disabled while being in mouselook
|
|
else if (mLastMotion == ANIM_AGENT_STAND && mCurrentSet->getMouselookStandDisable() && mInMouselook)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AOSet::AOState* state = mCurrentSet->getStateByRemapID(mLastMotion);
|
|
if (!state)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "cycle without state." << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
if (state->mSteps.empty())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "cycle without animations in state." << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
// make sure we disable cycling only for timed cycle, so manual cycling still works, even with cycling switched off
|
|
if (!state->mCycle && cycleMode == CycleAny)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "cycle timeout, but state is set to not cycling." << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
uuid_vec_t oldAnimations = state->mCurrentAnimationIDs;
|
|
uuid_vec_t animations;
|
|
|
|
if (cycleMode == CycleAny)
|
|
{
|
|
mCurrentSet->getAnimationsForState(state, animations);
|
|
}
|
|
else
|
|
{
|
|
if (cycleMode == CyclePrevious)
|
|
{
|
|
if (state->mCurrentAnimation == 0)
|
|
{
|
|
state->mCurrentAnimation = static_cast<U32>(state->mSteps.size()) - 1;
|
|
}
|
|
else
|
|
{
|
|
state->mCurrentAnimation--;
|
|
}
|
|
}
|
|
else if (cycleMode == CycleNext)
|
|
{
|
|
state->mCurrentAnimation++;
|
|
if (state->mCurrentAnimation >= state->mSteps.size())
|
|
{
|
|
state->mCurrentAnimation = 0;
|
|
}
|
|
}
|
|
|
|
for (AOSet::AOAnimation& anim : state->mSteps[state->mCurrentAnimation].mMembers)
|
|
{
|
|
if (LLUUID assetId = AOSet::resolveAssetUUID(anim); assetId.notNull())
|
|
{
|
|
animations.emplace_back(assetId);
|
|
}
|
|
}
|
|
}
|
|
|
|
// don't do anything if the animation didn't change
|
|
if (animations == oldAnimations)
|
|
{
|
|
return;
|
|
}
|
|
|
|
state->mCurrentAnimationIDs = animations;
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
|
|
if (!animations.empty())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "requesting animation start for motion " << gAnimLibrary.animationName(mLastMotion) << ": " << animations.front() << " (" << animations.size() << " animation(s))" << LL_ENDL;
|
|
for (const LLUUID& animation : animations)
|
|
{
|
|
// don't restart animations that play in both the old and the new step
|
|
if (std::find(oldAnimations.begin(), oldAnimations.end(), animation) == oldAnimations.end())
|
|
{
|
|
gAgent.sendAnimationRequest(animation, ANIM_REQUEST_START);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LL_DEBUGS("AOEngine") << "overrider came back with no animations for motion " << gAnimLibrary.animationName(mLastMotion) << "." << LL_ENDL;
|
|
}
|
|
|
|
for (const LLUUID& oldAnimation : oldAnimations)
|
|
{
|
|
if (std::find(animations.begin(), animations.end(), oldAnimation) == animations.end())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Cycling state " << state->mName << " - stopping animation " << oldAnimation << LL_ENDL;
|
|
gAgent.sendAnimationRequest(oldAnimation, ANIM_REQUEST_STOP);
|
|
gAgentAvatarp->LLCharacter::stopMotion(oldAnimation);
|
|
}
|
|
}
|
|
|
|
if (resetTimer)
|
|
{
|
|
mCurrentSet->resetTimer();
|
|
}
|
|
}
|
|
|
|
void AOEngine::playAnimation(S32 stepIndex, S32 memberIndex)
|
|
{
|
|
if (!mEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!mCurrentSet)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "cycle without set." << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
// do not cycle if we're sitting and sit-override is off
|
|
if (mLastMotion == ANIM_AGENT_SIT && !mCurrentSet->getSitOverride())
|
|
{
|
|
return;
|
|
}
|
|
// do not cycle if we're standing and mouselook stand override is disabled while being in mouselook
|
|
else if (mLastMotion == ANIM_AGENT_STAND && mCurrentSet->getMouselookStandDisable() && mInMouselook)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AOSet::AOState* state = mCurrentSet->getStateByRemapID(mLastMotion);
|
|
if (!state)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "cycle without state." << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
if (stepIndex < 0 || stepIndex >= (S32)state->mSteps.size())
|
|
{
|
|
LL_WARNS("AOEngine") << "play request for step index " << stepIndex << " out of range." << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
AOSet::AOAnimationStep& step = state->mSteps[stepIndex];
|
|
|
|
uuid_vec_t animations;
|
|
if (memberIndex >= 0)
|
|
{
|
|
if (memberIndex >= (S32)step.mMembers.size())
|
|
{
|
|
LL_WARNS("AOEngine") << "play request for member index " << memberIndex << " out of range." << LL_ENDL;
|
|
return;
|
|
}
|
|
if (LLUUID assetId = AOSet::resolveAssetUUID(step.mMembers[memberIndex]); assetId.notNull())
|
|
{
|
|
animations.emplace_back(assetId);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (AOSet::AOAnimation& anim : step.mMembers)
|
|
{
|
|
if (LLUUID assetId = AOSet::resolveAssetUUID(anim); assetId.notNull())
|
|
{
|
|
animations.emplace_back(assetId);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (animations.empty())
|
|
{
|
|
LL_WARNS("AOEngine") << "no animation assets resolved for step " << step.mName << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
uuid_vec_t oldAnimations = state->mCurrentAnimationIDs;
|
|
|
|
// don't do anything if the animation didn't change
|
|
if (animations == oldAnimations)
|
|
{
|
|
return;
|
|
}
|
|
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
|
|
state->mCurrentAnimation = static_cast<U32>(stepIndex);
|
|
state->mCurrentAnimationIDs = animations;
|
|
|
|
for (const LLUUID& animation : animations)
|
|
{
|
|
if (std::find(oldAnimations.begin(), oldAnimations.end(), animation) == oldAnimations.end())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "requesting animation start for motion " << gAnimLibrary.animationName(mLastMotion) << ": " << animation << LL_ENDL;
|
|
gAgent.sendAnimationRequest(animation, ANIM_REQUEST_START);
|
|
}
|
|
}
|
|
|
|
mAnimationChangedSignal(step.mInventoryUUID);
|
|
|
|
for (const LLUUID& oldAnimation : oldAnimations)
|
|
{
|
|
if (std::find(animations.begin(), animations.end(), oldAnimation) == animations.end())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Cycling state " << state->mName << " - stopping animation " << oldAnimation << LL_ENDL;
|
|
gAgent.sendAnimationRequest(oldAnimation, ANIM_REQUEST_STOP);
|
|
gAgentAvatarp->LLCharacter::stopMotion(oldAnimation);
|
|
}
|
|
}
|
|
|
|
mCurrentSet->resetTimer();
|
|
}
|
|
|
|
const AOSet* AOEngine::getCurrentSet() const
|
|
{
|
|
return mCurrentSet;
|
|
}
|
|
const AOSet::AOState* AOEngine::getCurrentState() const
|
|
{
|
|
return mCurrentSet->getStateByRemapID(mLastMotion);
|
|
}
|
|
|
|
void AOEngine::setItemDescription(const LLUUID& itemID, const std::string& description)
|
|
{
|
|
LLViewerInventoryItem* item = gInventory.getItem(itemID);
|
|
if (!item)
|
|
{
|
|
LL_WARNS("AOEngine") << "NULL inventory item found while trying to copy " << itemID << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
LLPointer<LLViewerInventoryItem> newItem = new LLViewerInventoryItem(item);
|
|
newItem->setDescription(description);
|
|
newItem->setComplete(true);
|
|
newItem->updateServer(false);
|
|
|
|
gInventory.updateItem(newItem);
|
|
}
|
|
|
|
void AOEngine::updateMemberSortOrder(std::vector<AOSet::AOAnimation>& animations)
|
|
{
|
|
for (U32 index = 0; index < animations.size(); ++index)
|
|
{
|
|
if (animations[index].mSortOrder != (S32)index)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "member sort order is " << animations[index].mSortOrder << " but index is " << index << LL_ENDL;
|
|
animations[index].mSortOrder = index;
|
|
setItemDescription(animations[index].mInventoryUUID, llformat("%d", index));
|
|
}
|
|
}
|
|
}
|
|
|
|
void AOEngine::updateSortOrder(AOSet::AOState* state)
|
|
{
|
|
for (U32 index = 0; index < state->mSteps.size(); ++index)
|
|
{
|
|
AOSet::AOAnimationStep& step = state->mSteps[index];
|
|
|
|
if (step.mSortOrder != (S32)index)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "step sort order is " << step.mSortOrder << " but index is " << index << LL_ENDL;
|
|
|
|
step.mSortOrder = index;
|
|
|
|
if (step.mIsGroup)
|
|
{
|
|
saveGroup(step);
|
|
}
|
|
else
|
|
{
|
|
setItemDescription(step.mInventoryUUID, llformat("%d", index));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AOEngine::addSet(const std::string& name, inventory_func_type callback, bool reload)
|
|
{
|
|
if (mAOFolder.isNull())
|
|
{
|
|
LL_WARNS("AOEngine") << ROOT_AO_FOLDER << " folder not there yet. Requesting recreation." << LL_ENDL;
|
|
tick();
|
|
return;
|
|
}
|
|
|
|
bool wasProtected = gSavedPerAccountSettings.getBOOL("LockAOFolders");
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
LL_DEBUGS("AOEngine") << "adding set folder " << name << LL_ENDL;
|
|
gInventory.createNewCategory(mAOFolder, LLFolderType::FT_NONE, name, [callback, wasProtected](const LLUUID &new_cat_id)
|
|
{
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", wasProtected);
|
|
|
|
callback(new_cat_id);
|
|
});
|
|
|
|
if (reload)
|
|
{
|
|
mTimerCollection.enableReloadTimer(true);
|
|
}
|
|
}
|
|
|
|
bool AOEngine::cloneSet(AOSet* sourceSet, std::string_view newName)
|
|
{
|
|
if (!sourceSet)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (mImportSet)
|
|
{
|
|
LL_WARNS("AOEngine") << "Set import or clone already running, ignoring clone request." << LL_ENDL;
|
|
return false;
|
|
}
|
|
|
|
if (mAOFolder.isNull())
|
|
{
|
|
LL_WARNS("AOEngine") << ROOT_AO_FOLDER << " folder not there yet. Requesting recreation." << LL_ENDL;
|
|
tick();
|
|
return false;
|
|
}
|
|
|
|
LL_INFOS("AOEngine") << "cloning set " << sourceSet->getName() << " into " << newName << LL_ENDL;
|
|
mImportSet = new AOSet(sourceSet->getInventoryUUID());
|
|
mImportSet->setName(newName);
|
|
mImportSet->setSitOverride(sourceSet->getSitOverride());
|
|
mImportSet->setSmart(sourceSet->getSmart());
|
|
mImportSet->setMouselookStandDisable(sourceSet->getMouselookStandDisable());
|
|
|
|
for (S32 index = 0; index < AOSet::AOSTATES_MAX; ++index)
|
|
{
|
|
const AOSet::AOState* sourceState = sourceSet->getState(index);
|
|
AOSet::AOState* newState = mImportSet->getState(index);
|
|
newState->mCycle = sourceState->mCycle;
|
|
newState->mRandom = sourceState->mRandom;
|
|
newState->mCycleTime = sourceState->mCycleTime;
|
|
newState->mSteps = sourceState->mSteps;
|
|
newState->mTracks = sourceState->mTracks;
|
|
}
|
|
|
|
mImportSet->setInventoryUUID(LLUUID::null);
|
|
mTimerCollection.enableImportTimer(true);
|
|
mImportRetryCount = 0;
|
|
processImport(false);
|
|
return true;
|
|
}
|
|
|
|
bool AOEngine::createAnimationLink(const LLUUID& categoryID, const LLInventoryItem* item)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Asset ID " << item->getAssetUUID() << " inventory id " << item->getUUID() << " category id " << categoryID << LL_ENDL;
|
|
|
|
if (categoryID.isNull())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "target category UUID not found, failing." << LL_ENDL;
|
|
return false;
|
|
}
|
|
|
|
LLInventoryObject::const_object_list_t obj_array{ LLConstPointer<LLInventoryObject>(item) };
|
|
link_inventory_array(categoryID,
|
|
obj_array,
|
|
LLPointer<LLInventoryCallback>(nullptr));
|
|
|
|
return true;
|
|
}
|
|
|
|
void AOEngine::addAnimation(const AOSet* set, AOSet::AOState* state, const LLInventoryItem* item, bool reload)
|
|
{
|
|
AOSet::AOAnimation anim;
|
|
anim.mAssetUUID = item->getAssetUUID();
|
|
anim.mInventoryUUID = item->getUUID();
|
|
anim.mOriginalUUID = item->getLinkedUUID();
|
|
anim.mName = item->getName();
|
|
anim.mSortOrder = static_cast<S32>(state->mSteps.size()) + 1;
|
|
|
|
AOSet::AOAnimationStep step;
|
|
step.mName = anim.mName;
|
|
step.mInventoryUUID = anim.mInventoryUUID;
|
|
step.mSortOrder = anim.mSortOrder;
|
|
step.mIsGroup = false;
|
|
step.mMembers.emplace_back(std::move(anim));
|
|
state->mSteps.emplace_back(std::move(step));
|
|
|
|
bool wasProtected = gSavedPerAccountSettings.getBOOL("LockAOFolders");
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
bool success = createAnimationLink(state->mInventoryUUID, item);
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", wasProtected);
|
|
|
|
if (success)
|
|
{
|
|
if (reload)
|
|
{
|
|
mTimerCollection.enableReloadTimer(true);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// creating the animation link failed, so we need to create a new folder for this state -
|
|
// add the animation asset to the queue of animations to insert into the state - this takes
|
|
// care of multi animation drag & drop that come in faster than the viewer can create a new
|
|
// inventory folder
|
|
state->mAddQueue.push_back(item);
|
|
|
|
// if this is the first queued animation for this state, create the folder asyncronously
|
|
if (state->mAddQueue.size() == 1)
|
|
{
|
|
gInventory.createNewCategory(set->getInventoryUUID(), LLFolderType::FT_NONE, state->mName, [this, state, reload, wasProtected](const LLUUID &new_cat_id)
|
|
{
|
|
state->mInventoryUUID = new_cat_id;
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
|
|
// add all queued animations to this state's folder and then clear the queue
|
|
for (const auto item : state->mAddQueue)
|
|
{
|
|
createAnimationLink(state->mInventoryUUID, item);
|
|
}
|
|
state->mAddQueue.clear();
|
|
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", wasProtected);
|
|
|
|
if (reload)
|
|
{
|
|
mTimerCollection.enableReloadTimer(true);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
std::string AOEngine::sanitiseFolderName(const std::string& name, const std::string& fallback) const
|
|
{
|
|
std::string clean;
|
|
clean.reserve(name.size());
|
|
|
|
for (char character : name)
|
|
{
|
|
if (character == ':' || character == '|')
|
|
{
|
|
continue;
|
|
}
|
|
if (static_cast<unsigned char>(character) < 0x20 || static_cast<unsigned char>(character) > 0x7E)
|
|
{
|
|
continue;
|
|
}
|
|
clean += character;
|
|
}
|
|
|
|
LLStringUtil::trim(clean);
|
|
|
|
if (clean.empty())
|
|
{
|
|
return fallback;
|
|
}
|
|
return clean;
|
|
}
|
|
|
|
std::string AOEngine::nextGroupName(const AOSet::AOState* state) const
|
|
{
|
|
S32 number = 1;
|
|
while (true)
|
|
{
|
|
std::string candidate = llformat("Group %d", number);
|
|
bool taken = false;
|
|
for (const auto& step : state->mSteps)
|
|
{
|
|
if (step.mIsGroup && step.mName == candidate)
|
|
{
|
|
taken = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!taken)
|
|
{
|
|
return candidate;
|
|
}
|
|
++number;
|
|
}
|
|
}
|
|
|
|
std::string AOEngine::nextTrackName(const AOSet::AOState* state) const
|
|
{
|
|
S32 number = 1;
|
|
while (true)
|
|
{
|
|
std::string candidate = llformat("Track %d", number);
|
|
bool taken = false;
|
|
for (const auto& track : state->mTracks)
|
|
{
|
|
if (track.mName == candidate)
|
|
{
|
|
taken = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!taken)
|
|
{
|
|
return candidate;
|
|
}
|
|
++number;
|
|
}
|
|
}
|
|
|
|
// group subfolder name: "<name>:OR<step order>" - categories have no description field, so order travels in the folder name
|
|
std::string AOEngine::getGroupFolderName(const AOSet::AOAnimationStep& step) const
|
|
{
|
|
return step.mName + llformat(":OR%d", step.mSortOrder);
|
|
}
|
|
|
|
// track subfolder name: "<name>:TR[:CT<time>][:CY][:RN]" - same scheme as state folders, with :TR marking the subfolder as a track
|
|
std::string AOEngine::getTrackFolderName(const AOSet::AOTrack& track) const
|
|
{
|
|
std::string params = track.mName + ":TR";
|
|
if (track.mCycleTime > 0.0f)
|
|
{
|
|
params += llformat(":CT%.2f", track.mCycleTime);
|
|
}
|
|
if (track.mCycle)
|
|
{
|
|
params += ":CY";
|
|
}
|
|
if (track.mRandom)
|
|
{
|
|
params += ":RN";
|
|
}
|
|
return params;
|
|
}
|
|
|
|
void AOEngine::saveGroup(const AOSet::AOAnimationStep& step)
|
|
{
|
|
if (!step.mIsGroup || step.mInventoryUUID.isNull())
|
|
{
|
|
return;
|
|
}
|
|
bool wasProtected = gSavedPerAccountSettings.getBOOL("LockAOFolders");
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
rename_category(&gInventory, step.mInventoryUUID, getGroupFolderName(step));
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", wasProtected);
|
|
}
|
|
|
|
void AOEngine::saveTrack(const AOSet::AOTrack& track)
|
|
{
|
|
if (track.mInventoryUUID.isNull())
|
|
{
|
|
return;
|
|
}
|
|
bool wasProtected = gSavedPerAccountSettings.getBOOL("LockAOFolders");
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
rename_category(&gInventory, track.mInventoryUUID, getTrackFolderName(track));
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", wasProtected);
|
|
}
|
|
|
|
void AOEngine::addAnimationToGroup(AOSet::AOState* state, S32 stepIndex, const LLInventoryItem* item)
|
|
{
|
|
if (!state || stepIndex < 0 || stepIndex >= (S32)state->mSteps.size() || !item)
|
|
{
|
|
return;
|
|
}
|
|
AOSet::AOAnimationStep& step = state->mSteps[stepIndex];
|
|
if (!step.mIsGroup)
|
|
{
|
|
// dropped onto a single step - that is a merge
|
|
createGroupFromMerge(state, stepIndex, item);
|
|
return;
|
|
}
|
|
bool wasProtected = gSavedPerAccountSettings.getBOOL("LockAOFolders");
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
createAnimationLink(step.mInventoryUUID, item);
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", wasProtected);
|
|
mTimerCollection.enableReloadTimer(true);
|
|
}
|
|
|
|
void AOEngine::createGroupFromMerge(AOSet::AOState* state, S32 stepIndex, const LLInventoryItem* item)
|
|
{
|
|
if (!state || stepIndex < 0 || stepIndex >= (S32)state->mSteps.size() || !item)
|
|
{
|
|
return;
|
|
}
|
|
AOSet::AOAnimationStep& step = state->mSteps[stepIndex];
|
|
if (step.mIsGroup)
|
|
{
|
|
addAnimationToGroup(state, stepIndex, item);
|
|
return;
|
|
}
|
|
const LLUUID targetLinkID = step.mInventoryUUID;
|
|
|
|
// queue the drop - more drops onto the same single may arrive while the subfolder is still being created asynchronously
|
|
auto& queue = mPendingGroupMerges[targetLinkID];
|
|
queue.push_back(item);
|
|
if (queue.size() > 1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::string groupName = sanitiseFolderName(nextGroupName(state), "Group") + llformat(":OR%d", stepIndex);
|
|
|
|
bool wasProtected = gSavedPerAccountSettings.getBOOL("LockAOFolders");
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
gInventory.createNewCategory(state->mInventoryUUID, LLFolderType::FT_NONE, groupName, [this, targetLinkID, wasProtected](const LLUUID& new_cat_id)
|
|
{
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
|
|
// move the existing animation link into the new subfolder as first member
|
|
if (LLViewerInventoryItem* linkItem = gInventory.getItem(targetLinkID))
|
|
{
|
|
gInventory.changeItemParent(linkItem, new_cat_id, false);
|
|
}
|
|
|
|
// link all queued animations behind it
|
|
for (const auto queuedItem : mPendingGroupMerges[targetLinkID])
|
|
{
|
|
createAnimationLink(new_cat_id, queuedItem);
|
|
}
|
|
mPendingGroupMerges.erase(targetLinkID);
|
|
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", wasProtected);
|
|
mTimerCollection.enableReloadTimer(true);
|
|
});
|
|
}
|
|
|
|
bool AOEngine::createGroupFromFolder(AOSet::AOState* state, const LLInventoryCategory* category)
|
|
{
|
|
if (!state || !category || state->mInventoryUUID.isNull())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
LLInventoryModel::cat_array_t* cats;
|
|
LLInventoryModel::item_array_t* items;
|
|
gInventory.getDirectDescendentsOf(category->getUUID(), cats, items);
|
|
std::vector<const LLInventoryItem*> animations;
|
|
if (items)
|
|
{
|
|
for (const auto& item : *items)
|
|
{
|
|
if (item->getInventoryType() == LLInventoryType::IT_ANIMATION)
|
|
{
|
|
animations.push_back(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (animations.empty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
std::string groupName = sanitiseFolderName(category->getName(), nextGroupName(state)) + llformat(":OR%d", static_cast<S32>(state->mSteps.size()));
|
|
|
|
bool wasProtected = gSavedPerAccountSettings.getBOOL("LockAOFolders");
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
gInventory.createNewCategory(state->mInventoryUUID, LLFolderType::FT_NONE, groupName, [this, animations, wasProtected](const LLUUID& new_cat_id)
|
|
{
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
for (const auto animationItem : animations)
|
|
{
|
|
createAnimationLink(new_cat_id, animationItem);
|
|
}
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", wasProtected);
|
|
mTimerCollection.enableReloadTimer(true);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
void AOEngine::extractMemberFromGroup(AOSet::AOState* state, S32 stepIndex, S32 memberIndex)
|
|
{
|
|
if (!state || stepIndex < 0 || stepIndex >= (S32)state->mSteps.size())
|
|
{
|
|
return;
|
|
}
|
|
AOSet::AOAnimationStep& step = state->mSteps[stepIndex];
|
|
if (!step.mIsGroup || memberIndex < 0 || memberIndex >= (S32)step.mMembers.size())
|
|
{
|
|
return;
|
|
}
|
|
|
|
LLViewerInventoryItem* linkItem = gInventory.getItem(step.mMembers[memberIndex].mInventoryUUID);
|
|
if (!linkItem)
|
|
{
|
|
LL_WARNS("AOEngine") << "group member link " << step.mMembers[memberIndex].mInventoryUUID << " not found, cannot move it out of group " << step.mName << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
const LLUUID oldLinkID = linkItem->getUUID();
|
|
const LLUUID groupFolderID = (step.mMembers.size() == 1) ? step.mInventoryUUID : LLUUID::null; // extracting the last member retires the whole subfolder, otherwise only the old link
|
|
const std::string sortOrder = llformat("%d", stepIndex); // place the extracted single right behind its former group
|
|
|
|
LLInventoryObject::const_object_list_t obj_array{ LLConstPointer<LLInventoryObject>(linkItem) };
|
|
LLPointer<LLInventoryCallback> cb = new LLBoostFuncInventoryCallback([this, oldLinkID, groupFolderID, sortOrder](const LLUUID& newItemID)
|
|
{
|
|
// only dispose of the old link when the replacement actually exists
|
|
if (newItemID.isNull())
|
|
{
|
|
return;
|
|
}
|
|
setItemDescription(newItemID, sortOrder);
|
|
if (groupFolderID.notNull())
|
|
{
|
|
purgeFolder(groupFolderID);
|
|
}
|
|
else
|
|
{
|
|
remove_inventory_object(oldLinkID, nullptr);
|
|
gInventory.notifyObservers();
|
|
}
|
|
mTimerCollection.enableReloadTimer(true);
|
|
});
|
|
link_inventory_array(state->mInventoryUUID, obj_array, cb);
|
|
|
|
step.mMembers.erase(step.mMembers.begin() + memberIndex);
|
|
|
|
if (step.mMembers.size() == 1)
|
|
{
|
|
collapseGroupToSingle(state, step);
|
|
}
|
|
else if (step.mMembers.empty())
|
|
{
|
|
state->mSteps.erase(state->mSteps.begin() + stepIndex);
|
|
}
|
|
else
|
|
{
|
|
updateMemberSortOrder(step.mMembers);
|
|
}
|
|
}
|
|
|
|
void AOEngine::collapseGroupToSingle(AOSet::AOState* state, AOSet::AOAnimationStep& step)
|
|
{
|
|
if (!step.mIsGroup || step.mMembers.size() != 1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LLViewerInventoryItem* linkItem = gInventory.getItem(step.mMembers.front().mInventoryUUID);
|
|
if (!linkItem)
|
|
{
|
|
LL_WARNS("AOEngine") << "last group member link " << step.mMembers.front().mInventoryUUID << " not found, cannot dissolve group " << step.mName << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
const LLUUID groupFolderID = step.mInventoryUUID;
|
|
const std::string sortOrder = llformat("%d", step.mSortOrder);
|
|
|
|
LLInventoryObject::const_object_list_t obj_array{ LLConstPointer<LLInventoryObject>(linkItem) };
|
|
LLPointer<LLInventoryCallback> cb = new LLBoostFuncInventoryCallback([this, groupFolderID, sortOrder](const LLUUID& newItemID)
|
|
{
|
|
// only retire the subfolder when the replacement link actually exists
|
|
if (newItemID.isNull())
|
|
{
|
|
return;
|
|
}
|
|
setItemDescription(newItemID, sortOrder);
|
|
purgeFolder(groupFolderID);
|
|
mTimerCollection.enableReloadTimer(true);
|
|
});
|
|
link_inventory_array(state->mInventoryUUID, obj_array, cb);
|
|
}
|
|
|
|
bool AOEngine::renameGroup(AOSet::AOState* state, S32 stepIndex, std::string_view newName)
|
|
{
|
|
if (!state || stepIndex < 0 || stepIndex >= (S32)state->mSteps.size())
|
|
{
|
|
return false;
|
|
}
|
|
AOSet::AOAnimationStep& step = state->mSteps[stepIndex];
|
|
if (!step.mIsGroup)
|
|
{
|
|
return false;
|
|
}
|
|
std::string cleanName = sanitiseFolderName(std::string(newName), "");
|
|
if (cleanName.empty())
|
|
{
|
|
return false;
|
|
}
|
|
step.mName = cleanName;
|
|
saveGroup(step);
|
|
mUpdatedSignal();
|
|
return true;
|
|
}
|
|
|
|
std::string AOEngine::createEmptyTrack(AOSet::AOState* state)
|
|
{
|
|
if (!state || state->mInventoryUUID.isNull())
|
|
{
|
|
return std::string();
|
|
}
|
|
std::string trackName = sanitiseFolderName(nextTrackName(state), "Track");
|
|
bool wasProtected = gSavedPerAccountSettings.getBOOL("LockAOFolders");
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
gInventory.createNewCategory(state->mInventoryUUID, LLFolderType::FT_NONE, trackName + ":TR", [this, wasProtected](const LLUUID& new_cat_id)
|
|
{
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", wasProtected);
|
|
mTimerCollection.enableReloadTimer(true);
|
|
});
|
|
return trackName;
|
|
}
|
|
|
|
void AOEngine::addAnimationToTrack(AOSet::AOState* state, S32 trackIndex, const LLInventoryItem* item)
|
|
{
|
|
if (!state || trackIndex < 0 || trackIndex >= (S32)state->mTracks.size() || !item)
|
|
{
|
|
return;
|
|
}
|
|
bool wasProtected = gSavedPerAccountSettings.getBOOL("LockAOFolders");
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
createAnimationLink(state->mTracks[trackIndex].mInventoryUUID, item);
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", wasProtected);
|
|
mTimerCollection.enableReloadTimer(true);
|
|
}
|
|
|
|
void AOEngine::removeTrack(AOSet::AOState* state, S32 trackIndex)
|
|
{
|
|
if (!state || trackIndex < 0 || trackIndex >= (S32)state->mTracks.size())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (mActiveTrackState == state)
|
|
{
|
|
stopActiveTracks();
|
|
}
|
|
if (mActiveAlwaysState == state)
|
|
{
|
|
stopAlwaysAnimations();
|
|
}
|
|
|
|
purgeFolder(state->mTracks[trackIndex].mInventoryUUID);
|
|
state->mTracks.erase(state->mTracks.begin() + trackIndex);
|
|
mTimerCollection.enableReloadTimer(true);
|
|
}
|
|
|
|
void AOEngine::removeTrackAnimation(AOSet::AOState* state, S32 trackIndex, S32 memberIndex)
|
|
{
|
|
if (!state || trackIndex < 0 || trackIndex >= (S32)state->mTracks.size())
|
|
{
|
|
return;
|
|
}
|
|
AOSet::AOTrack& track = state->mTracks[trackIndex];
|
|
if (memberIndex < 0 || memberIndex >= (S32)track.mAnimations.size())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (mActiveTrackState == state)
|
|
{
|
|
stopActiveTracks();
|
|
}
|
|
if (mActiveAlwaysState == state)
|
|
{
|
|
stopAlwaysAnimations();
|
|
}
|
|
|
|
LL_DEBUGS("AOEngine") << "purging track member: " << track.mAnimations[memberIndex].mInventoryUUID << LL_ENDL;
|
|
remove_inventory_object(track.mAnimations[memberIndex].mInventoryUUID, nullptr);
|
|
gInventory.notifyObservers();
|
|
track.mAnimations.erase(track.mAnimations.begin() + memberIndex);
|
|
if (track.mAnimations.empty())
|
|
{
|
|
purgeFolder(track.mInventoryUUID);
|
|
state->mTracks.erase(state->mTracks.begin() + trackIndex);
|
|
}
|
|
else
|
|
{
|
|
updateMemberSortOrder(track.mAnimations);
|
|
}
|
|
|
|
mTimerCollection.enableReloadTimer(true);
|
|
}
|
|
|
|
bool AOEngine::swapTrackAnimationWithPrevious(AOSet::AOState* state, S32 trackIndex, S32 memberIndex)
|
|
{
|
|
if (!state || trackIndex < 0 || trackIndex >= (S32)state->mTracks.size())
|
|
{
|
|
return false;
|
|
}
|
|
AOSet::AOTrack& track = state->mTracks[trackIndex];
|
|
if (memberIndex < 1 || memberIndex >= (S32)track.mAnimations.size())
|
|
{
|
|
return false;
|
|
}
|
|
std::swap(track.mAnimations[memberIndex], track.mAnimations[memberIndex - 1]);
|
|
updateMemberSortOrder(track.mAnimations);
|
|
return true;
|
|
}
|
|
|
|
bool AOEngine::swapTrackAnimationWithNext(AOSet::AOState* state, S32 trackIndex, S32 memberIndex)
|
|
{
|
|
if (!state || trackIndex < 0 || trackIndex >= (S32)state->mTracks.size())
|
|
{
|
|
return false;
|
|
}
|
|
AOSet::AOTrack& track = state->mTracks[trackIndex];
|
|
if (memberIndex < 0 || memberIndex >= (S32)track.mAnimations.size() - 1)
|
|
{
|
|
return false;
|
|
}
|
|
std::swap(track.mAnimations[memberIndex], track.mAnimations[memberIndex + 1]);
|
|
updateMemberSortOrder(track.mAnimations);
|
|
return true;
|
|
}
|
|
|
|
void AOEngine::setTrackCycle(AOSet::AOState* state, S32 trackIndex, bool cycle)
|
|
{
|
|
if (state && trackIndex >= 0 && trackIndex < (S32)state->mTracks.size())
|
|
{
|
|
state->mTracks[trackIndex].mCycle = cycle;
|
|
saveTrack(state->mTracks[trackIndex]);
|
|
if (state == mActiveAlwaysState)
|
|
{
|
|
refreshAlwaysTimers();
|
|
}
|
|
}
|
|
}
|
|
|
|
void AOEngine::setTrackRandomize(AOSet::AOState* state, S32 trackIndex, bool randomize)
|
|
{
|
|
if (state && trackIndex >= 0 && trackIndex < (S32)state->mTracks.size())
|
|
{
|
|
state->mTracks[trackIndex].mRandom = randomize;
|
|
saveTrack(state->mTracks[trackIndex]);
|
|
}
|
|
}
|
|
|
|
void AOEngine::setTrackCycleTime(AOSet::AOState* state, S32 trackIndex, F32 time)
|
|
{
|
|
if (state && trackIndex >= 0 && trackIndex < (S32)state->mTracks.size())
|
|
{
|
|
state->mTracks[trackIndex].mCycleTime = time;
|
|
saveTrack(state->mTracks[trackIndex]);
|
|
if (state == mActiveAlwaysState)
|
|
{
|
|
refreshAlwaysTimers();
|
|
}
|
|
}
|
|
}
|
|
|
|
void AOEngine::playTrackAnimation(AOSet::AOState* state, S32 trackIndex, S32 memberIndex)
|
|
{
|
|
if (!mEnabled || !state || (mActiveTrackState != state && mActiveAlwaysState != state))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (trackIndex < 0 || trackIndex >= (S32)state->mTracks.size())
|
|
{
|
|
return;
|
|
}
|
|
|
|
AOSet::AOTrack& track = state->mTracks[trackIndex];
|
|
if (memberIndex < 0)
|
|
{
|
|
memberIndex = track.mCurrentAnimation;
|
|
}
|
|
if (memberIndex >= (S32)track.mAnimations.size())
|
|
{
|
|
return;
|
|
}
|
|
|
|
LLUUID animation = AOSet::resolveAssetUUID(track.mAnimations[memberIndex]);
|
|
if (animation.isNull() || animation == track.mCurrentAnimationID)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (track.mCurrentAnimationID.notNull())
|
|
{
|
|
gAgent.sendAnimationRequest(track.mCurrentAnimationID, ANIM_REQUEST_STOP);
|
|
if (isAgentAvatarValid())
|
|
{
|
|
gAgentAvatarp->LLCharacter::stopMotion(track.mCurrentAnimationID);
|
|
}
|
|
}
|
|
|
|
track.mCurrentAnimation = memberIndex;
|
|
track.mCurrentAnimationID = animation;
|
|
gAgent.sendAnimationRequest(animation, ANIM_REQUEST_START);
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
}
|
|
|
|
void AOEngine::playAlwaysAnimation(S32 stepIndex, S32 memberIndex)
|
|
{
|
|
AOSet::AOState* state = mActiveAlwaysState;
|
|
if (!mEnabled || !state)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (stepIndex < 0 || stepIndex >= (S32)state->mSteps.size())
|
|
{
|
|
LL_WARNS("AOEngine") << "always state play request for step index " << stepIndex << " out of range." << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
AOSet::AOAnimationStep& step = state->mSteps[stepIndex];
|
|
uuid_vec_t animations;
|
|
if (memberIndex >= 0)
|
|
{
|
|
if (memberIndex >= (S32)step.mMembers.size())
|
|
{
|
|
LL_WARNS("AOEngine") << "always state play request for member index " << memberIndex << " out of range." << LL_ENDL;
|
|
return;
|
|
}
|
|
if (LLUUID assetId = AOSet::resolveAssetUUID(step.mMembers[memberIndex]); assetId.notNull())
|
|
{
|
|
animations.emplace_back(assetId);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (AOSet::AOAnimation& anim : step.mMembers)
|
|
{
|
|
if (LLUUID assetId = AOSet::resolveAssetUUID(anim); assetId.notNull())
|
|
{
|
|
animations.emplace_back(assetId);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (animations.empty())
|
|
{
|
|
LL_WARNS("AOEngine") << "no animation assets resolved for always state step " << step.mName << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
if (animations == state->mCurrentAnimationIDs)
|
|
{
|
|
return;
|
|
}
|
|
|
|
state->mCurrentAnimation = static_cast<U32>(stepIndex);
|
|
swapStateAnimations(state, animations);
|
|
refreshAlwaysTimers();
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
}
|
|
|
|
bool AOEngine::findForeignItems(const LLUUID& uuid) const
|
|
{
|
|
bool moved = false;
|
|
|
|
LLInventoryModel::item_array_t* items;
|
|
LLInventoryModel::cat_array_t* cats;
|
|
|
|
gInventory.getDirectDescendentsOf(uuid, cats, items);
|
|
|
|
if (cats)
|
|
{
|
|
for (const auto& cat : *cats)
|
|
{
|
|
if (findForeignItems(cat->getUUID()))
|
|
{
|
|
moved = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// count backwards in case we have to remove items
|
|
bool wasProtected = gSavedPerAccountSettings.getBOOL("LockAOFolders");
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
|
|
if (items)
|
|
{
|
|
for (S32 index = static_cast<S32>(items->size()) - 1; index >= 0; --index)
|
|
{
|
|
bool move{ false };
|
|
|
|
const LLPointer<LLViewerInventoryItem>& item = items->at(index);
|
|
if (item->getIsLinkType())
|
|
{
|
|
if (item->getInventoryType() != LLInventoryType::IT_ANIMATION)
|
|
{
|
|
LL_DEBUGS("AOEngine") << item->getName() << " is a link but does not point to an animation." << LL_ENDL;
|
|
move = true;
|
|
}
|
|
else
|
|
{
|
|
LL_DEBUGS("AOEngine") << item->getName() << " is an animation link." << LL_ENDL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LL_DEBUGS("AOEngine") << item->getName() << " is not a link!" << LL_ENDL;
|
|
move = true;
|
|
}
|
|
|
|
if (move)
|
|
{
|
|
moved = true;
|
|
gInventory.changeItemParent(item, gInventory.findCategoryUUIDForType(LLFolderType::FT_LOST_AND_FOUND), false);
|
|
LL_DEBUGS("AOEngine") << item->getName() << " moved to lost and found!" << LL_ENDL;
|
|
}
|
|
}
|
|
}
|
|
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", wasProtected);
|
|
|
|
return moved;
|
|
}
|
|
|
|
// needs a three-step process, since purge of categories only seems to work from trash
|
|
void AOEngine::purgeFolder(const LLUUID& uuid) const
|
|
{
|
|
// unprotect it
|
|
bool wasProtected = gSavedPerAccountSettings.getBOOL("LockAOFolders");
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
|
|
// move everything that's not an animation link to "lost and found"
|
|
if (findForeignItems(uuid))
|
|
{
|
|
LLNotificationsUtil::add("AOForeignItemsFound", LLSD());
|
|
}
|
|
|
|
// trash it
|
|
gInventory.removeCategory(uuid);
|
|
|
|
// clean it
|
|
purge_descendents_of(uuid, nullptr);
|
|
gInventory.notifyObservers();
|
|
|
|
// purge it
|
|
remove_inventory_object(uuid, nullptr);
|
|
gInventory.notifyObservers();
|
|
|
|
// protect it
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", wasProtected);
|
|
}
|
|
|
|
bool AOEngine::removeSet(AOSet* set)
|
|
{
|
|
purgeFolder(set->getInventoryUUID());
|
|
|
|
mTimerCollection.enableReloadTimer(true);
|
|
return true;
|
|
}
|
|
|
|
bool AOEngine::removeAnimation(const AOSet* set, AOSet::AOState* state, S32 stepIndex, S32 memberIndex)
|
|
{
|
|
if (stepIndex < 0 || stepIndex >= (S32)state->mSteps.size())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
AOSet::AOAnimationStep& step = state->mSteps[stepIndex];
|
|
if (memberIndex >= 0 && step.mIsGroup)
|
|
{
|
|
if (memberIndex >= (S32)step.mMembers.size())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
LL_DEBUGS("AOEngine") << "purging group member: " << step.mMembers[memberIndex].mInventoryUUID << LL_ENDL;
|
|
remove_inventory_object(step.mMembers[memberIndex].mInventoryUUID, nullptr);
|
|
gInventory.notifyObservers();
|
|
|
|
step.mMembers.erase(step.mMembers.begin() + memberIndex);
|
|
|
|
if (step.mMembers.size() == 1)
|
|
{
|
|
collapseGroupToSingle(state, step);
|
|
}
|
|
else if (step.mMembers.empty())
|
|
{
|
|
purgeFolder(step.mInventoryUUID);
|
|
state->mSteps.erase(state->mSteps.begin() + stepIndex);
|
|
}
|
|
else
|
|
{
|
|
updateMemberSortOrder(step.mMembers);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
if (step.mIsGroup)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "purging group folder: " << step.mInventoryUUID << LL_ENDL;
|
|
purgeFolder(step.mInventoryUUID);
|
|
state->mSteps.erase(state->mSteps.begin() + stepIndex);
|
|
}
|
|
else
|
|
{
|
|
LLViewerInventoryItem* item = gInventory.getItem(step.mInventoryUUID);
|
|
if (item)
|
|
{
|
|
// check if this item is actually an animation link
|
|
bool move = true;
|
|
if (item->getIsLinkType())
|
|
{
|
|
if (item->getInventoryType() == LLInventoryType::IT_ANIMATION)
|
|
{
|
|
// it is an animation link, so mark it to be purged
|
|
move = false;
|
|
}
|
|
}
|
|
|
|
// this item was not an animation link, move it to lost and found
|
|
if (move)
|
|
{
|
|
gInventory.changeItemParent(item, gInventory.findCategoryUUIDForType(LLFolderType::FT_LOST_AND_FOUND), false);
|
|
LLNotificationsUtil::add("AOForeignItemsFound", LLSD());
|
|
update();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// purge the item from inventory
|
|
LL_DEBUGS("AOEngine") << __LINE__ << " purging: " << step.mInventoryUUID << LL_ENDL;
|
|
remove_inventory_object(step.mInventoryUUID, nullptr);
|
|
gInventory.notifyObservers();
|
|
|
|
state->mSteps.erase(state->mSteps.begin() + stepIndex);
|
|
}
|
|
|
|
// count steps, not raw inventory items - group subfolders alone must not keep a state folder alive once its last step is gone
|
|
if (state->mSteps.empty() && state->mTracks.empty())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "purging folder " << state->mName << " from inventory because it's empty." << LL_ENDL;
|
|
|
|
LLInventoryModel::item_array_t* items;
|
|
LLInventoryModel::cat_array_t* cats;
|
|
gInventory.getDirectDescendentsOf(set->getInventoryUUID(), cats, items);
|
|
|
|
if (cats)
|
|
{
|
|
for (const auto& cat : *cats)
|
|
{
|
|
std::vector<std::string> params;
|
|
LLStringUtil::getTokens(cat->getName(), params, ":");
|
|
|
|
if (params.empty())
|
|
{
|
|
LL_WARNS("AOEngine") << "Unexpected folder found in ao set folder: " << cat->getName() << LL_ENDL;
|
|
return false;
|
|
}
|
|
|
|
const std::string& stateName = params[0];
|
|
|
|
if (state->mName.compare(stateName) == 0)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "folder found: " << cat->getName() << " purging uuid " << cat->getUUID() << LL_ENDL;
|
|
|
|
purgeFolder(cat->getUUID());
|
|
state->mInventoryUUID.setNull();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
updateSortOrder(state);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// reorder steps (memberIndex -1) or members within a group; member moves are clamped to their group, they never leave it through the edges
|
|
bool AOEngine::swapWithPrevious(AOSet::AOState* state, S32 stepIndex, S32 memberIndex)
|
|
{
|
|
if (stepIndex < 0 || stepIndex >= (S32)state->mSteps.size())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (memberIndex >= 0)
|
|
{
|
|
AOSet::AOAnimationStep& step = state->mSteps[stepIndex];
|
|
if (!step.mIsGroup || memberIndex < 1 || memberIndex >= (S32)step.mMembers.size())
|
|
{
|
|
return false;
|
|
}
|
|
std::swap(step.mMembers[memberIndex], step.mMembers[memberIndex - 1]);
|
|
updateMemberSortOrder(step.mMembers);
|
|
return true;
|
|
}
|
|
|
|
if (state->mSteps.size() < 2 || stepIndex == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
std::swap(state->mSteps[stepIndex], state->mSteps[stepIndex - 1]);
|
|
updateSortOrder(state);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool AOEngine::swapWithNext(AOSet::AOState* state, S32 stepIndex, S32 memberIndex)
|
|
{
|
|
if (stepIndex < 0 || stepIndex >= (S32)state->mSteps.size())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (memberIndex >= 0)
|
|
{
|
|
AOSet::AOAnimationStep& step = state->mSteps[stepIndex];
|
|
if (!step.mIsGroup || memberIndex < 0 || memberIndex >= (S32)step.mMembers.size() - 1)
|
|
{
|
|
return false;
|
|
}
|
|
std::swap(step.mMembers[memberIndex], step.mMembers[memberIndex + 1]);
|
|
updateMemberSortOrder(step.mMembers);
|
|
return true;
|
|
}
|
|
|
|
if (state->mSteps.size() < 2 || stepIndex == (S32)state->mSteps.size() - 1)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
std::swap(state->mSteps[stepIndex], state->mSteps[stepIndex + 1]);
|
|
updateSortOrder(state);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool AOEngine::readAnimationLinks(const LLUUID& categoryID, std::vector<AOSet::AOAnimation>& animations)
|
|
{
|
|
if (!gInventory.isCategoryComplete(categoryID))
|
|
{
|
|
LL_DEBUGS("AOEngine") << "category " << categoryID << " is incomplete, fetching descendents" << LL_ENDL;
|
|
gInventory.fetchDescendentsOf(categoryID);
|
|
return false;
|
|
}
|
|
LLInventoryModel::item_array_t* items;
|
|
LLInventoryModel::cat_array_t* dummy;
|
|
|
|
gInventory.getDirectDescendentsOf(categoryID, dummy, items);
|
|
|
|
if (items)
|
|
{
|
|
for (const auto& item : *items)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Found animation link " << item->LLInventoryItem::getName()
|
|
<< " desc " << item->LLInventoryItem::getDescription()
|
|
<< " asset " << item->getAssetUUID() << LL_ENDL;
|
|
|
|
AOSet::AOAnimation anim;
|
|
anim.mName = item->getName();
|
|
anim.mInventoryUUID = item->getUUID();
|
|
anim.mOriginalUUID = item->getLinkedUUID();
|
|
anim.mAssetUUID.setNull();
|
|
|
|
// if we can find the original animation already right here, save its asset ID, otherwise this will
|
|
// be tried again in AOSet::getAnimationsForState() and/or AOEngine::cycle()
|
|
if (item->getLinkedItem())
|
|
{
|
|
anim.mAssetUUID = item->getAssetUUID();
|
|
}
|
|
|
|
S32 sortOrder;
|
|
if (!LLStringUtil::convertToS32(item->LLInventoryItem::getDescription(), sortOrder))
|
|
{
|
|
sortOrder = -1;
|
|
}
|
|
anim.mSortOrder = sortOrder;
|
|
|
|
LL_DEBUGS("AOEngine") << "current sort order is " << sortOrder << LL_ENDL;
|
|
|
|
if (sortOrder == -1)
|
|
{
|
|
LL_WARNS("AOEngine") << "sort order was unknown so append to the end of the list" << LL_ENDL;
|
|
animations.emplace_back(std::move(anim));
|
|
}
|
|
else
|
|
{
|
|
bool inserted = false;
|
|
for (size_t index = 0; index < animations.size(); ++index)
|
|
{
|
|
if (animations[index].mSortOrder > sortOrder)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "inserting at index " << index << LL_ENDL;
|
|
animations.insert(animations.begin() + index, anim);
|
|
inserted = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!inserted)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "not inserted yet, appending to the list instead" << LL_ENDL;
|
|
animations.emplace_back(std::move(anim));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool AOEngine::reloadStateAnimations(AOSet* set, AOSet::AOState* state)
|
|
{
|
|
LLInventoryModel::item_array_t* dummyItems;
|
|
LLInventoryModel::cat_array_t* cats;
|
|
bool allComplete = true;
|
|
state->mSteps.clear();
|
|
state->mTracks.clear();
|
|
|
|
auto insertStep = [state](AOSet::AOAnimationStep&& step)
|
|
{
|
|
if (step.mSortOrder == -1)
|
|
{
|
|
LL_WARNS("AOEngine") << "step sort order was unknown so append to the end of the list" << LL_ENDL;
|
|
state->mSteps.emplace_back(std::move(step));
|
|
return;
|
|
}
|
|
|
|
for (size_t index = 0; index < state->mSteps.size(); ++index)
|
|
{
|
|
if (state->mSteps[index].mSortOrder > step.mSortOrder)
|
|
{
|
|
state->mSteps.insert(state->mSteps.begin() + index, std::move(step));
|
|
return;
|
|
}
|
|
}
|
|
state->mSteps.emplace_back(std::move(step));
|
|
};
|
|
|
|
std::vector<AOSet::AOAnimation> directLinks;
|
|
if (!readAnimationLinks(state->mInventoryUUID, directLinks))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
for (auto& anim : directLinks)
|
|
{
|
|
AOSet::AOAnimationStep step;
|
|
step.mName = anim.mName;
|
|
step.mInventoryUUID = anim.mInventoryUUID;
|
|
step.mSortOrder = anim.mSortOrder;
|
|
step.mIsGroup = false;
|
|
step.mMembers.emplace_back(std::move(anim));
|
|
insertStep(std::move(step));
|
|
}
|
|
|
|
gInventory.getDirectDescendentsOf(state->mInventoryUUID, cats, dummyItems);
|
|
|
|
if (cats)
|
|
{
|
|
for (const auto& cat : *cats)
|
|
{
|
|
std::vector<std::string> params;
|
|
LLStringUtil::getTokens(cat->getName(), params, ":");
|
|
if (params.empty())
|
|
{
|
|
LL_WARNS("AOEngine") << "Subfolder with empty name in state folder: " << cat->getName() << LL_ENDL;
|
|
continue;
|
|
}
|
|
|
|
bool isTrack = false;
|
|
for (size_t num = 1; num < params.size(); ++num)
|
|
{
|
|
if (params[num] == "TR")
|
|
{
|
|
isTrack = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (isTrack)
|
|
{
|
|
AOSet::AOTrack track;
|
|
track.mName = params[0];
|
|
track.mInventoryUUID = cat->getUUID();
|
|
|
|
for (size_t num = 1; num < params.size(); ++num)
|
|
{
|
|
if (params[num] == "TR")
|
|
{
|
|
continue;
|
|
}
|
|
else if (params[num] == "CY")
|
|
{
|
|
track.mCycle = true;
|
|
}
|
|
else if (params[num] == "RN")
|
|
{
|
|
track.mRandom = true;
|
|
}
|
|
else if (params[num].substr(0, 2) == "CT")
|
|
{
|
|
LLStringUtil::convertToF32(params[num].substr(2), track.mCycleTime);
|
|
}
|
|
else
|
|
{
|
|
LL_WARNS("AOEngine") << "Unknown AO track option " << params[num] << LL_ENDL;
|
|
}
|
|
}
|
|
|
|
if (!readAnimationLinks(track.mInventoryUUID, track.mAnimations))
|
|
{
|
|
allComplete = false;
|
|
continue;
|
|
}
|
|
|
|
updateMemberSortOrder(track.mAnimations);
|
|
state->mTracks.emplace_back(std::move(track));
|
|
}
|
|
else
|
|
{
|
|
AOSet::AOAnimationStep step;
|
|
step.mName = params[0];
|
|
step.mInventoryUUID = cat->getUUID();
|
|
step.mIsGroup = true;
|
|
step.mSortOrder = -1;
|
|
|
|
for (size_t num = 1; num < params.size(); ++num)
|
|
{
|
|
if (params[num].substr(0, 2) == "OR")
|
|
{
|
|
S32 sortOrder;
|
|
if (LLStringUtil::convertToS32(params[num].substr(2), sortOrder))
|
|
{
|
|
step.mSortOrder = sortOrder;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LL_WARNS("AOEngine") << "Unknown AO group option " << params[num] << LL_ENDL;
|
|
}
|
|
}
|
|
|
|
if (!readAnimationLinks(step.mInventoryUUID, step.mMembers))
|
|
{
|
|
allComplete = false;
|
|
continue;
|
|
}
|
|
|
|
if (step.mMembers.empty())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "ignoring empty group subfolder " << cat->getName() << LL_ENDL;
|
|
continue;
|
|
}
|
|
|
|
updateMemberSortOrder(step.mMembers);
|
|
insertStep(std::move(step));
|
|
}
|
|
}
|
|
}
|
|
|
|
updateSortOrder(state);
|
|
|
|
if (auto currentState = gSavedPerAccountSettings.getLLSD("FSCurrentAOState");
|
|
currentState.has("CurrentSet") && currentState["CurrentSet"].asString() == set->getName() &&
|
|
currentState.has("CurrentStateAnimations") && currentState["CurrentStateAnimations"].has(state->mName))
|
|
{
|
|
auto currentStateAnimId{ currentState["CurrentStateAnimations"][state->mName].asUUID() };
|
|
LLXORCipher cipher(ENCRYPTION_MAGIC_ID.mData, UUID_BYTES);
|
|
cipher.decrypt(currentStateAnimId.mData, UUID_BYTES);
|
|
|
|
for (size_t stepIndex = 0; stepIndex < state->mSteps.size(); ++stepIndex)
|
|
{
|
|
bool found = false;
|
|
for (const auto& member : state->mSteps[stepIndex].mMembers)
|
|
{
|
|
if (member.mAssetUUID == currentStateAnimId)
|
|
{
|
|
state->mCurrentAnimation = static_cast<U32>(stepIndex);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (found)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return allComplete;
|
|
}
|
|
|
|
void AOEngine::update()
|
|
{
|
|
if (mAOFolder.isNull())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!gInventory.isCategoryComplete(mAOFolder))
|
|
{
|
|
LL_DEBUGS("AOEngine") << "#AO folder hasn't fully fetched yet, try again next timer tick." << LL_ENDL;
|
|
gInventory.fetchDescendentsOf(mAOFolder);
|
|
return;
|
|
}
|
|
|
|
// move everything that's not an animation link to "lost and found"
|
|
if (findForeignItems(mAOFolder))
|
|
{
|
|
LLNotificationsUtil::add("AOForeignItemsFound", LLSD());
|
|
}
|
|
|
|
LLInventoryModel::cat_array_t* categories;
|
|
LLInventoryModel::item_array_t* items;
|
|
|
|
bool allComplete = true;
|
|
mTimerCollection.enableSettingsTimer(false);
|
|
|
|
gInventory.getDirectDescendentsOf(mAOFolder, categories, items);
|
|
|
|
if (categories)
|
|
{
|
|
std::vector<std::pair<LLUUID, AOSet*>> setsToWrite;
|
|
for (const auto& currentCategory : *categories)
|
|
{
|
|
const std::string& setFolderName = currentCategory->getName();
|
|
|
|
if (setFolderName.empty())
|
|
{
|
|
LL_WARNS("AOEngine") << "Folder with empty name in AO folder" << LL_ENDL;
|
|
continue;
|
|
}
|
|
|
|
std::vector<std::string> params;
|
|
LLStringUtil::getTokens(setFolderName, params, ":");
|
|
if (params.empty())
|
|
{
|
|
LL_WARNS("AOEngine") << "Unexpected folder found in ao set folder: " << currentCategory->getName() << LL_ENDL;
|
|
continue;
|
|
}
|
|
|
|
auto setName{ params[0] };
|
|
AOSet* newSet = getSetByName(setName);
|
|
if (!newSet)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Adding set " << setFolderName << " to AO." << LL_ENDL;
|
|
newSet = new AOSet(currentCategory->getUUID());
|
|
newSet->setName(setName);
|
|
mSets.emplace_back(newSet);
|
|
setsToWrite.emplace_back(currentCategory->getUUID(), newSet);
|
|
}
|
|
else
|
|
{
|
|
if (newSet->getComplete())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Set " << setName << " already complete. Skipping." << LL_ENDL;
|
|
continue;
|
|
}
|
|
LL_DEBUGS("AOEngine") << "Updating set " << setFolderName << " in AO." << LL_ENDL;
|
|
setsToWrite.emplace_back(currentCategory->getUUID(), newSet);
|
|
}
|
|
allComplete = false;
|
|
|
|
for (auto num = 1; num < params.size(); ++num)
|
|
{
|
|
if (params[num].size() != 2)
|
|
{
|
|
LL_WARNS("AOEngine") << "Unknown AO set option " << params[num] << LL_ENDL;
|
|
}
|
|
else if (params[num] == "SO")
|
|
{
|
|
newSet->setSitOverride(true);
|
|
}
|
|
else if (params[num] == "SM")
|
|
{
|
|
newSet->setSmart(true);
|
|
}
|
|
else if (params[num] == "DM")
|
|
{
|
|
newSet->setMouselookStandDisable(true);
|
|
}
|
|
else if (params[num] == "**")
|
|
{
|
|
mDefaultSet = newSet;
|
|
mCurrentSet = newSet;
|
|
}
|
|
else
|
|
{
|
|
LL_WARNS("AOEngine") << "Unknown AO set option " << params[num] << LL_ENDL;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const auto& [categoryUUID, newSet] : setsToWrite)
|
|
{
|
|
if (!mDefaultSet)
|
|
{
|
|
// No default is set, so instead use the set saved in FSCurrentAOState
|
|
if (auto currentState = gSavedPerAccountSettings.getLLSD("FSCurrentAOState");
|
|
currentState.has("CurrentSet") && currentState["CurrentSet"].asString() == newSet->getName())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Selecting current set from settings: " << newSet->getName() << LL_ENDL;
|
|
mCurrentSet = newSet;
|
|
}
|
|
}
|
|
|
|
if (gInventory.isCategoryComplete(categoryUUID))
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Set " << newSet->getName() << " is complete, reading states ..." << LL_ENDL;
|
|
|
|
LLInventoryModel::cat_array_t* stateCategories;
|
|
gInventory.getDirectDescendentsOf(categoryUUID, stateCategories, items);
|
|
newSet->setComplete(true);
|
|
|
|
for (const auto& stateCategory : *stateCategories)
|
|
{
|
|
std::vector<std::string> state_params;
|
|
LLStringUtil::getTokens(stateCategory->getName(), state_params, ":");
|
|
if (state_params.empty())
|
|
{
|
|
LL_WARNS("AOEngine") << "Unexpected state folder found in ao set: " << stateCategory->getName() << LL_ENDL;
|
|
continue;
|
|
}
|
|
const std::string& stateName = state_params[0];
|
|
|
|
AOSet::AOState* state = newSet->getStateByName(stateName);
|
|
if (!state)
|
|
{
|
|
LL_WARNS("AOEngine") << "Unknown state " << stateName << ". Skipping." << LL_ENDL;
|
|
continue;
|
|
}
|
|
LL_DEBUGS("AOEngine") << "Reading state " << stateName << LL_ENDL;
|
|
|
|
state->mInventoryUUID = stateCategory->getUUID();
|
|
for (auto num = 1; num < state_params.size(); ++num)
|
|
{
|
|
if (state_params[num] == "CY")
|
|
{
|
|
state->mCycle = true;
|
|
LL_DEBUGS("AOEngine") << "Cycle on" << LL_ENDL;
|
|
}
|
|
else if (state_params[num] == "RN")
|
|
{
|
|
state->mRandom = true;
|
|
LL_DEBUGS("AOEngine") << "Random on" << LL_ENDL;
|
|
}
|
|
else if (state_params[num].substr(0, 2) == "CT")
|
|
{
|
|
LLStringUtil::convertToF32(state_params[num].substr(2, state_params[num].size() - 2), state->mCycleTime);
|
|
LL_DEBUGS("AOEngine") << "Cycle Time specified:" << state->mCycleTime << LL_ENDL;
|
|
}
|
|
else
|
|
{
|
|
LL_WARNS("AOEngine") << "Unknown AO set option " << state_params[num] << LL_ENDL;
|
|
}
|
|
}
|
|
|
|
if (!gInventory.isCategoryComplete(state->mInventoryUUID))
|
|
{
|
|
LL_DEBUGS("AOEngine") << "State category " << stateName << " is incomplete, fetching descendents" << LL_ENDL;
|
|
gInventory.fetchDescendentsOf(state->mInventoryUUID);
|
|
allComplete = false;
|
|
newSet->setComplete(false);
|
|
continue;
|
|
}
|
|
if (!reloadStateAnimations(newSet, state))
|
|
{
|
|
LL_DEBUGS("AOEngine") << "State " << stateName << " has incomplete subfolders, fetching" << LL_ENDL;
|
|
allComplete = false;
|
|
newSet->setComplete(false);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Set " << newSet->getName() << " is incomplete, fetching descendents" << LL_ENDL;
|
|
gInventory.fetchDescendentsOf(categoryUUID);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (allComplete)
|
|
{
|
|
mEnabled = gSavedPerAccountSettings.getBOOL("UseAO");
|
|
|
|
if (!mCurrentSet && !mSets.empty())
|
|
{
|
|
LL_DEBUGS("AOEngine") << "No set currently selected, choosing the first in the list." << LL_ENDL;
|
|
selectSet(mSets[0]);
|
|
}
|
|
|
|
mTimerCollection.enableInventoryTimer(false);
|
|
mTimerCollection.enableSettingsTimer(true);
|
|
|
|
LL_INFOS("AOEngine") << "sending update signal" << LL_ENDL;
|
|
mUpdatedSignal();
|
|
enable(mEnabled);
|
|
}
|
|
}
|
|
|
|
void AOEngine::reload(bool fromTimer)
|
|
{
|
|
bool wasEnabled = mEnabled;
|
|
|
|
mTimerCollection.enableReloadTimer(false);
|
|
|
|
if (wasEnabled)
|
|
{
|
|
enable(false);
|
|
}
|
|
|
|
gAgent.stopCurrentAnimations();
|
|
mLastOverriddenMotion = ANIM_AGENT_STAND;
|
|
|
|
clear(fromTimer);
|
|
mAOFolder.setNull();
|
|
mTimerCollection.enableInventoryTimer(true);
|
|
tick();
|
|
|
|
if (wasEnabled)
|
|
{
|
|
enable(true);
|
|
}
|
|
}
|
|
|
|
AOSet* AOEngine::getSetByName(std::string_view name) const
|
|
{
|
|
for (auto set : mSets)
|
|
{
|
|
if (set->getName() == name)
|
|
{
|
|
return set;
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
const std::string AOEngine::getCurrentSetName() const
|
|
{
|
|
if (mCurrentSet)
|
|
{
|
|
return mCurrentSet->getName();
|
|
}
|
|
return std::string();
|
|
}
|
|
|
|
const AOSet* AOEngine::getDefaultSet() const
|
|
{
|
|
return mDefaultSet;
|
|
}
|
|
|
|
void AOEngine::selectSet(AOSet* set)
|
|
{
|
|
if (mEnabled && mCurrentSet)
|
|
{
|
|
stopActiveTracks();
|
|
stopAlwaysAnimations();
|
|
if (AOSet::AOState* state = mCurrentSet->getStateByRemapID(mLastOverriddenMotion))
|
|
{
|
|
stopCurrentAnimations(state);
|
|
mCurrentSet->stopTimer();
|
|
}
|
|
}
|
|
|
|
mCurrentSet = set;
|
|
|
|
if (mEnabled)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "enabling with motion " << gAnimLibrary.animationName(mLastMotion) << LL_ENDL;
|
|
startAlwaysAnimations();
|
|
gAgent.sendAnimationRequest(override(mLastMotion, true), ANIM_REQUEST_START);
|
|
}
|
|
}
|
|
|
|
AOSet* AOEngine::selectSetByName(std::string_view name)
|
|
{
|
|
if (AOSet* set = getSetByName(name))
|
|
{
|
|
selectSet(set);
|
|
return set;
|
|
}
|
|
LL_WARNS("AOEngine") << "Could not find AO set " << name << LL_ENDL;
|
|
return nullptr;
|
|
}
|
|
|
|
const std::vector<AOSet*> AOEngine::getSetList() const
|
|
{
|
|
return mSets;
|
|
}
|
|
|
|
void AOEngine::saveSet(const AOSet* set)
|
|
{
|
|
if (!set)
|
|
{
|
|
return;
|
|
}
|
|
|
|
std::string setParams = getSetFolderName(set);
|
|
if (set == mDefaultSet)
|
|
{
|
|
setParams += ":**";
|
|
}
|
|
|
|
/*
|
|
// This works fine, but LL seems to have added a few helper functions in llinventoryfunctions.h
|
|
// so let's make use of them. This code is just for reference
|
|
|
|
LLViewerInventoryCategory* cat=gInventory.getCategory(set->getInventoryUUID());
|
|
LL_WARNS("AOEngine") << cat << LL_ENDL;
|
|
cat->rename(setParams);
|
|
cat->updateServer(false);
|
|
gInventory.addChangedMask(LLInventoryObserver::LABEL, cat->getUUID());
|
|
gInventory.notifyObservers();
|
|
*/
|
|
bool wasProtected = gSavedPerAccountSettings.getBOOL("LockAOFolders");
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
rename_category(&gInventory, set->getInventoryUUID(), setParams);
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", wasProtected);
|
|
|
|
LL_INFOS("AOEngine") << "sending update signal" << LL_ENDL;
|
|
mUpdatedSignal();
|
|
}
|
|
|
|
bool AOEngine::renameSet(AOSet* set, std::string_view name)
|
|
{
|
|
if (name.empty() || name.find(":") != std::string_view::npos)
|
|
{
|
|
return false;
|
|
}
|
|
set->setName(name);
|
|
set->setDirty(true);
|
|
|
|
return true;
|
|
}
|
|
|
|
std::string AOEngine::getSetFolderName(const AOSet* set) const
|
|
{
|
|
std::string params = set->getName();
|
|
if (set->getSitOverride())
|
|
{
|
|
params += ":SO";
|
|
}
|
|
if (set->getSmart())
|
|
{
|
|
params += ":SM";
|
|
}
|
|
if (set->getMouselookStandDisable())
|
|
{
|
|
params += ":DM";
|
|
}
|
|
return params;
|
|
}
|
|
|
|
std::string AOEngine::getStateFolderName(const AOSet::AOState* state) const
|
|
{
|
|
std::string params = state->mName;
|
|
if (state->mCycleTime > 0.0f)
|
|
{
|
|
params += llformat(":CT%.2f", state->mCycleTime);
|
|
}
|
|
if (state->mCycle)
|
|
{
|
|
params += ":CY";
|
|
}
|
|
if (state->mRandom)
|
|
{
|
|
params += ":RN";
|
|
}
|
|
return params;
|
|
}
|
|
|
|
void AOEngine::saveState(const AOSet::AOState* state)
|
|
{
|
|
std::string stateParams = getStateFolderName(state);
|
|
bool wasProtected = gSavedPerAccountSettings.getBOOL("LockAOFolders");
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", false);
|
|
rename_category(&gInventory, state->mInventoryUUID, stateParams);
|
|
gSavedPerAccountSettings.setBOOL("LockAOFolders", wasProtected);
|
|
}
|
|
|
|
void AOEngine::saveSettings()
|
|
{
|
|
for (auto set : mSets)
|
|
{
|
|
if (set->getDirty())
|
|
{
|
|
saveSet(set);
|
|
LL_INFOS("AOEngine") << "dirty set saved " << set->getName() << LL_ENDL;
|
|
set->setDirty(false);
|
|
}
|
|
|
|
for (S32 stateIndex = 0; stateIndex < AOSet::AOSTATES_MAX; ++stateIndex)
|
|
{
|
|
AOSet::AOState* state = set->getState(stateIndex);
|
|
if (state->mDirty)
|
|
{
|
|
saveState(state);
|
|
LL_INFOS("AOEngine") << "dirty state saved " << state->mName << LL_ENDL;
|
|
state->mDirty = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void AOEngine::inMouselook(bool mouselook)
|
|
{
|
|
if (mInMouselook == mouselook)
|
|
{
|
|
return;
|
|
}
|
|
|
|
mInMouselook = mouselook;
|
|
|
|
if (!mCurrentSet)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!mCurrentSet->getMouselookStandDisable())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!mEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (mLastMotion != ANIM_AGENT_STAND)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (mouselook)
|
|
{
|
|
AOSet::AOState* state = mCurrentSet->getState(AOSet::Standing);
|
|
if (!state)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!state->mCurrentAnimationIDs.empty())
|
|
{
|
|
LL_DEBUGS("AOEngine") << " stopping animation(s) in state " << state->mName << LL_ENDL;
|
|
stopCurrentAnimations(state);
|
|
}
|
|
stopActiveTracks();
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_STAND, ANIM_REQUEST_START);
|
|
}
|
|
else
|
|
{
|
|
stopAllStandVariants();
|
|
gAgent.sendAnimationRequest(override(ANIM_AGENT_STAND, true), ANIM_REQUEST_START);
|
|
}
|
|
}
|
|
|
|
void AOEngine::setDefaultSet(AOSet* set)
|
|
{
|
|
mDefaultSet = set;
|
|
for (auto set : mSets)
|
|
{
|
|
set->setDirty(true);
|
|
}
|
|
}
|
|
|
|
void AOEngine::setOverrideSits(AOSet* set, bool override_sit)
|
|
{
|
|
set->setSitOverride(override_sit);
|
|
set->setDirty(true);
|
|
|
|
if (mCurrentSet != set)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (mLastMotion != ANIM_AGENT_SIT)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!mEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (override_sit)
|
|
{
|
|
stopAllSitVariants();
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_SIT_GENERIC, ANIM_REQUEST_START);
|
|
}
|
|
else
|
|
{
|
|
// remove sit cycle cover up
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_SIT_GENERIC, ANIM_REQUEST_STOP);
|
|
|
|
AOSet::AOState* state = mCurrentSet->getState(AOSet::Sitting);
|
|
if (state)
|
|
{
|
|
if (!state->mCurrentAnimationIDs.empty())
|
|
{
|
|
stopCurrentAnimations(state);
|
|
}
|
|
stopActiveTracks();
|
|
mAnimationChangedSignal(LLUUID::null);
|
|
}
|
|
|
|
if (!foreignAnimations())
|
|
{
|
|
gAgent.sendAnimationRequest(ANIM_AGENT_SIT, ANIM_REQUEST_START);
|
|
}
|
|
}
|
|
}
|
|
|
|
void AOEngine::setSmart(AOSet* set, bool smart)
|
|
{
|
|
set->setSmart(smart);
|
|
set->setDirty(true);
|
|
|
|
if (!mEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (smart)
|
|
{
|
|
// make sure to restart the sit cancel timer to fix sit overrides when the object we are
|
|
// sitting on is playing its own animation
|
|
const LLViewerObject* agentRoot = dynamic_cast<LLViewerObject*>(gAgentAvatarp->getRoot());
|
|
if (agentRoot && agentRoot->getID() != gAgentID)
|
|
{
|
|
mSitCancelTimer.oneShot();
|
|
}
|
|
}
|
|
}
|
|
|
|
void AOEngine::setDisableMouselookStands(AOSet* set, bool disabled)
|
|
{
|
|
set->setMouselookStandDisable(disabled);
|
|
set->setDirty(true);
|
|
|
|
if (mCurrentSet != set)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!mEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// make sure an update happens if needed
|
|
mInMouselook = !gAgentCamera.cameraMouselook();
|
|
inMouselook(!mInMouselook);
|
|
}
|
|
|
|
void AOEngine::setCycle(AOSet::AOState* state, bool cycle)
|
|
{
|
|
state->mCycle = cycle;
|
|
state->mDirty = true;
|
|
if (state == mActiveAlwaysState)
|
|
{
|
|
refreshAlwaysTimers();
|
|
}
|
|
}
|
|
|
|
void AOEngine::setRandomize(AOSet::AOState* state, bool randomize)
|
|
{
|
|
state->mRandom = randomize;
|
|
state->mDirty = true;
|
|
}
|
|
|
|
void AOEngine::setCycleTime(AOSet::AOState* state, F32 time)
|
|
{
|
|
state->mCycleTime = time;
|
|
state->mDirty = true;
|
|
if (state == mActiveAlwaysState)
|
|
{
|
|
refreshAlwaysTimers();
|
|
}
|
|
}
|
|
|
|
void AOEngine::tick()
|
|
{
|
|
// <FS:ND> make sure agent is alive and kicking before doing anything
|
|
if (!isAgentAvatarValid())
|
|
{
|
|
return;
|
|
}
|
|
// </FS:ND>
|
|
|
|
const LLUUID categoryID = gInventory.findCategoryByName(ROOT_FIRESTORM_FOLDER);
|
|
|
|
if (categoryID.isNull())
|
|
{
|
|
LL_WARNS("AOEngine") << "no " << ROOT_FIRESTORM_FOLDER << " folder yet. Creating ..." << LL_ENDL;
|
|
gInventory.createNewCategory(gInventory.getRootFolderID(), LLFolderType::FT_NONE, ROOT_FIRESTORM_FOLDER);
|
|
mAOFolder.setNull();
|
|
}
|
|
else
|
|
{
|
|
LLInventoryModel::cat_array_t* categories;
|
|
LLInventoryModel::item_array_t* items;
|
|
gInventory.getDirectDescendentsOf(categoryID, categories, items);
|
|
LL_DEBUGS("AOEngine") << "cat " << categories->size() << " items " << items->size() << LL_ENDL;
|
|
|
|
for (const auto& cat : *categories)
|
|
{
|
|
const std::string& catName = cat->getName();
|
|
if (catName.compare(ROOT_AO_FOLDER) == 0)
|
|
{
|
|
mAOFolder = cat->getUUID();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (mAOFolder.isNull())
|
|
{
|
|
LL_WARNS("AOEngine") << "no " << ROOT_AO_FOLDER << " folder yet. Creating ..." << LL_ENDL;
|
|
gInventory.createNewCategory(categoryID, LLFolderType::FT_NONE, ROOT_AO_FOLDER);
|
|
}
|
|
else
|
|
{
|
|
LL_INFOS("AOEngine") << "AO basic folder structure intact." << LL_ENDL;
|
|
update();
|
|
}
|
|
}
|
|
}
|
|
|
|
bool AOEngine::importNotecard(const LLInventoryItem* item)
|
|
{
|
|
if (item)
|
|
{
|
|
LL_INFOS("AOEngine") << "importing AO notecard: " << item->getName() << LL_ENDL;
|
|
if (getSetByName(item->getName()))
|
|
{
|
|
LLNotificationsUtil::add("AOImportSetAlreadyExists", LLSD());
|
|
return false;
|
|
}
|
|
|
|
if (!gAgent.allowOperation(PERM_COPY, item->getPermissions(), GP_OBJECT_MANIPULATE) && !gAgent.isGodlike())
|
|
{
|
|
LLNotificationsUtil::add("AOImportPermissionDenied", LLSD());
|
|
return false;
|
|
}
|
|
|
|
if (item->getAssetUUID().notNull())
|
|
{
|
|
// create the new set with the folder UUID where the notecard is in, so we can reference it
|
|
// in the notecard reader, this will later be cleared to make room for the real #AO subfolder
|
|
mImportSet = new AOSet(item->getParentUUID());
|
|
mImportSet->setName(item->getName());
|
|
|
|
LLUUID* newUUID = new LLUUID(item->getAssetUUID());
|
|
const LLHost sourceSim;
|
|
|
|
gAssetStorage->getInvItemAsset(
|
|
sourceSim,
|
|
gAgentID,
|
|
gAgentSessionID,
|
|
item->getPermissions().getOwner(),
|
|
LLUUID::null,
|
|
item->getUUID(),
|
|
item->getAssetUUID(),
|
|
item->getType(),
|
|
&onNotecardLoadComplete,
|
|
(void*) newUUID,
|
|
true
|
|
);
|
|
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// static
|
|
void AOEngine::onNotecardLoadComplete(const LLUUID& assetUUID, LLAssetType::EType type,
|
|
void* userdata, S32 status, LLExtStat extStatus)
|
|
{
|
|
if (status != LL_ERR_NOERR)
|
|
{
|
|
// AOImportDownloadFailed
|
|
LLNotificationsUtil::add("AOImportDownloadFailed", LLSD());
|
|
// NULL tells the importer to cancel all operations and free the import set memory
|
|
AOEngine::instance().parseNotecard(nullptr);
|
|
return;
|
|
}
|
|
LL_DEBUGS("AOEngine") << "Downloading import notecard complete." << LL_ENDL;
|
|
|
|
LLFileSystem file(assetUUID, type, LLFileSystem::READ);
|
|
|
|
S32 notecardSize = file.getSize();
|
|
char* buffer = new char[notecardSize + 1];
|
|
buffer[notecardSize] = 0;
|
|
|
|
if (file.read((U8*)buffer, notecardSize))
|
|
{
|
|
AOEngine::instance().parseNotecard(buffer);
|
|
}
|
|
else
|
|
{
|
|
delete[] buffer;
|
|
}
|
|
}
|
|
|
|
void AOEngine::parseNotecard(const char* buffer)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "parsing import notecard" << LL_ENDL;
|
|
|
|
bool isValid = false;
|
|
|
|
if (!buffer)
|
|
{
|
|
LL_WARNS("AOEngine") << "buffer==NULL - aborting import" << LL_ENDL;
|
|
// NOTE: cleanup is always the same, needs streamlining
|
|
delete mImportSet;
|
|
mImportSet = nullptr;
|
|
mUpdatedSignal();
|
|
return;
|
|
}
|
|
|
|
std::string text(buffer);
|
|
delete[] buffer;
|
|
|
|
std::vector<std::string> lines;
|
|
LLStringUtil::getTokens(text, lines, "\n");
|
|
|
|
auto it = std::find_if(lines.begin(), lines.end(), [](const std::string& line) {
|
|
return line.find("Text length ") == 0;
|
|
});
|
|
|
|
if (it == lines.end()) {
|
|
// Line not found
|
|
LLNotificationsUtil::add("AOImportNoText", LLSD());
|
|
delete mImportSet;
|
|
mImportSet = nullptr;
|
|
mUpdatedSignal();
|
|
return;
|
|
}
|
|
|
|
// Line found, 'it' points to the found element
|
|
std::size_t found = std::distance(lines.begin(), it) + 1;
|
|
|
|
// mImportSet->getInventoryUUID() right now contains the folder UUID where the notecard is in
|
|
LLViewerInventoryCategory* importCategory = gInventory.getCategory(mImportSet->getInventoryUUID());
|
|
if (!importCategory)
|
|
{
|
|
LLNotificationsUtil::add("AOImportNoFolder", LLSD());
|
|
delete mImportSet;
|
|
mImportSet = nullptr;
|
|
mUpdatedSignal();
|
|
return;
|
|
}
|
|
|
|
std::map<std::string, LLUUID> animationMap;
|
|
LLInventoryModel::cat_array_t* dummy;
|
|
LLInventoryModel::item_array_t* items;
|
|
|
|
gInventory.getDirectDescendentsOf(mImportSet->getInventoryUUID(), dummy, items);
|
|
for (auto& item : *items)
|
|
{
|
|
animationMap[item->getName()] = item->getUUID();
|
|
LL_DEBUGS("AOEngine") << "animation " << item->getName() << " has inventory UUID " << animationMap[item->getName()] << LL_ENDL;
|
|
}
|
|
|
|
// [ State ]Anim1|Anim2|Anim3
|
|
for (auto index = found; index < lines.size(); ++index)
|
|
{
|
|
std::string line = lines[index];
|
|
|
|
// cut off the trailing } of a notecard asset's text portion in the last line
|
|
if (index == lines.size() - 1)
|
|
{
|
|
line = line.substr(0, line.size() - 1);
|
|
}
|
|
|
|
LLStringUtil::trim(line);
|
|
|
|
if (line.empty())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (line[0] == '#') // <ND/> FIRE-3801; skip comments to reduce spam to local chat.
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (line.find("[") != 0)
|
|
{
|
|
LLSD args;
|
|
args["LINE"] = (S32)index;
|
|
LLNotificationsUtil::add("AOImportNoStatePrefix", args);
|
|
continue;
|
|
}
|
|
|
|
if (line.find("]") == std::string::npos)
|
|
{
|
|
LLSD args;
|
|
args["LINE"] = (S32)index;
|
|
LLNotificationsUtil::add("AOImportNoValidDelimiter", args);
|
|
continue;
|
|
}
|
|
auto endTag = line.find("]");
|
|
|
|
std::string stateName = line.substr(1, endTag - 1);
|
|
LLStringUtil::trim(stateName);
|
|
|
|
AOSet::AOState* newState = mImportSet->getStateByName(stateName);
|
|
if (!newState)
|
|
{
|
|
LLSD args;
|
|
args["NAME"] = stateName;
|
|
LLNotificationsUtil::add("AOImportStateNameNotFound", args);
|
|
continue;
|
|
}
|
|
|
|
std::string animationLine = line.substr(endTag + 1);
|
|
std::vector<std::string> stepList;
|
|
LLStringUtil::getTokens(animationLine, stepList, "|");
|
|
|
|
for (auto stepIndex = 0; stepIndex < stepList.size(); ++stepIndex)
|
|
{
|
|
std::vector<std::string> memberList;
|
|
LLStringUtil::getTokens(stepList[stepIndex], memberList, ",");
|
|
AOSet::AOAnimationStep step;
|
|
step.mSortOrder = static_cast<S32>(newState->mSteps.size());
|
|
for (auto memberIndex = 0; memberIndex < memberList.size(); ++memberIndex)
|
|
{
|
|
std::string animationName = memberList[memberIndex];
|
|
LLStringUtil::trim(animationName);
|
|
if (animationName.empty())
|
|
{
|
|
continue;
|
|
}
|
|
AOSet::AOAnimation animation;
|
|
animation.mName = animationName;
|
|
animation.mInventoryUUID = animationMap[animation.mName];
|
|
if (animation.mInventoryUUID.isNull())
|
|
{
|
|
LLSD args;
|
|
args["NAME"] = animation.mName;
|
|
LLNotificationsUtil::add("AOImportAnimationNotFound", args);
|
|
continue;
|
|
}
|
|
animation.mSortOrder = static_cast<S32>(step.mMembers.size());
|
|
step.mMembers.emplace_back(std::move(animation));
|
|
}
|
|
|
|
if (step.mMembers.empty())
|
|
{
|
|
continue;
|
|
}
|
|
step.mIsGroup = step.mMembers.size() > 1;
|
|
step.mName = step.mIsGroup ? nextGroupName(newState) : step.mMembers.front().mName;
|
|
newState->mSteps.emplace_back(std::move(step));
|
|
isValid = true;
|
|
}
|
|
}
|
|
|
|
if (!isValid)
|
|
{
|
|
LLNotificationsUtil::add("AOImportInvalid", LLSD());
|
|
// NOTE: cleanup is always the same, needs streamlining
|
|
delete mImportSet;
|
|
mImportSet = nullptr;
|
|
mUpdatedSignal();
|
|
return;
|
|
}
|
|
|
|
// clear out set UUID so processImport() knows we need to create a new folder for it
|
|
mImportSet->setInventoryUUID(LLUUID::null);
|
|
mTimerCollection.enableImportTimer(true);
|
|
mImportRetryCount = 0;
|
|
processImport(false);
|
|
}
|
|
|
|
void AOEngine::processImportState(AOSet::AOState* state)
|
|
{
|
|
if (state->mInventoryUUID.isNull())
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (S32 stepIndex = static_cast<S32>(state->mSteps.size()) - 1; stepIndex >= 0; --stepIndex)
|
|
{
|
|
AOSet::AOAnimationStep step = std::move(state->mSteps[stepIndex]);
|
|
state->mSteps.erase(state->mSteps.begin() + stepIndex);
|
|
|
|
if (!step.mIsGroup)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "linking animation " << step.mName << LL_ENDL;
|
|
if (!createAnimationLink(state->mInventoryUUID, gInventory.getItem(step.mMembers.front().mInventoryUUID)))
|
|
{
|
|
LLSD args;
|
|
args["NAME"] = step.mName;
|
|
LLNotificationsUtil::add("AOImportLinkFailed", args);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
std::string groupName = sanitiseFolderName(step.mName, "Group") + llformat(":OR%d", step.mSortOrder);
|
|
std::vector<AOSet::AOAnimation> members = std::move(step.mMembers);
|
|
|
|
LL_DEBUGS("AOEngine") << "creating group subfolder " << groupName << LL_ENDL;
|
|
gInventory.createNewCategory(state->mInventoryUUID, LLFolderType::FT_NONE, groupName, [this, members](const LLUUID& new_cat_id)
|
|
{
|
|
for (S32 memberIndex = static_cast<S32>(members.size()) - 1; memberIndex >= 0; --memberIndex)
|
|
{
|
|
if (!createAnimationLink(new_cat_id, gInventory.getItem(members[memberIndex].mInventoryUUID)))
|
|
{
|
|
LLSD args;
|
|
args["NAME"] = members[memberIndex].mName;
|
|
LLNotificationsUtil::add("AOImportLinkFailed", args);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
for (S32 trackIndex = static_cast<S32>(state->mTracks.size()) - 1; trackIndex >= 0; --trackIndex)
|
|
{
|
|
AOSet::AOTrack track = std::move(state->mTracks[trackIndex]);
|
|
state->mTracks.erase(state->mTracks.begin() + trackIndex);
|
|
|
|
track.mInventoryUUID.setNull();
|
|
std::string trackName = getTrackFolderName(track);
|
|
std::vector<AOSet::AOAnimation> members = std::move(track.mAnimations);
|
|
|
|
LL_DEBUGS("AOEngine") << "creating track subfolder " << trackName << LL_ENDL;
|
|
gInventory.createNewCategory(state->mInventoryUUID, LLFolderType::FT_NONE, trackName, [this, members](const LLUUID& new_cat_id)
|
|
{
|
|
for (S32 memberIndex = static_cast<S32>(members.size()) - 1; memberIndex >= 0; --memberIndex)
|
|
{
|
|
if (!createAnimationLink(new_cat_id, gInventory.getItem(members[memberIndex].mInventoryUUID)))
|
|
{
|
|
LLSD args;
|
|
args["NAME"] = members[memberIndex].mName;
|
|
LLNotificationsUtil::add("AOImportLinkFailed", args);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
void AOEngine::processImport(bool from_timer)
|
|
{
|
|
if (mImportSet->getInventoryUUID().isNull())
|
|
{
|
|
// create new inventory folder for this AO set, the next timer tick should pick it up
|
|
addSet(getSetFolderName(mImportSet), [this](const LLUUID& new_cat_id)
|
|
{
|
|
mImportSet->setInventoryUUID(new_cat_id);
|
|
}, false);
|
|
|
|
mImportRetryCount++;
|
|
|
|
// if it takes this long to create a new inventoey category, there might be something wrong,
|
|
// so give some user feedback at first
|
|
if (mImportRetryCount == 2)
|
|
{
|
|
LLSD args;
|
|
args["NAME"] = mImportSet->getName();
|
|
LLNotificationsUtil::add("AOImportRetryCreateSet", args);
|
|
return;
|
|
}
|
|
// by now there clearly is something wrong, so stop trying
|
|
else if (mImportRetryCount == 5)
|
|
{
|
|
// NOTE: cleanup is the same as at the end of this function. Needs streamlining.
|
|
LLSD args;
|
|
args["NAME"] = mImportSet->getName();
|
|
mTimerCollection.enableImportTimer(false);
|
|
delete mImportSet;
|
|
mImportSet = nullptr;
|
|
mUpdatedSignal();
|
|
LLNotificationsUtil::add("AOImportAbortCreateSet", args);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
bool allComplete = true;
|
|
for (S32 index = 0; index < AOSet::AOSTATES_MAX; ++index)
|
|
{
|
|
AOSet::AOState* state = mImportSet->getState(index);
|
|
if (!state->mSteps.empty() || !state->mTracks.empty())
|
|
{
|
|
allComplete = false;
|
|
LL_DEBUGS("AOEngine") << "state " << state->mName << " still has animations to link." << LL_ENDL;
|
|
|
|
if (state->mInventoryUUID.isNull())
|
|
{
|
|
gInventory.createNewCategory(mImportSet->getInventoryUUID(), LLFolderType::FT_NONE, getStateFolderName(state), [this, state](const LLUUID& new_cat_id)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "new_cat_id: " << new_cat_id << LL_ENDL;
|
|
state->mInventoryUUID = new_cat_id;
|
|
processImportState(state);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
processImportState(state);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (allComplete)
|
|
{
|
|
mTimerCollection.enableImportTimer(false);
|
|
mOldImportSets.push_back(mImportSet); //<ND/> FIRE-3801; Cannot delete here, or LLInstanceTracker gets upset. Just remember and delete mOldImportSets once we can.
|
|
mImportSet = nullptr;
|
|
reload(from_timer);
|
|
LLNotificationsUtil::add("AOImportComplete");
|
|
}
|
|
}
|
|
|
|
const LLUUID& AOEngine::getAOFolder() const
|
|
{
|
|
return mAOFolder;
|
|
}
|
|
|
|
void AOEngine::onRegionChange()
|
|
{
|
|
// do nothing if the AO is off
|
|
if (!mEnabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// catch errors without crashing
|
|
if (!mCurrentSet)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Current set was NULL" << LL_ENDL;
|
|
return;
|
|
}
|
|
|
|
reassertAlwaysAnimations(false);
|
|
|
|
// sitting needs special attention
|
|
if (mLastMotion == ANIM_AGENT_SIT)
|
|
{
|
|
// do nothing if sit overrides was disabled
|
|
if (!mCurrentSet->getSitOverride())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// do nothing if the last overridden motion wasn't a sit.
|
|
// happens when sit override is enabled but there were no
|
|
// sit animations added to the set yet
|
|
if (mLastOverriddenMotion != ANIM_AGENT_SIT)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AOSet::AOState* state = mCurrentSet->getState(AOSet::Sitting);
|
|
if (!state)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// do nothing if no AO animation is playing (e.g. smart sit cancel)
|
|
if (state->mCurrentAnimationIDs.empty())
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
// restart current animation on region crossing
|
|
gAgent.sendAnimationRequest(mLastMotion, ANIM_REQUEST_START);
|
|
}
|
|
|
|
void AOEngine::updatePersistedStateAnimations()
|
|
{
|
|
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
|
|
AOSitCancelTimer::AOSitCancelTimer()
|
|
: LLEventTimer(0.1f),
|
|
mTickCount(0)
|
|
{
|
|
mEventTimer.stop();
|
|
}
|
|
|
|
void AOSitCancelTimer::oneShot()
|
|
{
|
|
mTickCount = 0;
|
|
mEventTimer.start();
|
|
}
|
|
|
|
void AOSitCancelTimer::stop()
|
|
{
|
|
mEventTimer.stop();
|
|
}
|
|
|
|
bool AOSitCancelTimer::tick()
|
|
{
|
|
mTickCount++;
|
|
AOEngine::instance().checkSitCancel();
|
|
if (mTickCount == 10)
|
|
{
|
|
mEventTimer.stop();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
|
|
AOTimerCollection::AOTimerCollection()
|
|
: LLEventTimer(INVENTORY_POLLING_INTERVAL),
|
|
mInventoryTimer(true),
|
|
mSettingsTimer(false),
|
|
mReloadTimer(false),
|
|
mImportTimer(false)
|
|
{
|
|
updateTimers();
|
|
}
|
|
|
|
bool AOTimerCollection::tick()
|
|
{
|
|
if (mInventoryTimer)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Inventory timer tick()" << LL_ENDL;
|
|
AOEngine::instance().tick();
|
|
}
|
|
if (mSettingsTimer)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Settings timer tick()" << LL_ENDL;
|
|
AOEngine::instance().saveSettings();
|
|
}
|
|
if (mReloadTimer)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Reload timer tick()" << LL_ENDL;
|
|
AOEngine::instance().reload(true);
|
|
}
|
|
if (mImportTimer)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "Import timer tick()" << LL_ENDL;
|
|
AOEngine::instance().processImport(true);
|
|
}
|
|
|
|
// always return false or the LLEventTimer will be deleted -> crash
|
|
return false;
|
|
}
|
|
|
|
void AOTimerCollection::enableInventoryTimer(bool enable)
|
|
{
|
|
mInventoryTimer = enable;
|
|
updateTimers();
|
|
}
|
|
|
|
void AOTimerCollection::enableSettingsTimer(bool enable)
|
|
{
|
|
mSettingsTimer = enable;
|
|
updateTimers();
|
|
}
|
|
|
|
void AOTimerCollection::enableReloadTimer(bool enable)
|
|
{
|
|
mReloadTimer = enable;
|
|
updateTimers();
|
|
}
|
|
|
|
void AOTimerCollection::enableImportTimer(bool enable)
|
|
{
|
|
mImportTimer = enable;
|
|
updateTimers();
|
|
}
|
|
|
|
void AOTimerCollection::updateTimers()
|
|
{
|
|
if (!mInventoryTimer && !mSettingsTimer && !mReloadTimer && !mImportTimer)
|
|
{
|
|
LL_DEBUGS("AOEngine") << "no timer needed, stopping internal timer." << LL_ENDL;
|
|
mEventTimer.stop();
|
|
}
|
|
else
|
|
{
|
|
LL_DEBUGS("AOEngine") << "timer needed, starting internal timer." << LL_ENDL;
|
|
mEventTimer.start();
|
|
}
|
|
}
|
|
|
|
AOTrackTimer::AOTrackTimer(S32 stateEnum, S32 trackIndex, F32 period)
|
|
: LLEventTimer(period),
|
|
mStateEnum(stateEnum),
|
|
mTrackIndex(trackIndex)
|
|
{
|
|
}
|
|
|
|
bool AOTrackTimer::tick()
|
|
{
|
|
AOEngine::instance().trackTimeout(mStateEnum, mTrackIndex);
|
|
// never self-delete, the engine owns this timer through mActiveTrackTimers
|
|
return false;
|
|
}
|