/** * @file animationexplorer.cpp * @brief Animation Explorer floater implementation * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2010, Linden Research, Inc. * Copyright (C) 2013, 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 * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ #include "llviewerprecompiledheaders.h" #include "animationexplorer.h" #include "indra_constants.h" // for MASK_ALT etc. #include "message.h" // for gMessageSystem //#include "stdenums.h" // for ADD_TOP #include "llagent.h" // for gAgent #include "llanimationstates.h" #include "llbutton.h" #include "llcachename.h" // for gCacheName #include "llcheckboxctrl.h" #include "llfloater.h" #include "llfloaterreg.h" #include "llkeyframefallmotion.h" #include "llrect.h" #include "llscrolllistctrl.h" #include "lltimer.h" #include "lltoolmgr.h" // for MASK_ORBIT etc. #include "lltrans.h" #include "lluuid.h" #include "llview.h" #include "llviewerobjectlist.h" #include "llviewerregion.h" #include "llviewerwindow.h" // for gViewerWindow #include "llvoavatar.h" #include "llvoavatarself.h" // for gAgentAvatarp #include "llavatarnamecache.h" #include "fsassetblacklist.h" constexpr S32 MAX_ANIMATIONS = 100; // -------------------------------------------------------------------------- RecentAnimationList::RecentAnimationList() : LLSingleton() { } RecentAnimationList::~RecentAnimationList() { } void RecentAnimationList::addAnimation(const LLUUID& id, const LLUUID& playedBy) { AnimationEntry entry; entry.animationID = id; entry.playedBy = playedBy; entry.time = LLTimer::getElapsedSeconds(); AnimationExplorer* explorer = LLFloaterReg::findTypedInstance("animation_explorer"); // only remember animation when it wasn't played by ourselves or the explorer window is open, // so the list doesn't get polluted if (playedBy != gAgentAvatarp->getID() || explorer) { mAnimationList.push_back(entry); // only keep a certain number of entries if (mAnimationList.size() > MAX_ANIMATIONS) { mAnimationList.pop_front(); } } // if the animation explorer floater is open, send this animation over immediately if (explorer) { explorer->addAnimation(id, playedBy, LLTimer::getElapsedSeconds()); } } void RecentAnimationList::requestList(AnimationExplorer* explorer) { if (explorer) { // send the list of recent animations to the given animation explorer floater for (std::deque::iterator iter = mAnimationList.begin(); iter != mAnimationList.end(); ++iter) { AnimationEntry entry = *iter; explorer->addAnimation(entry.animationID, entry.playedBy, entry.time); } } } // -------------------------------------------------------------------------- AnimationExplorer::AnimationExplorer(const LLSD& key) : LLFloater(key), mPreviewCtrl(nullptr), mLastMouseX(0), mLastMouseY(0) { } AnimationExplorer::~AnimationExplorer() { mAnimationPreview = nullptr; for (const auto& cb : mAvatarNameCacheConnections) { if (cb.second.connected()) { cb.second.disconnect(); } } mAvatarNameCacheConnections.clear(); } void AnimationExplorer::startMotion(const LLUUID& motionID) { if (!mAnimationPreview) { return; } LLVOAvatar* avatarp = mAnimationPreview->getDummyAvatar(); avatarp->deactivateAllMotions(); avatarp->startMotion(ANIM_AGENT_STAND, 0.0f); if (motionID.notNull()) { avatarp->startMotion(motionID, 0.0f); } } BOOL AnimationExplorer::postBuild() { mAnimationScrollList = getChild("animation_list"); mStopButton = getChild("stop_btn"); mBlacklistButton = getChild("blacklist_btn"); mStopAndRevokeButton = getChild("stop_and_revoke_btn"); mNoOwnedAnimationsCheckBox = getChild("no_owned_animations_check"); mAnimationScrollList->setCommitCallback(boost::bind(&AnimationExplorer::onSelectAnimation, this)); mStopButton->setCommitCallback(boost::bind(&AnimationExplorer::onStopPressed, this)); mBlacklistButton->setCommitCallback(boost::bind(&AnimationExplorer::onBlacklistPressed, this)); mStopAndRevokeButton->setCommitCallback(boost::bind(&AnimationExplorer::onStopAndRevokePressed, this)); mNoOwnedAnimationsCheckBox->setCommitCallback(boost::bind(&AnimationExplorer::onOwnedCheckToggled, this)); mPreviewCtrl = findChild("animation_preview"); if (mPreviewCtrl) { if (isAgentAvatarValid()) { mAnimationPreview = new LLPreviewAnimation(mPreviewCtrl->getRect().getWidth(), mPreviewCtrl->getRect().getHeight()); mAnimationPreview->setZoom(2.0f); startMotion(LLUUID::null); } } else { LL_WARNS("AnimationExplorer") << "Could not find animation preview control to place animation texture" << LL_ENDL; } // request list of recent animations update(); return TRUE; } void AnimationExplorer::onSelectAnimation() { LLScrollListItem* item = mAnimationScrollList->getFirstSelected(); if (!item) { return; } S32 column = mAnimationScrollList->getColumn("animation_id")->mIndex; mCurrentAnimationID = item->getColumn(column)->getValue().asUUID(); column = mAnimationScrollList->getColumn("object_id")->mIndex; mCurrentObject = item->getColumn(column)->getValue().asUUID(); startMotion(mCurrentAnimationID); } void AnimationExplorer::onStopPressed() { if (mCurrentAnimationID.notNull()) { gAgentAvatarp->stopMotion(mCurrentAnimationID); gAgent.sendAnimationRequest(mCurrentAnimationID, ANIM_REQUEST_STOP); } } void AnimationExplorer::onBlacklistPressed() { onStopPressed(); LLScrollListItem* item = mAnimationScrollList->getFirstSelected(); if (!item) { return; } S32 column = mAnimationScrollList->getColumn("played_by")->mIndex; std::string region_name{}; if (gAgent.getRegion()) { region_name = gAgent.getRegion()->getName(); } FSAssetBlacklist::getInstance()->addNewItemToBlacklist(mCurrentAnimationID, item->getColumn(column)->getValue(), region_name, LLAssetType::AT_ANIMATION); } void AnimationExplorer::onStopAndRevokePressed() { onStopPressed(); if (mCurrentObject.notNull()) { if (LLViewerObject* vo = gObjectList.findObject(mCurrentObject); vo) { gAgentAvatarp->revokePermissionsOnObject(vo); } } } void AnimationExplorer::onOwnedCheckToggled() { update(); updateList(LLTimer::getElapsedSeconds()); } void AnimationExplorer::draw() { LLFloater::draw(); LLRect r = mPreviewCtrl->getRect(); if (mAnimationPreview) { mAnimationPreview->requestUpdate(); gGL.color3f(1.0f, 1.0f, 1.0f); gGL.getTexUnit(0)->bind(mAnimationPreview); gGL.begin(LLRender::TRIANGLES); { gGL.texCoord2f(0.0f, 1.0f); gGL.vertex2i(r.mLeft, r.mTop); gGL.texCoord2f(0.0f, 0.0f); gGL.vertex2i(r.mLeft, r.mBottom); gGL.texCoord2f(1.0f, 0.0f); gGL.vertex2i(r.mRight, r.mBottom); gGL.texCoord2f(0.0f, 1.0f); gGL.vertex2i(r.mLeft, r.mTop); gGL.texCoord2f(1.0f, 0.0f); gGL.vertex2i(r.mRight, r.mBottom); gGL.texCoord2f(1.0f, 1.0f); gGL.vertex2i(r.mRight, r.mTop); } gGL.end(); gGL.getTexUnit(0)->unbind(LLTexUnit::TT_TEXTURE); } // update tiems and "Still playing" status in the list once every few seconds static F64 last_update = 0.0; F64 time = LLTimer::getElapsedSeconds(); if (time - last_update > 5.0) { last_update = time; updateList(time); } } void AnimationExplorer::update() { // stop playing preview animations when reloading the list startMotion(LLUUID::null); mAnimationScrollList->deleteAllItems(); RecentAnimationList::instance().requestList(this); } void AnimationExplorer::updateList(F64 current_timestamp) { S32 played_column = mAnimationScrollList->getColumn("played")->mIndex; S32 timestamp_column = mAnimationScrollList->getColumn("timestamp")->mIndex; S32 priority_column = mAnimationScrollList->getColumn("priority")->mIndex; S32 object_id_column = mAnimationScrollList->getColumn("object_id")->mIndex; S32 anim_id_column = mAnimationScrollList->getColumn("animation_id")->mIndex; // go through the full animation scroll list std::vector items = mAnimationScrollList->getAllData(); for (std::vector::iterator list_iter = items.begin(); list_iter != items.end(); ++list_iter) { LLScrollListItem* item = *list_iter; // get a pointer to the "Played" column text LLScrollListText* played_text = dynamic_cast(item->getColumn(played_column)); // get the object ID from the list LLUUID object_id = item->getColumn(object_id_column)->getValue().asUUID(); LLUUID anim_id = item->getColumn(anim_id_column)->getValue().asUUID(); // assume this animation is not running first bool is_running = false; // go through the list of playing animations to find out if this animation played by // this object is still running for (const auto& [anim_object_id, anim_anim_id] : gAgentAvatarp->mAnimationSources) { // object and animation found if (anim_object_id == object_id && anim_anim_id == anim_id) { // set text to "Still playing" and break out of this loop played_text->setText(LLTrans::getString("animation_explorer_still_playing")); is_running = true; break; } } // animation was not found to be running if (!is_running) { // get timestamp when this animation was started F64 timestamp = item->getColumn(timestamp_column)->getValue().asReal(); // update text to show the number of seconds ago when this animation was started LLStringUtil::format_map_t args; args["SECONDS"] = llformat("%d", (S32) (current_timestamp - timestamp)); played_text->setText(LLTrans::getString("animation_explorer_seconds_ago", args)); } std::string prio_text = LLTrans::getString("animation_explorer_unknown_priority"); if (LLKeyframeMotion* motion = dynamic_cast(gAgentAvatarp->findMotion(anim_id)); motion) { prio_text = llformat("%d", (S32)motion->getPriority()); } dynamic_cast(item->getColumn(priority_column))->setText(prio_text); } } void AnimationExplorer::addAnimation(const LLUUID& id, const LLUUID& played_by, F64 time) { // don't add animations that are played by ourselves when the filter box is checked if (played_by == gAgentAvatarp->getID()) { if (mNoOwnedAnimationsCheckBox->getValue().asBoolean()) { return; } } // set object name to UUID at first std::string playedByName = played_by.asString(); // find out if the object is still in reach if (LLViewerObject* vo = gObjectList.findObject(played_by); vo) { // if it was an avatar, get the name here if (vo->isAvatar()) { LLAvatarName av_name; if (LLAvatarNameCache::get(played_by, &av_name)) { playedByName = av_name.getCompleteName(); } else { if (mAvatarNameCacheConnections.find(played_by) != mAvatarNameCacheConnections.end()) { boost::signals2::connection cb_connection = LLAvatarNameCache::get(played_by, boost::bind(&AnimationExplorer::onAvatarNameCallback, this, _1, _2)); mAvatarNameCacheConnections.insert(std::make_pair(played_by, cb_connection)); } playedByName = LLTrans::getString("AvatarNameWaiting"); } } // not an avatar, do a lookup by UUID else { // find out if we know the name to this UUID already std::map::iterator iter = mKnownIDs.find(played_by); // if we don't know it yet, start a lookup if (iter == mKnownIDs.end()) { // if we are not already looking up this object's name, send a request out if (std::find(mRequestedIDs.begin(), mRequestedIDs.end(), played_by) == mRequestedIDs.end()) { // remember which object names we already requested mRequestedIDs.push_back(played_by); LLMessageSystem* msg = gMessageSystem; msg->newMessageFast(_PREHASH_ObjectSelect); msg->nextBlockFast(_PREHASH_AgentData); msg->addUUIDFast(_PREHASH_AgentID, gAgentID); msg->addUUIDFast(_PREHASH_SessionID, gAgentSessionID); msg->nextBlockFast(_PREHASH_ObjectData); msg->addU32Fast(_PREHASH_ObjectLocalID, vo->getLocalID()); msg->sendReliable(gAgentAvatarp->getRegion()->getHost()); msg->newMessageFast(_PREHASH_ObjectDeselect); msg->nextBlockFast(_PREHASH_AgentData); msg->addUUIDFast(_PREHASH_AgentID, gAgentID); msg->addUUIDFast(_PREHASH_SessionID, gAgentSessionID); msg->nextBlockFast(_PREHASH_ObjectData); msg->addU32Fast(_PREHASH_ObjectLocalID, vo->getLocalID()); msg->sendReliable(gAgentAvatarp->getRegion()->getHost()); } } else { // we know the name already playedByName = mKnownIDs[played_by]; } } } // insert the item into the scroll list LLSD item; item["columns"][0]["column"] = "played_by"; item["columns"][0]["value"] = playedByName; item["columns"][1]["column"] = "played"; item["columns"][1]["value"] = LLTrans::getString("animation_explorer_still_playing"); item["columns"][2]["column"] = "priority"; item["columns"][2]["value"] = LLTrans::getString("animation_explorer_unknown_priority"); item["columns"][3]["column"] = "timestamp"; item["columns"][3]["value"] = time; item["columns"][4]["column"] = "animation_id"; item["columns"][4]["value"] = id; item["columns"][5]["column"] = "object_id"; item["columns"][5]["value"] = played_by; mAnimationScrollList->addElement(item, ADD_TOP); } void AnimationExplorer::onAvatarNameCallback(const LLUUID& id, const LLAvatarName& av_name) { if (auto iter = mAvatarNameCacheConnections.find(id); iter != mAvatarNameCacheConnections.end()) { if (iter->second.connected()) { iter->second.disconnect(); } mAvatarNameCacheConnections.erase(iter); } updateListEntry(id, av_name.getCompleteName()); } void AnimationExplorer::requestNameCallback(LLMessageSystem* msg) { // if we weren't looking for any IDs, ignore this callback if (mRequestedIDs.empty()) { return; } // we might have received more than one answer in one block S32 num = msg->getNumberOfBlocksFast(_PREHASH_ObjectData); for (S32 index = 0; index < num; ++index) { LLUUID object_id; msg->getUUIDFast(_PREHASH_ObjectData, _PREHASH_ObjectID, object_id, index); // if this is one of the objects we were looking for, process the data if (auto iter = std::find(mRequestedIDs.begin(), mRequestedIDs.end(), object_id); iter != mRequestedIDs.end()) { // get the name of the object std::string object_name; msg->getStringFast(_PREHASH_ObjectData, _PREHASH_Name, object_name, index); // remove the object from the lookup list and add it to the known names list mRequestedIDs.erase(iter); mKnownIDs[object_id] = object_name; updateListEntry(object_id, object_name); } } } void AnimationExplorer::updateListEntry(const LLUUID& id, const std::string& name) { S32 object_id_column = mAnimationScrollList->getColumn("object_id")->mIndex; S32 played_by_column = mAnimationScrollList->getColumn("played_by")->mIndex; // find all scroll list entries with this object UUID and update the names there for (LLScrollListItem* item : mAnimationScrollList->getAllData()) { const LLUUID& list_object_id = item->getColumn(object_id_column)->getValue().asUUID(); if (id == list_object_id) { LLScrollListText* played_by_text = (LLScrollListText*)item->getColumn(played_by_column); played_by_text->setText(name); } } } // Copied from llfloaterbvhpreview.cpp BOOL AnimationExplorer::handleMouseDown(S32 x, S32 y, MASK mask) { if (mPreviewCtrl && mPreviewCtrl->getRect().pointInRect(x, y)) { bringToFront(x, y); gFocusMgr.setMouseCapture(this); gViewerWindow->hideCursor(); mLastMouseX = x; mLastMouseY = y; return TRUE; } return LLFloater::handleMouseDown(x, y, mask); } // Copied from llfloaterbvhpreview.cpp BOOL AnimationExplorer::handleMouseUp(S32 x, S32 y, MASK mask) { gFocusMgr.setMouseCapture(FALSE); gViewerWindow->showCursor(); return LLFloater::handleMouseUp(x, y, mask); } // (Almost) Copied from llfloaterbvhpreview.cpp BOOL AnimationExplorer::handleHover(S32 x, S32 y, MASK mask) { if (!mPreviewCtrl || !mAnimationPreview || !mPreviewCtrl->getRect().pointInRect(x, y)) { return LLFloater::handleHover(x, y, mask); } MASK local_mask = mask & ~MASK_ALT; if (mAnimationPreview && hasMouseCapture()) { if (local_mask == MASK_PAN) { // pan here mAnimationPreview->pan((F32)(x - mLastMouseX) * -0.005f, (F32)(y - mLastMouseY) * -0.005f); } else if (local_mask == MASK_ORBIT) { F32 yaw_radians = (F32)(x - mLastMouseX) * -0.01f; F32 pitch_radians = (F32)(y - mLastMouseY) * 0.02f; mAnimationPreview->rotate(yaw_radians, pitch_radians); } else { F32 yaw_radians = (F32)(x - mLastMouseX) * -0.01f; F32 zoom_amt = (F32)(y - mLastMouseY) * 0.02f; mAnimationPreview->rotate(yaw_radians, 0.f); mAnimationPreview->zoom(zoom_amt); } mAnimationPreview->requestUpdate(); LLUI::getInstance()->setMousePositionLocal(this, mLastMouseX, mLastMouseY); } else if (local_mask == MASK_ORBIT) { gViewerWindow->setCursor(UI_CURSOR_TOOLCAMERA); } else if (local_mask == MASK_PAN) { gViewerWindow->setCursor(UI_CURSOR_TOOLPAN); } else { gViewerWindow->setCursor(UI_CURSOR_TOOLZOOMIN); } return TRUE; } // (Almost) Copied from llfloaterbvhpreview.cpp BOOL AnimationExplorer::handleScrollWheel(S32 x, S32 y, S32 clicks) { if (mPreviewCtrl && mPreviewCtrl->getRect().pointInRect(x, y)) { mAnimationPreview->zoom((F32)clicks * -0.2f); mAnimationPreview->requestUpdate(); return TRUE; } return LLFloater::handleScrollWheel(x, y, clicks); } // Copied from llfloaterbvhpreview.cpp void AnimationExplorer::onMouseCaptureLost() { gViewerWindow->showCursor(); }