From 5b6f5fa5474e16c7af70005e30d280188e91f2f2 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 9 Jun 2026 19:18:51 -0400 Subject: [PATCH 1/8] Add Scene Explorer floater for browsing and filtering region content A filterable LLFolderView scene-graph tree of the current region for environment builders and region administrators: Region -> linksets -> child prims (kept in link order), plus Avatars -> attachment-point folders -> attachments. Animesh objects list under their wearer; control and UI avatars are excluded. - ALObjectPropertiesCache: session cache of bulk ObjectProperties data, fed by a batched ObjectSelect/ObjectDeselect probe (throttled, deduped, retry-capped, skips the user's live selection); handlers forward to LLSelectMgr and in-flight tracking suppresses its node-less reply warning per-id - Idle-driven lifecycle: time-sliced discovery/widget build (~6ms/frame) and time-sliced filtering, so dense regions populate without stalls - Filters (persisted): text over name/desc/UUID, owner, scripted/light/ particles flags, radius (re-armed as the agent moves); sorts: distance/ name/land impact/triangles/type - Actions: focus (zoom-to-object), edit, inspect, teleport, copy id; configurable double-click activation; derendered objects excluded via ALDerenderList - fetchObjectCostsCoro: chunk GetObjectCost requests to the cap limit and retire pending ids if the coroutine is torn down mid-sequence Co-Authored-By: Claude Opus 4.8 (1M context) --- indra/newview/CMakeLists.txt | 6 + indra/newview/alderenderlist.h | 2 +- indra/newview/alfloatersceneexplorer.cpp | 1106 +++++++++++++++++ indra/newview/alfloatersceneexplorer.h | 127 ++ indra/newview/alobjectproperties.cpp | 269 ++++ indra/newview/alobjectproperties.h | 203 +++ indra/newview/alsceneexplorermodel.cpp | 376 ++++++ indra/newview/alsceneexplorermodel.h | 296 +++++ .../newview/app_settings/settings_alchemy.xml | 77 ++ indra/newview/llselectmgr.cpp | 10 +- indra/newview/llstartup.cpp | 7 +- indra/newview/llviewerfloaterreg.cpp | 2 + indra/newview/llviewerobjectlist.cpp | 121 +- .../default/xui/en/floater_scene_explorer.xml | 178 +++ .../default/xui/en/menu_scene_explorer.xml | 36 + .../skins/default/xui/en/menu_viewer.xml | 11 + 16 files changed, 2776 insertions(+), 51 deletions(-) create mode 100644 indra/newview/alfloatersceneexplorer.cpp create mode 100644 indra/newview/alfloatersceneexplorer.h create mode 100644 indra/newview/alobjectproperties.cpp create mode 100644 indra/newview/alobjectproperties.h create mode 100644 indra/newview/alsceneexplorermodel.cpp create mode 100644 indra/newview/alsceneexplorermodel.h create mode 100644 indra/newview/skins/default/xui/en/floater_scene_explorer.xml create mode 100644 indra/newview/skins/default/xui/en/menu_scene_explorer.xml diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 3a81cdf207..9e1a5ab77d 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -170,9 +170,11 @@ set(viewer_SOURCE_FILES alfloaterprofilelegacy.cpp alfloaterprogressview.cpp alfloaterregiontracker.cpp + alfloatersceneexplorer.cpp alfloatertransactionlog.cpp alfloaterwebprofile.cpp allegacynotificationwellwindow.cpp + alobjectproperties.cpp alpanelaomini.cpp alpanelaopulldown.cpp alpanelavatarlegacy.cpp @@ -195,6 +197,7 @@ set(viewer_SOURCE_FILES alpanelsearchweb.cpp alpanelstreaminfo.cpp alpickitem.cpp + alsceneexplorermodel.cpp alstreaminfo.cpp altoolalign.cpp alunzip.cpp @@ -932,9 +935,11 @@ set(viewer_HEADER_FILES alfloaterprofilelegacy.h alfloaterprogressview.h alfloaterregiontracker.h + alfloatersceneexplorer.h alfloatertransactionlog.h alfloaterwebprofile.h allegacynotificationwellwindow.h + alobjectproperties.h alpanelaomini.h alpanelaopulldown.h alpanelavatarlegacy.h @@ -957,6 +962,7 @@ set(viewer_HEADER_FILES alpanelsearchweb.h alpanelstreaminfo.h alpickitem.h + alsceneexplorermodel.h alstreaminfo.h altoolalign.h alunzip.h diff --git a/indra/newview/alderenderlist.h b/indra/newview/alderenderlist.h index 9ce823ee04..16f228335c 100644 --- a/indra/newview/alderenderlist.h +++ b/indra/newview/alderenderlist.h @@ -151,6 +151,7 @@ class ALDerenderList : public LLSingleton using entry_list_t = std::list>; const entry_list_t& getEntries() const { return m_Entries; } + bool isDerendered(const LLUUID& idObject) { return getObjectEntry(idObject) != nullptr; } void removeObject(ALDerenderEntry::EEntryType eType, const LLUUID& idObject); void removeObjects(ALDerenderEntry::EEntryType eType, const uuid_vec_t& idsObject); protected: @@ -170,7 +171,6 @@ class ALDerenderList : public LLSingleton protected: ALDerenderObject* getObjectEntry(const LLUUID& idObject) /*const*/; ALDerenderObject* getObjectEntry(U64 idRegion, const LLUUID& idObject, U32 idRootLocal) /*const*/; - bool isDerendered(const LLUUID& idObject) /*const*/ { return getObjectEntry(idObject) != nullptr; } bool isDerendered(U64 idRegion, const LLUUID& idObject, U32 idRootLocal) /*const*/ { return getObjectEntry(idRegion, idObject, idRootLocal) != nullptr; } /* diff --git a/indra/newview/alfloatersceneexplorer.cpp b/indra/newview/alfloatersceneexplorer.cpp new file mode 100644 index 0000000000..30d0600a09 --- /dev/null +++ b/indra/newview/alfloatersceneexplorer.cpp @@ -0,0 +1,1106 @@ +/** + * @file alfloatersceneexplorer.cpp + * @brief Scene Explorer floater: a filterable scene-graph tree of region content + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ + +#include "llviewerprecompiledheaders.h" + +#include "alfloatersceneexplorer.h" + +#include "message.h" + +#include "llcallbacklist.h" +#include "lltimer.h" + +#include "llbutton.h" +#include "llclipboard.h" +#include "llcombobox.h" +#include "llfiltereditor.h" +#include "llfolderview.h" +#include "llfolderviewitem.h" +#include "llscrollcontainer.h" +#include "lltextbox.h" +#include "lltrans.h" +#include "lluicolortable.h" +#include "lluictrlfactory.h" + +#include "alderenderlist.h" +#include "alobjectproperties.h" +#include "llagent.h" +#include "llavatarname.h" +#include "llavatarnamecache.h" +#include "llfloaterreg.h" +#include "llfloatertools.h" +#include "llselectmgr.h" +#include "lltoolcomp.h" +#include "lltoolmgr.h" +#include "llviewercontrol.h" +#include "llviewerjointattachment.h" +#include "llviewermenu.h" +#include "llviewerobjectlist.h" +#include "llviewerregion.h" +#include "llvoavatar.h" + +namespace +{ + // Mirrors LLSelectMgr's per-packet object cap (llselectmgr.cpp). + constexpr S32 MAX_OBJECTS_PER_PACKET = 254; + + // Send a batched ObjectSelect / ObjectDeselect message by local id, mirroring + // the batching in LLSelectMgr (MAX_OBJECTS_PER_PACKET / isSendFullFast). + void sendObjectSelectionMessage(const char* message_name, const std::vector& local_ids, const LLHost& host) + { + LLMessageSystem* msg = gMessageSystem; + bool start_new_message = true; + S32 count = 0; + + for (U32 local_id : local_ids) + { + if (start_new_message) + { + msg->newMessageFast(message_name); + msg->nextBlockFast(_PREHASH_AgentData); + msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID()); + msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID()); + start_new_message = false; + } + + msg->nextBlockFast(_PREHASH_ObjectData); + msg->addU32Fast(_PREHASH_ObjectLocalID, local_id); + ++count; + + if (msg->isSendFullFast(nullptr) || count >= MAX_OBJECTS_PER_PACKET) + { + msg->sendReliable(host); + start_new_message = true; + count = 0; + } + } + + if (!start_new_message) + { + msg->sendReliable(host); + } + } + + // Deterministic synthetic UUID for an avatar's attachment-point grouping + // folder. Name-based (MD5) so it is stable across reconciles and will not + // collide with real object ids. + LLUUID attachmentPointKey(const LLUUID& avatar_id, S32 point_index) + { + LLUUID key; + key.generate(avatar_id.asString() + ":ap:" + std::to_string(point_index)); + return key; + } + + // 1-based position of 'child' within 'parent's maintained child list, which + // is the viewer's canonical link order (LLViewerObject::addChild appends in + // arrival/link order). Returns 0 if not a direct child. + S32 linkOrderInParent(LLViewerObject* parent, const LLViewerObject* child) + { + if (!parent || !child) + return 0; + S32 idx = 1; + for (const auto& c : parent->getChildren()) + { + if (c.get() == child) + return idx; + ++idx; + } + return 0; + } + + std::string placeholderName(ALSceneExplorerItem::EItemType type, const LLUUID& id) + { + switch (type) + { + case ALSceneExplorerItem::TYPE_AVATAR: + { + LLAvatarName av_name; + if (LLAvatarNameCache::get(id, &av_name)) + return av_name.getCompleteName(); + return std::string("(loading avatar)"); + } + case ALSceneExplorerItem::TYPE_ATTACHMENT: + return std::string("(attachment)"); + default: + return std::string("(object)"); + } + } +} + +// ============================================================================ +ALFloaterSceneExplorer::ALFloaterSceneExplorer(const LLSD& key) +: LLFloater(key) +{ +} + +ALFloaterSceneExplorer::~ALFloaterSceneExplorer() +{ + gIdleCallbacks.deleteFunction(onIdle, this); + if (mPropsConn.connected()) + mPropsConn.disconnect(); +} + +bool ALFloaterSceneExplorer::postBuild() +{ + mTreePanel = getChild("scene_tree"); + buildTree(); + + mShowAvatars = gSavedSettings.getBOOL("ALSceneExplorerShowAvatars"); + + // Push persisted filter/sort state into the controls before wiring the + // commit callbacks, so the UI, the saved settings, and the filter object + // all agree from the first frame. + getChild("show_avatars_check")->setValue(mShowAvatars); + const U32 flag_mask = gSavedSettings.getU32("ALSceneExplorerFlagFilter"); + getChild("flag_scripted")->setValue((flag_mask & ALObjectProperties::FLAG_SCRIPTED) != 0); + getChild("flag_light")->setValue((flag_mask & ALObjectProperties::FLAG_LIGHT) != 0); + getChild("flag_particles")->setValue((flag_mask & ALObjectProperties::FLAG_PARTICLES) != 0); + getChild("limit_radius_check")->setValue(gSavedSettings.getBOOL("ALSceneExplorerLimitRadius")); + getChild("radius_slider")->setValue(gSavedSettings.getF32("ALSceneExplorerRadius")); + getChild("owner_combo")->setCurrentByIndex((S32)gSavedSettings.getU32("ALSceneExplorerOwnerFilter")); + getChild("sort_combo")->setCurrentByIndex((S32)gSavedSettings.getU32("ALSceneExplorerSortOrder")); + + getChild("filter_input")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + getChild("owner_combo")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + getChild("limit_radius_check")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + getChild("radius_slider")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + getChild("flag_scripted")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + getChild("flag_light")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + getChild("flag_particles")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + getChild("show_avatars_check")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + getChild("sort_combo")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onSortChanged, this)); + + getChild("refresh_btn")->setClickedCallback(boost::bind(&ALFloaterSceneExplorer::reconcile, this)); + getChild("focus_btn")->setClickedCallback(boost::bind(&ALFloaterSceneExplorer::doFocus, this)); + getChild("edit_btn")->setClickedCallback(boost::bind(&ALFloaterSceneExplorer::doEdit, this)); + getChild("inspect_btn")->setClickedCallback(boost::bind(&ALFloaterSceneExplorer::doInspect, this)); + getChild("teleport_btn")->setClickedCallback(boost::bind(&ALFloaterSceneExplorer::doTeleport, this)); + getChild("copy_id_btn")->setClickedCallback(boost::bind(&ALFloaterSceneExplorer::doCopyID, this)); + + // Context-menu commands (menu_scene_explorer.xml, shown by the folder view). + mCommitCallbackRegistrar.add("SceneExplorer.Focus", boost::bind(&ALFloaterSceneExplorer::doFocus, this)); + mCommitCallbackRegistrar.add("SceneExplorer.Edit", boost::bind(&ALFloaterSceneExplorer::doEdit, this)); + mCommitCallbackRegistrar.add("SceneExplorer.Inspect", boost::bind(&ALFloaterSceneExplorer::doInspect, this)); + mCommitCallbackRegistrar.add("SceneExplorer.Teleport", boost::bind(&ALFloaterSceneExplorer::doTeleport, this)); + mCommitCallbackRegistrar.add("SceneExplorer.CopyID", boost::bind(&ALFloaterSceneExplorer::doCopyID, this)); + + mPropsConn = ALObjectPropertiesCache::instance().setChangeCallback( + boost::bind(&ALFloaterSceneExplorer::onPropsCacheChanged, this, _1)); + + // Restore persisted sort order, and seed the filter object from the + // controls restored above. + mViewModel.getSorter().setMode((ALSceneExplorerSort::ESortMode)gSavedSettings.getU32("ALSceneExplorerSortOrder")); + onFilterChanged(); + + // Drive discovery / fetch / filtering / layout from the viewer idle loop, the + // way LLInventoryPanel does, rather than from draw(). This keeps tree mutation + // out of the render pass and lets the work time-slice across frames; idleUpdate + // itself no-ops while the floater isn't visible. + gIdleCallbacks.addFunction(onIdle, this); + + return true; +} + +void ALFloaterSceneExplorer::onOpen(const LLSD& key) +{ + mReconcileTimer.reset(); + reconcile(); +} + +void ALFloaterSceneExplorer::onClose(bool app_quitting) +{ +} + +void ALFloaterSceneExplorer::draw() +{ + // All discovery / fetch / filter / layout now happens in idleUpdate() (driven + // by gIdleCallbacks); draw() just renders the current tree state. + LLFloater::draw(); +} + +// static +void ALFloaterSceneExplorer::onIdle(void* user_data) +{ + static_cast(user_data)->idleUpdate(); +} + +void ALFloaterSceneExplorer::idleUpdate() +{ + // No work while hidden, minimised, or closed (single-instance floaters stay + // alive when closed). An occluded-but-open floater still counts as visible, + // which is what we want. + if (!mTree || !isInVisibleChain()) + return; + + if (mReconcileTimer.getElapsedTimeF32() > 1.5f) + { + mReconcileTimer.reset(); + reconcile(); + } + + // Build newly-discovered nodes a few milliseconds at a time so a dense region + // (up to ~60k objects) streams in instead of stalling the frame. + if (!mBuildQueue.empty()) + drainBuildQueue(0.006); + + if (mRetryTimer.getElapsedTimeF32() > 8.f) + { + mRetryTimer.reset(); + retryUnresolved(); + } + if (mFetchTimer.getElapsedTimeF32() > 0.5f) + { + mFetchTimer.reset(); + drainPropsQueue(); + } + + // The folder view's own idle routine: runs (time-sliced) filtering, finalises + // it, then arranges/lays out the tree in one consistent pass. + mTree->update(); + syncSelectionToWorld(); +} + +void ALFloaterSceneExplorer::drainBuildQueue(F64 max_time) +{ + if (mBuildQueue.empty()) + return; + LLViewerRegion* region = gAgent.getRegion(); + if (!region) + return; + + // Always make progress on at least one node, then keep going until the time + // budget for this frame is spent. + const F64 end_time = LLTimer::getTotalSeconds() + max_time; + do + { + const LLUUID id = mBuildQueue.front(); + mBuildQueue.pop_front(); + mQueued.erase(id); + + LLViewerObject* obj = gObjectList.findObject(id); + if (obj && !obj->isDead() && obj->getRegion() == region) + getOrCreateNode(obj); // creates the widget (and any missing ancestors) + } + while (!mBuildQueue.empty() && LLTimer::getTotalSeconds() < end_time); +} + +// ============================================================================ +// Tree construction +// ============================================================================ +void ALFloaterSceneExplorer::buildTree() +{ + LLViewerRegion* region = gAgent.getRegion(); + const std::string region_name = region ? region->getName() : LLStringUtil::null; + + mRootItem = new ALSceneExplorerItem(ALSceneExplorerItem::TYPE_REGION, LLUUID::null, + "Region: " + region_name, mViewModel, this); + + LLFolderView::Params p(LLUICtrlFactory::getDefaultParams()); + p.name = "scene_explorer_tree"; + p.title = mRootItem->getName(); + p.rect = LLRect(0, 0, mTreePanel->getRect().getWidth(), 0); + p.parent_panel = mTreePanel; + p.tool_tip = std::string("scene_explorer_tree"); + p.listener = mRootItem; + p.view_model = &mViewModel; + p.root = nullptr; + p.use_ellipses = true; + p.options_menu = "menu_scene_explorer.xml"; + mTree = LLUICtrlFactory::create(p); + mTree->setCallbackRegistrar(&mCommitCallbackRegistrar); + mTree->setEnableRegistrar(&mEnableCallbackRegistrar); + + LLRect scroller_rect = mTreePanel->getRect(); + scroller_rect.translate(-scroller_rect.mLeft, -scroller_rect.mBottom); + LLScrollContainer::Params sp(LLUICtrlFactory::getDefaultParams()); + sp.rect(scroller_rect); + LLScrollContainer* scroller = LLUICtrlFactory::create(sp); + scroller->setFollowsAll(); + mTreePanel->addChild(scroller); + scroller->addChild(mTree); + mTree->setScrollContainer(scroller); + mTree->setFollowsAll(); + mTree->addChild(mTree->mStatusTextBox); + + mViewModel.setFolderView(mTree); + + mObjectsCategory = new ALSceneExplorerItem(ALSceneExplorerItem::TYPE_CATEGORY_OBJECTS, + LLUUID::generateNewID(), "Objects", mViewModel, this); + mRootItem->addChild(mObjectsCategory); + mObjectsWidget = createWidget(mObjectsCategory, true, mTree); + + mAvatarsCategory = new ALSceneExplorerItem(ALSceneExplorerItem::TYPE_CATEGORY_AVATARS, + LLUUID::generateNewID(), "Avatars", mViewModel, this); + mRootItem->addChild(mAvatarsCategory); + mAvatarsWidget = createWidget(mAvatarsCategory, true, mTree); + + mTree->setOpen(true); + static_cast(mObjectsWidget)->setOpen(true); +} + +LLFolderViewItem* ALFloaterSceneExplorer::createWidget(ALSceneExplorerItem* item, bool is_folder, LLFolderViewItem* parent_widget) +{ + LLFolderViewItem::Params params(LLUICtrlFactory::getDefaultParams()); + params.name = item->getName(); + params.root = mTree; + params.listener = item; + params.tool_tip = item->getName(); + params.font_color = LLUIColorTable::instance().getColor("MenuItemEnabledColor", LLColor4::white); + params.font_highlight_color = LLUIColorTable::instance().getColor("MenuItemHighlightColor", LLColor4::white); + + LLFolderViewItem* widget; + if (is_folder) + { + // Folders are our custom subclass (double-click acts on the object; + // disclosure arrow expands). We populate children eagerly (unlike + // inventory's lazy fetch), so mark them inited immediately: + // LLFolderViewFolder::arrange() gates both sort(this) and the + // folder-complete computation on mAreChildrenInited; left false, our + // folders would never sort their children and would forever report + // "incomplete" (perpetual disclosure arrow). + ALSceneExplorerFolder* folder = LLUICtrlFactory::create(params); + folder->setChildrenInited(true); + widget = folder; + } + else + { + widget = LLUICtrlFactory::create(params); + } + + if (parent_widget) + widget->addToFolder(static_cast(parent_widget)); + + return widget; +} + +ALSceneExplorerItem* ALFloaterSceneExplorer::getOrCreateNode(LLViewerObject* obj) +{ + if (!obj) + return nullptr; + + const LLUUID& id = obj->getID(); + auto found = mItems.find(id); + if (found != mItems.end()) + return found->second; + + ALSceneExplorerItem::EItemType type; + ALSceneExplorerItem* parent_item = nullptr; + LLFolderViewItem* parent_widget = nullptr; + bool is_folder = false; + S32 link_order = 0; + + if (obj->asAvatar()) + { + type = ALSceneExplorerItem::TYPE_AVATAR; + parent_item = mAvatarsCategory; + parent_widget = mAvatarsWidget; + is_folder = true; + } + else if (obj->isRootEdit()) + { + // Root of a linkset: an attachment root nests under its avatar's + // attachment-point folder; everything else is a world linkset. + if (obj->isAttachment()) + { + // getAvatarAncestor() walks the parent chain to the real wearer. + // (getAvatar() would return an animesh object's own control avatar, + // which has no attachment point for it.) + ALSceneExplorerItem* point_node = getOrCreatePointFolder(obj->getAvatarAncestor(), obj); + if (!point_node) + return nullptr; + type = ALSceneExplorerItem::TYPE_ATTACHMENT; + parent_item = point_node; + auto wit = mWidgets.find(point_node->getUUID()); + parent_widget = (wit != mWidgets.end()) ? wit->second : nullptr; + } + else + { + type = ALSceneExplorerItem::TYPE_LINKSET; + parent_item = mObjectsCategory; + parent_widget = mObjectsWidget; + } + // Every root is a folder so single- and multi-prim objects sort together + // in one list; a childless folder simply draws no disclosure arrow. + is_folder = true; + } + else + { + // Child prim of a linkset (world or attachment). + LLViewerObject* root = obj->getRootEdit(); + ALSceneExplorerItem* root_node = (root && root != obj) ? getOrCreateNode(root) : nullptr; + if (!root_node) + return nullptr; + type = ALSceneExplorerItem::TYPE_PRIM; + parent_item = root_node; + auto wit = mWidgets.find(root->getID()); + parent_widget = (wit != mWidgets.end()) ? wit->second : nullptr; + link_order = linkOrderInParent(root, obj); + } + + if (!parent_item || !parent_widget) + return nullptr; + + ALSceneExplorerItem* item = new ALSceneExplorerItem(type, id, placeholderName(type, id), mViewModel, this); + item->setLinkOrder(link_order); + ALObjectProperties::Record rec = ALObjectProperties::fromObject(obj); + item->updateRecord(rec); + + parent_item->addChild(item); + LLFolderViewItem* widget = createWidget(item, is_folder, parent_widget); + + mItems[id] = item; + mWidgets[id] = widget; + + // Pick up already-cached server props, otherwise request them. Avatar + // display names resolve through the name cache instead; subscribe once and + // refresh when the name lands (handle-guarded in case we close first). + applyServerProps(item); + if (type != ALSceneExplorerItem::TYPE_AVATAR) + { + queueProps(id); + } + else + { + LLAvatarName av_name; + if (!LLAvatarNameCache::get(id, &av_name)) + { + LLHandle handle = getDerivedHandle(); + LLAvatarNameCache::get(id, + [handle](const LLUUID& av_id, const LLAvatarName& name) + { + if (ALFloaterSceneExplorer* self = handle.get()) + self->onAvatarNameLoaded(av_id, name); + }); + } + } + + widget->refresh(); // populate label/suffix/icon from the model + return item; +} + +ALSceneExplorerItem* ALFloaterSceneExplorer::getOrCreatePointFolder(LLViewerObject* avatar_obj, LLViewerObject* attachment) +{ + LLVOAvatar* avatar = avatar_obj ? avatar_obj->asAvatar() : nullptr; + if (!avatar || !attachment) + return nullptr; + + // Resolve the hosting attachment point from the object's attachment state + // (getTargetAttachmentPoint also handles the ATTACHMENT_ADD mask), then + // recover its index for the synthetic folder key. + LLViewerJointAttachment* point = avatar->getTargetAttachmentPoint(attachment); + S32 point_index = 0; + if (point) + { + for (const auto& [idx, jp] : avatar->mAttachmentPoints) + { + if (jp == point) + { + point_index = idx; + break; + } + } + } + if (!point || !point_index) + return nullptr; + + // The point folder hangs off the avatar node; ensure that exists first. + ALSceneExplorerItem* av_node = getOrCreateNode(avatar_obj); + if (!av_node) + return nullptr; + auto avwit = mWidgets.find(avatar_obj->getID()); + LLFolderViewItem* av_widget = (avwit != mWidgets.end()) ? avwit->second : nullptr; + if (!av_widget) + return nullptr; + + const LLUUID key = attachmentPointKey(avatar_obj->getID(), point_index); + auto found = mItems.find(key); + if (found != mItems.end()) + return found->second; + + // Localize the joint name the same way the attach menus do ("R Forearm", + // "Skull", ... are keys in strings.xml). + std::string point_name = point->getName(); + point_name = point_name.empty() ? std::string("Attachment point") + : LLTrans::getString(point_name); + + ALSceneExplorerItem* point_node = new ALSceneExplorerItem( + ALSceneExplorerItem::TYPE_ATTACHMENT_POINT, key, point_name, mViewModel, this); + + av_node->addChild(point_node); + LLFolderViewItem* point_widget = createWidget(point_node, true, av_widget); + + mItems[key] = point_node; + mWidgets[key] = point_widget; + + point_widget->refresh(); + return point_node; +} + +void ALFloaterSceneExplorer::eraseSubtreeMaps(ALSceneExplorerItem* item) +{ + for (auto it = item->getChildrenBegin(); it != item->getChildrenEnd(); ++it) + { + eraseSubtreeMaps(static_cast(it->get())); + } + const LLUUID& id = item->getUUID(); + mWidgets.erase(id); + mItems.erase(id); +} + +void ALFloaterSceneExplorer::removeNode(ALSceneExplorerItem* item) +{ + if (!item) + return; + + const LLUUID id = item->getUUID(); + auto wit = mWidgets.find(id); + LLFolderViewItem* widget = (wit != mWidgets.end()) ? wit->second : nullptr; + + eraseSubtreeMaps(item); + + // getParent() is public on the LLFolderViewModelItem interface but protected + // in the Common base, so reach it through a base-class pointer. + LLFolderViewModelItem* base_item = item; + if (LLFolderViewModelItem* parent = const_cast(base_item->getParent())) + parent->removeChild(item); + + if (widget) + widget->destroyView(); +} + +void ALFloaterSceneExplorer::clearTree() +{ + // Wholesale teardown (used on region change): drop every object/avatar + // subtree but keep the structural root and category nodes. Collect ids + // first — removing a root frees its descendants' model items, so holding + // raw pointers across removals would dangle. + std::vector roots; + auto collect_children = [&roots](ALSceneExplorerItem* category) + { + if (!category) + return; + for (auto it = category->getChildrenBegin(); it != category->getChildrenEnd(); ++it) + { + roots.push_back(static_cast(it->get())->getUUID()); + } + }; + collect_children(mObjectsCategory); + collect_children(mAvatarsCategory); + for (const LLUUID& id : roots) + { + auto it = mItems.find(id); + if (it != mItems.end()) + removeNode(it->second); + } + + mFetchQueue.clear(); + mQueuedProps.clear(); + mBuildQueue.clear(); + mQueued.clear(); +} + +void ALFloaterSceneExplorer::reconcile() +{ + LLViewerRegion* region = gAgent.getRegion(); + if (!region || !mTree) + return; + + // Region crossing: tear the tree down wholesale and rebuild for the new + // region (the property cache already dropped itself via the agent's + // region-changed callback). Cheaper and cleaner than discovering each + // stale node's absence one-by-one, and keeps the root label current. + const U64 region_handle = region->getHandle(); + if (region_handle != mLastRegionHandle) + { + mLastRegionHandle = region_handle; + clearTree(); + if (mRootItem) + mRootItem->setName("Region: " + region->getName()); + } + + // Snapshot the derender list once per pass: isDerendered() is a linear + // list walk, far too slow to call per object on a dense region. + boost::unordered_set derendered; + for (const auto& entry : ALDerenderList::instance().getEntries()) + { + if (entry && entry->getType() == ALDerenderEntry::TYPE_OBJECT) + derendered.insert(entry->getID()); + } + + boost::unordered_set present; + const S32 num = gObjectList.getNumObjects(); + present.reserve(num); + for (S32 i = 0; i < num; ++i) + { + LLViewerObject* obj = gObjectList.getObject(i); + if (!obj || obj->isDead()) + continue; + if (obj->getRegion() != region) + continue; + if (obj->isHUDAttachment()) + continue; + // Terrain (surface patches) has no server-side object properties. + if (obj->getPCode() == LLViewerObject::LL_VO_SURFACE_PATCH) + continue; + + LLVOAvatar* avatarp = obj->asAvatar(); + // Control avatars (animesh) and UI/preview avatars have no associated + // user and aren't real scene avatars. The animesh object itself still + // appears via the normal object / attachment path below. + if (avatarp && (avatarp->isControlAvatar() || avatarp->isUIAvatar())) + continue; + + const bool is_avatar = (avatarp != nullptr); + const bool is_attachment = obj->isAttachment(); + if ((is_avatar || is_attachment) && !mShowAvatars) + continue; + // Unselectable objects (water/sky/ground, no-select prims) can neither be + // fetched from the server nor edited, so leave them out entirely. + if (!is_avatar && !obj->mbCanSelect) + continue; + if (!is_avatar && !is_attachment && derendered.count(obj->getID())) + continue; + + const LLUUID& id = obj->getID(); + present.insert(id); + + auto it = mItems.find(id); + if (it == mItems.end()) + { + // Not built yet: enqueue for time-sliced creation in drainBuildQueue() + // rather than building thousands of widgets inline this frame. + if (mQueued.insert(id).second) + mBuildQueue.push_back(id); + continue; + } + + // Cheap per-pass refresh. Distance changes as things move (drives the + // distance sort + "Nm" suffix); land-impact / object cost arrive + // asynchronously after a node is built, so we keep pulling those cached + // accounting values until they populate. Flags, geometry and the ARC + // render cost (a profiled per-face hotspot) were captured at build time + // and rarely change, so we skip the full fillFromObject() here to keep + // the 1.5s reconcile O(N)-cheap even on a 60k-object region. The radius + // filter is re-armed below when the agent has moved, since this write + // deliberately doesn't dirty per-item filter state. + ALSceneExplorerItem* node = it->second; + ALObjectProperties::Record& rec = node->getRecordRef(); + rec.mPosRegion = obj->getPositionRegion(); + rec.mPosGlobal = obj->getPositionGlobal(); + rec.mDistance = (F32)(rec.mPosGlobal - gAgent.getPositionGlobal()).magVec(); + rec.mObjectCost = obj->getObjectCost(); + rec.mLandImpact = obj->getLinksetCost(); + } + + // Collect ids, not pointers: removing a linkset root frees its descendant + // model items, so a raw descendant pointer collected earlier in the pass + // would dangle. Each id is re-looked-up immediately before removal. + std::vector to_remove; + for (const auto& entry : mItems) + { + // Attachment-point folders are synthetic (no backing object); they are + // pruned below once empty rather than matched against the present set. + if (entry.second->getItemType() == ALSceneExplorerItem::TYPE_ATTACHMENT_POINT) + continue; + if (!present.count(entry.first)) + to_remove.push_back(entry.first); + } + for (const LLUUID& id : to_remove) + { + auto it = mItems.find(id); + if (it != mItems.end()) + removeNode(it->second); + } + + // Drop attachment-point folders that no longer hold any attachments. + std::vector empty_points; + for (const auto& entry : mItems) + { + if (entry.second->getItemType() == ALSceneExplorerItem::TYPE_ATTACHMENT_POINT + && entry.second->getChildrenCount() == 0) + { + empty_points.push_back(entry.first); + } + } + for (const LLUUID& id : empty_points) + { + auto it = mItems.find(id); + if (it != mItems.end()) + removeNode(it->second); + } + + // The per-pass distance refresh above doesn't dirty per-item filter state, + // so when the radius predicate is active, re-arm the whole filter once the + // agent has actually moved — otherwise "Within N m" would keep showing the + // object set from wherever the filter last ran. + ALSceneExplorerFilter& filter = mViewModel.getFilter(); + if (filter.isLimitRadiusActive()) + { + const LLVector3d agent_pos = gAgent.getPositionGlobal(); + if ((agent_pos - mLastFilterAgentPos).magVec() > 1.0) + { + mLastFilterAgentPos = agent_pos; + filter.setModified(LLFolderViewFilter::FILTER_RESTART); + } + } + + // Re-sort everything periodically only for the distance key, whose order + // shifts as the agent moves. Static keys (name/land-impact/triangles/type) + // re-sort per-parent when nodes are added or change, so a full periodic + // re-sort would just be wasted work on a large tree. + if (mViewModel.getSorter().getMode() == ALSceneExplorerSort::SORT_DISTANCE) + mViewModel.requestSortAll(); +} + +// ============================================================================ +// Async property fetch +// ============================================================================ +void ALFloaterSceneExplorer::queueProps(const LLUUID& id) +{ + if (id.isNull() || mQueuedProps.count(id)) + return; + ALObjectPropertiesCache& cache = ALObjectPropertiesCache::instance(); + // Already sent and awaiting the reply. + if (cache.isPending(id)) + return; + // Skip if we already have full data (creator/date) for this object; a + // family-only entry (e.g. from a hover) still warrants a full fetch. + const ALObjectPropertiesCache::ServerProps* p = cache.get(id); + if (p && p->mHasFullData) + return; + mQueuedProps.insert(id); + mFetchQueue.push_back(id); +} + +void ALFloaterSceneExplorer::drainPropsQueue() +{ + LLViewerRegion* region = gAgent.getRegion(); + if (!region || mFetchQueue.empty()) + return; + + ALObjectPropertiesCache& cache = ALObjectPropertiesCache::instance(); + std::vector local_ids; + local_ids.reserve(MAX_OBJECTS_PER_PACKET); + while (!mFetchQueue.empty() && local_ids.size() < (size_t)MAX_OBJECTS_PER_PACKET) + { + const LLUUID id = mFetchQueue.front(); + mFetchQueue.pop_front(); + mQueuedProps.erase(id); + + LLViewerObject* obj = gObjectList.findObject(id); + // Local ids are a per-region namespace, so never address an object + // that died or crossed into a neighbour region. + if (!obj || obj->isDead() || obj->getRegion() != region) + continue; + // Never disturb the user's live selection: a raw ObjectDeselect for an + // actively edited object desyncs the simulator's selection state (the + // sim halts physical objects and streams updates only while selected). + // retryUnresolved() re-queues it once it is no longer selected. + if (obj->isSelected()) + continue; + + local_ids.push_back(obj->getLocalID()); + cache.markPending(id); + } + if (local_ids.empty()) + return; + + // Bulk-select to provoke full ObjectProperties replies (which, unlike the + // family variant, include creator and creation date), then immediately + // deselect so nothing stays selected on the simulator. The replies are + // captured by ALObjectPropertiesCache, whose in-flight tracking also tells + // LLSelectMgr these node-less replies are expected (no warning). + const LLHost& host = region->getHost(); + sendObjectSelectionMessage(_PREHASH_ObjectSelect, local_ids, host); + sendObjectSelectionMessage(_PREHASH_ObjectDeselect, local_ids, host); +} + +void ALFloaterSceneExplorer::retryUnresolved() +{ + // Property replies arrive over unreliable transport and can be dropped, so + // periodically re-request objects whose full data never came back. Capped + // per object so something the sim never answers for can't generate retry + // traffic forever. + constexpr S32 MAX_PROPS_RETRIES = 8; + ALObjectPropertiesCache& cache = ALObjectPropertiesCache::instance(); + for (const auto& entry : mItems) + { + ALSceneExplorerItem* item = entry.second; + const ALSceneExplorerItem::EItemType type = item->getItemType(); + // Avatars resolve via the name cache and attachment-point folders are + // synthetic — neither has server object properties to fetch. + if (type == ALSceneExplorerItem::TYPE_AVATAR + || type == ALSceneExplorerItem::TYPE_ATTACHMENT_POINT + || item->isContainer()) + { + continue; + } + // Gate on the cache's full-data flag, not the record's mPropsValid: a + // family-only entry (hover) marks the record valid but the full fetch + // (creator / creation date) still needs its retry. + const ALObjectPropertiesCache::ServerProps* p = cache.get(entry.first); + if (p && p->mHasFullData) + continue; + if (mQueuedProps.count(entry.first)) + continue; // still waiting in the queue, not lost + if (item->getPropsRetries() >= MAX_PROPS_RETRIES) + continue; + item->notePropsRetry(); + cache.clearPending(entry.first); // assume the in-flight reply was lost + queueProps(entry.first); + } +} + +void ALFloaterSceneExplorer::applyServerProps(ALSceneExplorerItem* item) +{ + if (!item) + return; + const ALObjectPropertiesCache::ServerProps* p = + ALObjectPropertiesCache::instance().get(item->getUUID()); + if (!p) + return; + + ALObjectProperties::Record rec = item->getRecord(); + rec.mName = p->mName; + rec.mDescription = p->mDescription; + rec.mOwnerId = p->mOwnerId; + rec.mGroupId = p->mGroupId; + rec.mCreatorId = p->mCreatorId; + rec.mGroupOwned = p->mGroupOwned; + rec.mCreationDate = p->mCreationDate; + rec.mPropsValid = true; + + // Adopt the server name in the same pass (avatars keep their display name + // from the name cache instead). + const std::string display_name = + (!p->mName.empty() && item->getItemType() != ALSceneExplorerItem::TYPE_AVATAR) + ? p->mName : LLStringUtil::null; + item->updateRecord(rec, display_name); +} + +void ALFloaterSceneExplorer::onAvatarNameLoaded(const LLUUID& id, const LLAvatarName& av_name) +{ + auto it = mItems.find(id); + if (it == mItems.end()) + return; + it->second->setName(av_name.getCompleteName()); + auto wit = mWidgets.find(id); + if (wit != mWidgets.end()) + wit->second->refresh(); +} + +void ALFloaterSceneExplorer::onPropsCacheChanged(const LLUUID& id) +{ + auto it = mItems.find(id); + if (it == mItems.end()) + return; + applyServerProps(it->second); + + // The folder-view widget caches its label, so make it re-read the model. + // applyServerProps() already dirtied the filter via updateRecord()/setName(), + // so the next idle pass re-filters this node. + auto wit = mWidgets.find(id); + if (wit != mWidgets.end()) + wit->second->refresh(); +} + +// ============================================================================ +// Filters / sort +// ============================================================================ +void ALFloaterSceneExplorer::onFilterChanged() +{ + ALSceneExplorerFilter& f = mViewModel.getFilter(); + f.setFilterSubString(getChild("filter_input")->getText()); + + const S32 owner_idx = llmax(0, getChild("owner_combo")->getCurrentIndex()); + f.setOwnerMode((ALSceneExplorerFilter::EOwnerMode)owner_idx); + + U32 flags = 0; + if (getChild("flag_scripted")->getValue().asBoolean()) flags |= ALObjectProperties::FLAG_SCRIPTED; + if (getChild("flag_light")->getValue().asBoolean()) flags |= ALObjectProperties::FLAG_LIGHT; + if (getChild("flag_particles")->getValue().asBoolean()) flags |= ALObjectProperties::FLAG_PARTICLES; + f.setFlagMask(flags); + + const bool limit = getChild("limit_radius_check")->getValue().asBoolean(); + const F32 radius = (F32)getChild("radius_slider")->getValue().asReal(); + f.setRadius(radius, limit); + + // Persist the filter set (the text predicate is deliberately session-only). + gSavedSettings.setU32("ALSceneExplorerOwnerFilter", (U32)owner_idx); + gSavedSettings.setU32("ALSceneExplorerFlagFilter", flags); + gSavedSettings.setBOOL("ALSceneExplorerLimitRadius", limit); + gSavedSettings.setF32("ALSceneExplorerRadius", radius); + + const bool show_av = getChild("show_avatars_check")->getValue().asBoolean(); + if (show_av != mShowAvatars) + { + mShowAvatars = show_av; + gSavedSettings.setBOOL("ALSceneExplorerShowAvatars", show_av); + reconcile(); + } + // The filter setters above bumped the filter generation, so the next idle + // pass (mTree->update()) re-filters and re-arranges. +} + +void ALFloaterSceneExplorer::onSortChanged() +{ + const U32 mode = (U32)llmax(0, getChild("sort_combo")->getCurrentIndex()); + mViewModel.getSorter().setMode((ALSceneExplorerSort::ESortMode)mode); + gSavedSettings.setU32("ALSceneExplorerSortOrder", mode); + mViewModel.requestSortAll(); + if (mTree) + mTree->arrangeAll(); +} + +// ============================================================================ +// Actions +// ============================================================================ +LLViewerObject* ALFloaterSceneExplorer::getSelectedObject() const +{ + if (!mTree) + return nullptr; + LLFolderViewItem* sel = mTree->getCurSelectedItem(); + if (!sel) + return nullptr; + ALSceneExplorerItem* item = static_cast(sel->getViewModelItem()); + if (!item) + return nullptr; + return gObjectList.findObject(item->getUUID()); +} + +void ALFloaterSceneExplorer::selectInWorld(const uuid_vec_t& ids) +{ + std::vector objs; + objs.reserve(ids.size()); + for (const LLUUID& id : ids) + { + if (LLViewerObject* o = gObjectList.findObject(id)) + objs.push_back(o); + } + + LLSelectMgr* sm = LLSelectMgr::getInstance(); + mSyncingSelection = true; + sm->deselectAll(); + if (!objs.empty()) + sm->selectObjectAndFamily(objs); + mSyncingSelection = false; +} + +void ALFloaterSceneExplorer::syncSelectionToWorld() +{ + if (mSyncingSelection) + return; + LLViewerObject* obj = getSelectedObject(); + const LLUUID id = obj ? obj->getID() : LLUUID::null; + if (id == mLastSelectedID) + return; + mLastSelectedID = id; + if (obj) + { + uuid_vec_t ids; + ids.push_back(id); + selectInWorld(ids); + } +} + +void ALFloaterSceneExplorer::activateItem(const LLUUID& id) +{ + // Act on the activated item itself, not on whatever the tree currently has + // selected — double-click paths can fire without a selection change. + LLViewerObject* obj = gObjectList.findObject(id); + if (!obj) + return; + mLastSelectedID = id; // keep the per-frame selection sync from re-selecting + uuid_vec_t ids; + ids.push_back(id); + selectInWorld(ids); + + switch (gSavedSettings.getU32("ALSceneExplorerActivateAction")) + { + case 1: openBuildTools(); break; + case 2: LLFloaterReg::showInstance("inspect"); break; + case 3: break; // select only + case 0: + default: handle_zoom_to_object(id); break; + } +} + +void ALFloaterSceneExplorer::openBuildTools() +{ + LLFloaterReg::showInstance("build"); + LLToolMgr::getInstance()->setCurrentToolset(gBasicToolset); + if (gFloaterTools) + gFloaterTools->setEditTool(LLToolCompTranslate::getInstance()); +} + +void ALFloaterSceneExplorer::doFocus() +{ + if (LLViewerObject* obj = getSelectedObject()) + { + // Frames the object from the bounding box / FOV like the standard + // "Zoom To" everywhere else, instead of a fixed-offset camera jump. + handle_zoom_to_object(obj->getID()); + } +} + +void ALFloaterSceneExplorer::doEdit() +{ + LLViewerObject* obj = getSelectedObject(); + if (!obj) + return; + uuid_vec_t ids; + ids.push_back(obj->getID()); + selectInWorld(ids); + openBuildTools(); +} + +void ALFloaterSceneExplorer::doInspect() +{ + LLViewerObject* obj = getSelectedObject(); + if (!obj) + return; + uuid_vec_t ids; + ids.push_back(obj->getID()); + selectInWorld(ids); + LLFloaterReg::showInstance("inspect"); +} + +void ALFloaterSceneExplorer::doTeleport() +{ + LLViewerObject* obj = getSelectedObject(); + if (!obj) + return; + gAgent.teleportViaLocation(obj->getPositionGlobal()); +} + +void ALFloaterSceneExplorer::doCopyID() +{ + LLViewerObject* obj = getSelectedObject(); + LLUUID id; + if (obj) + { + id = obj->getID(); + } + else if (mTree && mTree->getCurSelectedItem()) + { + if (ALSceneExplorerItem* item = static_cast(mTree->getCurSelectedItem()->getViewModelItem())) + id = item->getUUID(); + } + if (id.notNull()) + { + const std::string id_str = id.asString(); + const LLWString wid = utf8str_to_wstring(id_str); + LLClipboard::instance().copyToClipboard(wid, 0, (S32)wid.size()); + } +} diff --git a/indra/newview/alfloatersceneexplorer.h b/indra/newview/alfloatersceneexplorer.h new file mode 100644 index 0000000000..2eccebd048 --- /dev/null +++ b/indra/newview/alfloatersceneexplorer.h @@ -0,0 +1,127 @@ +/** + * @file alfloatersceneexplorer.h + * @brief Scene Explorer floater: a filterable scene-graph tree of region content + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ +#ifndef AL_FLOATERSCENEEXPLORER_H +#define AL_FLOATERSCENEEXPLORER_H + +#include + +#include +#include +#include + +#include "llfloater.h" +#include "llframetimer.h" +#include "lluuid.h" + +#include "alsceneexplorermodel.h" + +class LLAvatarName; +class LLFolderView; +class LLFolderViewItem; +class LLPanel; +class LLViewerObject; + +class ALFloaterSceneExplorer final : public LLFloater +{ + friend class LLFloaterReg; +public: + bool postBuild() override; + void onOpen(const LLSD& key) override; + void onClose(bool app_quitting) override; + void draw() override; + + // Invoked from ALSceneExplorerItem::openItem (double-click / Enter). + void activateItem(const LLUUID& id); + +private: + ALFloaterSceneExplorer(const LLSD& key); + ~ALFloaterSceneExplorer() override; + + // --- idle update (driven by gIdleCallbacks, not draw) ----------------- + static void onIdle(void* user_data); + void idleUpdate(); + void drainBuildQueue(F64 max_time); + + // --- tree construction ------------------------------------------------ + void buildTree(); + void reconcile(); + ALSceneExplorerItem* getOrCreateNode(LLViewerObject* obj); + ALSceneExplorerItem* getOrCreatePointFolder(LLViewerObject* avatar, LLViewerObject* attachment); + LLFolderViewItem* createWidget(ALSceneExplorerItem* item, bool is_folder, LLFolderViewItem* parent_widget); + void eraseSubtreeMaps(ALSceneExplorerItem* item); + void removeNode(ALSceneExplorerItem* item); + void clearTree(); + + // --- async property fetch -------------------------------------------- + void queueProps(const LLUUID& id); + void drainPropsQueue(); + void retryUnresolved(); + void onPropsCacheChanged(const LLUUID& id); + void applyServerProps(ALSceneExplorerItem* item); + void onAvatarNameLoaded(const LLUUID& id, const LLAvatarName& av_name); + + // --- filters / sort --------------------------------------------------- + void onFilterChanged(); + void onSortChanged(); + + // --- actions ---------------------------------------------------------- + void syncSelectionToWorld(); + LLViewerObject* getSelectedObject() const; + void selectInWorld(const uuid_vec_t& ids); + void openBuildTools(); + void doFocus(); + void doEdit(); + void doInspect(); + void doTeleport(); + void doCopyID(); + + // --- members ---------------------------------------------------------- + ALSceneExplorerViewModel mViewModel; + LLFolderView* mTree = nullptr; + LLPanel* mTreePanel = nullptr; + + ALSceneExplorerItem* mRootItem = nullptr; + ALSceneExplorerItem* mObjectsCategory = nullptr; + ALSceneExplorerItem* mAvatarsCategory = nullptr; + LLFolderViewItem* mObjectsWidget = nullptr; + LLFolderViewItem* mAvatarsWidget = nullptr; + + boost::unordered_map mItems; + boost::unordered_map mWidgets; + + // Property-fetch pipeline: mFetchQueue holds ids waiting to be sent + // (mQueuedProps mirrors its membership for dedup); once sent, an id is + // tracked as in-flight by ALObjectPropertiesCache until its reply lands. + std::deque mFetchQueue; + boost::unordered_set mQueuedProps; + + // New objects discovered by reconcile() awaiting time-sliced widget creation + // in drainBuildQueue(), so a 60k-object region populates without a stall. + std::deque mBuildQueue; + boost::unordered_set mQueued; + + boost::signals2::connection mPropsConn; + + LLFrameTimer mReconcileTimer; + LLFrameTimer mFetchTimer; + LLFrameTimer mRetryTimer; + + U64 mLastRegionHandle = 0; + LLUUID mLastSelectedID; + LLVector3d mLastFilterAgentPos; // last agent position the radius filter ran at + bool mShowAvatars = true; + bool mSyncingSelection = false; +}; + +#endif // AL_FLOATERSCENEEXPLORER_H diff --git a/indra/newview/alobjectproperties.cpp b/indra/newview/alobjectproperties.cpp new file mode 100644 index 0000000000..a16ba69b78 --- /dev/null +++ b/indra/newview/alobjectproperties.cpp @@ -0,0 +1,269 @@ +/** + * @file alobjectproperties.cpp + * @brief Shared per-object metric record for the Scene Explorer, Inspect and detail views + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ + +#include "llviewerprecompiledheaders.h" + +#include "alobjectproperties.h" + +#include "message.h" + +#include "llagent.h" +#include "llprimitive.h" +#include "llselectmgr.h" +#include "lltextureentry.h" +#include "llviewerobject.h" +#include "llviewerregion.h" +#include "llvovolume.h" + +namespace ALObjectProperties +{ + +void fillFromObject(Record& rec, LLViewerObject* obj) +{ + if (!obj) + return; + + rec.mLocalId = obj->getLocalID(); + if (LLViewerRegion* regionp = obj->getRegion()) + { + rec.mRegionHandle = regionp->getHandle(); + } + + // Spatial + rec.mPosRegion = obj->getPositionRegion(); + rec.mPosGlobal = obj->getPositionGlobal(); + rec.mScale = obj->getScale(); + rec.mDistance = (F32)(rec.mPosGlobal - gAgent.getPositionGlobal()).magVec(); + + // Structure / geometry + rec.mPrimCount = obj->numChildren() + 1; + rec.mNumFaces = obj->getNumFaces(); + rec.mNumVertices = obj->getNumVertices(); + rec.mNumTriangles = obj->getNumIndices() / 3; + + U32 flags = FLAG_NONE; + + if (obj->asAvatar()) + { + rec.mGeom = GEOM_AVATAR; + flags |= FLAG_AVATAR; + } + else if (obj->isMesh()) + { + rec.mGeom = GEOM_MESH; + } + else if (obj->isSculpted()) + { + rec.mGeom = GEOM_SCULPT; + } + else + { + rec.mGeom = GEOM_PRIM; + } + + if (obj->flagScripted()) flags |= FLAG_SCRIPTED; + if (obj->flagUsePhysics()) flags |= FLAG_PHYSICS; + if (obj->flagPhantom()) flags |= FLAG_PHANTOM; + if (obj->flagTemporaryOnRez()) flags |= FLAG_TEMPORARY; + if (obj->isFlexible()) flags |= FLAG_FLEXIBLE; + if (obj->hasLightTexture()) flags |= FLAG_PROJECTOR; + if (obj->isReflectionProbe()) flags |= FLAG_REFLECTION_PROBE; + if (obj->isParticleSource()) flags |= FLAG_PARTICLES; + if (obj->isAttachment()) flags |= FLAG_ATTACHMENT; + if (obj->isAnimatedObject()) flags |= FLAG_ANIMATED; + if (obj->hasRenderMaterialParams()) flags |= FLAG_PBR_MATERIAL; + + // Volume-only features. + if (obj->getPCode() == LL_PCODE_VOLUME) + { + LLVOVolume* vobj = static_cast(obj); + + if (vobj->getIsLight()) + { + flags |= FLAG_LIGHT; + if (vobj->isLightSpotlight()) + flags |= FLAG_SPOTLIGHT; + } + if (vobj->hasMedia()) + flags |= FLAG_MEDIA; + + LLVOVolume::texture_cost_t textures; + rec.mRenderCost = (F32)vobj->getRenderCost(textures); + } + + // Per-face appearance flags. + const U8 num_tes = obj->getNumTEs(); + for (U8 i = 0; i < num_tes; ++i) + { + const LLTextureEntry* te = obj->getTE(i); + if (!te) + continue; + if (te->getFullbright()) + flags |= FLAG_FULLBRIGHT; + if (te->getGlow() > 0.f) + flags |= FLAG_GLOW; + if (te->getColor().mV[VALPHA] < 0.999f) + flags |= FLAG_ALPHA; + } + + rec.mFlags = flags; + + // Cost (cached values; trigger async refresh as a side effect). + rec.mObjectCost = obj->getObjectCost(); + rec.mLandImpact = obj->getLinksetCost(); + rec.mPhysicsCost = obj->getPhysicsCost(); + rec.mStreamingCost = obj->getStreamingCost(); +} + +Record fromObject(LLViewerObject* obj) +{ + Record rec; + if (obj) + { + rec.mId = obj->getID(); + fillFromObject(rec, obj); + } + return rec; +} + +std::string flagsToString(U32 flags) +{ + static const std::pair labels[] = { + { FLAG_SCRIPTED, "scripted" }, + { FLAG_PHYSICS, "physics" }, + { FLAG_PHANTOM, "phantom" }, + { FLAG_TEMPORARY, "temporary" }, + { FLAG_FLEXIBLE, "flexi" }, + { FLAG_LIGHT, "light" }, + { FLAG_SPOTLIGHT, "spotlight" }, + { FLAG_PROJECTOR, "projector" }, + { FLAG_GLOW, "glow" }, + { FLAG_FULLBRIGHT, "fullbright" }, + { FLAG_MEDIA, "media" }, + { FLAG_PARTICLES, "particles" }, + { FLAG_REFLECTION_PROBE, "probe" }, + { FLAG_ANIMATED, "animated" }, + { FLAG_ATTACHMENT, "attachment" }, + { FLAG_PBR_MATERIAL, "PBR" }, + { FLAG_ALPHA, "alpha" }, + }; + + std::string out; + for (const auto& [bit, name] : labels) + { + if (flags & bit) + { + if (!out.empty()) + out += ", "; + out += name; + } + } + return out; +} + +const std::string& iconName(const Record& rec) +{ + static const std::string s_object("Inv_Object"); + static const std::string s_avatar("Generic_Person"); + switch (rec.mGeom) + { + case GEOM_AVATAR: return s_avatar; + default: return s_object; + } +} + +} // namespace ALObjectProperties + +// ============================================================================ +// ALObjectPropertiesCache +// ============================================================================ +ALObjectPropertiesCache::ALObjectPropertiesCache() +{ + // Object names/owners can change and local ids are reused across visits, so + // drop everything when the agent changes region and let consumers re-fetch. + mRegionChangedConn = gAgent.addRegionChangedCallback(boost::bind(&ALObjectPropertiesCache::clear, this)); +} + +ALObjectPropertiesCache::~ALObjectPropertiesCache() +{ + if (mRegionChangedConn.connected()) + mRegionChangedConn.disconnect(); +} + +const ALObjectPropertiesCache::ServerProps* ALObjectPropertiesCache::get(const LLUUID& id) const +{ + auto it = mCache.find(id); + return (it != mCache.end()) ? &it->second : nullptr; +} + +// static +void ALObjectPropertiesCache::processObjectPropertiesFamily(LLMessageSystem* msg, void** user_data) +{ + LLUUID id; + msg->getUUIDFast(_PREHASH_ObjectData, _PREHASH_ObjectID, id); + if (id.notNull()) + { + ALObjectPropertiesCache& self = instance(); + ServerProps& p = self.mCache[id]; + msg->getUUIDFast(_PREHASH_ObjectData, _PREHASH_OwnerID, p.mOwnerId); + msg->getUUIDFast(_PREHASH_ObjectData, _PREHASH_GroupID, p.mGroupId); + msg->getStringFast(_PREHASH_ObjectData, _PREHASH_Name, p.mName); + msg->getStringFast(_PREHASH_ObjectData, _PREHASH_Description, p.mDescription); + p.mGroupOwned = p.mOwnerId.isNull() && p.mGroupId.notNull(); + self.mChangeSignal(id); + } + + // Preserve normal hover/selection behaviour. + LLSelectMgr::processObjectPropertiesFamily(msg, user_data); +} + +// static +void ALObjectPropertiesCache::processObjectProperties(LLMessageSystem* msg, void** user_data) +{ + ALObjectPropertiesCache& self = instance(); + const S32 count = msg->getNumberOfBlocksFast(_PREHASH_ObjectData); + uuid_vec_t ids; + ids.reserve(count); + for (S32 i = 0; i < count; ++i) + { + LLUUID id; + msg->getUUIDFast(_PREHASH_ObjectData, _PREHASH_ObjectID, id, i); + if (id.isNull()) + continue; + + ServerProps& p = self.mCache[id]; + msg->getUUIDFast(_PREHASH_ObjectData, _PREHASH_CreatorID, p.mCreatorId, i); + msg->getUUIDFast(_PREHASH_ObjectData, _PREHASH_OwnerID, p.mOwnerId, i); + msg->getUUIDFast(_PREHASH_ObjectData, _PREHASH_GroupID, p.mGroupId, i); + U64 creation_date = 0; + msg->getU64Fast(_PREHASH_ObjectData, _PREHASH_CreationDate, creation_date, i); + p.mCreationDate = (time_t)(creation_date / 1000000); + msg->getStringFast(_PREHASH_ObjectData, _PREHASH_Name, p.mName, i); + msg->getStringFast(_PREHASH_ObjectData, _PREHASH_Description, p.mDescription, i); + p.mGroupOwned = p.mOwnerId.isNull() && p.mGroupId.notNull(); + p.mHasFullData = true; + ids.push_back(id); + self.mChangeSignal(id); + } + + // Preserve normal selection / inspect / build behaviour. Forward before + // retiring the in-flight markers: LLSelectMgr consults isExpectedReply() + // to decide whether a reply block with no select node warrants a warning. + LLSelectMgr::processObjectProperties(msg, user_data); + + for (const LLUUID& id : ids) + { + self.mPendingRequests.erase(id); + } +} diff --git a/indra/newview/alobjectproperties.h b/indra/newview/alobjectproperties.h new file mode 100644 index 0000000000..35c4357f1f --- /dev/null +++ b/indra/newview/alobjectproperties.h @@ -0,0 +1,203 @@ +/** + * @file alobjectproperties.h + * @brief Shared per-object metric record for the Scene Explorer, Inspect and detail views + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ +#ifndef AL_OBJECTPROPERTIES_H +#define AL_OBJECTPROPERTIES_H + +#include +#include +#include + +#include "llsingleton.h" +#include "lluuid.h" +#include "v3math.h" +#include "v3dmath.h" + +class LLMessageSystem; +class LLViewerObject; + +// ============================================================================ +// ALObjectProperties - a flat, copyable snapshot of an object's properties. +// +// Local (geometry / flag / cost) fields are filled synchronously from a live +// LLViewerObject via fillFromObject(). Server fields (name/owner/creator/desc/ +// dates) arrive asynchronously and are merged by the owning view. +// ============================================================================ + +namespace ALObjectProperties +{ + // Geometry kind, used to pick the row icon. + enum EGeom : U8 + { + GEOM_PRIM = 0, + GEOM_MESH, + GEOM_SCULPT, + GEOM_AVATAR, + GEOM_UNKNOWN + }; + + // Feature flags. Independent of EGeom (an object can be e.g. a scripted, + // glowing, light-emitting mesh). Used by the Scene Explorer filter. + enum EFlag : U32 + { + FLAG_NONE = 0, + FLAG_SCRIPTED = 1 << 0, + FLAG_PHYSICS = 1 << 1, + FLAG_PHANTOM = 1 << 2, + FLAG_TEMPORARY = 1 << 3, + FLAG_FLEXIBLE = 1 << 4, + FLAG_LIGHT = 1 << 5, + FLAG_SPOTLIGHT = 1 << 6, // light with a projector cone + FLAG_PROJECTOR = 1 << 7, // has a projection texture + FLAG_GLOW = 1 << 8, + FLAG_FULLBRIGHT = 1 << 9, + FLAG_MEDIA = 1 << 10, + FLAG_PARTICLES = 1 << 11, + FLAG_REFLECTION_PROBE = 1 << 12, + FLAG_ANIMATED = 1 << 13, // animated (animesh) object + FLAG_ATTACHMENT = 1 << 14, + FLAG_PBR_MATERIAL = 1 << 15, // has a GLTF render material on any face + FLAG_ALPHA = 1 << 16, // any face is alpha-blended / transparent + FLAG_AVATAR = 1 << 17 + }; + + struct Record + { + // Identity + LLUUID mId; + U32 mLocalId = 0; + U64 mRegionHandle = 0; + + // Spatial (local, live) + LLVector3 mPosRegion; + LLVector3d mPosGlobal; + LLVector3 mScale; + F32 mDistance = 0.f; + + // Structure / geometry (local, live) + EGeom mGeom = GEOM_UNKNOWN; + U32 mFlags = FLAG_NONE; + S32 mPrimCount = 1; // linkset prim count for a root + S32 mNumFaces = 0; + U32 mNumVertices = 0; + U32 mNumTriangles = 0; + + // Cost (local cached; populates over time) + F32 mLandImpact = 0.f; // linkset cost for a root object + F32 mObjectCost = 0.f; + F32 mPhysicsCost = 0.f; + F32 mRenderCost = 0.f; // "ARC" + F32 mStreamingCost = 0.f; + + // Server props (async; merged by the view) + bool mPropsValid = false; + std::string mName; + std::string mDescription; + LLUUID mOwnerId; + LLUUID mGroupId; + LLUUID mCreatorId; + bool mGroupOwned = false; + time_t mCreationDate = 0; + + bool hasFlag(EFlag f) const { return (mFlags & f) != 0; } + }; + + // Fill the local (non-server) fields of @rec from a live object. The + // identity and server fields are left untouched so a cached record keeps + // its async data across refreshes. + void fillFromObject(Record& rec, LLViewerObject* obj); + + // Convenience: build a fresh record (identity + local fields) for @obj. + Record fromObject(LLViewerObject* obj); + + // A compact human-readable flag list for a row suffix / tooltip, e.g. + // "scripted, light, glow". Empty when no notable flags set. + std::string flagsToString(U32 flags); + + // UI image name for a record's geometry kind. + const std::string& iconName(const Record& rec); +} + +// ============================================================================ +// ALObjectPropertiesCache +// +// Session-lifetime cache of server-provided object properties (name, owner, +// group, creator, description, creation date), keyed by object UUID. Populated +// by the ObjectProperties / ObjectPropertiesFamily message handlers, which are +// registered in llstartup.cpp ahead of LLSelectMgr's and forward to it so all +// existing selection behaviour is preserved. Consumers (Scene Explorer, detail +// pane, ...) read from here and subscribe for change notifications instead of +// having to be in a selection. +// ============================================================================ +class ALObjectPropertiesCache : public LLSingleton +{ + LLSINGLETON(ALObjectPropertiesCache); + ~ALObjectPropertiesCache(); +public: + struct ServerProps + { + std::string mName; + std::string mDescription; + LLUUID mOwnerId; + LLUUID mGroupId; + LLUUID mCreatorId; + bool mGroupOwned = false; + time_t mCreationDate = 0; + bool mHasFullData = false; // creator/date came from full ObjectProperties + }; + + // Message handlers (registered in llstartup.cpp). Each caches the relevant + // fields, notifies listeners, then forwards to the matching LLSelectMgr + // handler so normal selection / inspect / build behaviour is unaffected. + static void processObjectProperties(LLMessageSystem* msg, void** user_data); + static void processObjectPropertiesFamily(LLMessageSystem* msg, void** user_data); + + // Returns the cached entry for @id, or nullptr if nothing received yet. + const ServerProps* get(const LLUUID& id) const; + + typedef boost::signals2::signal change_signal_t; + boost::signals2::connection setChangeCallback(const change_signal_t::slot_type& cb) + { + return mChangeSignal.connect(cb); + } + + // In-flight request tracking. The Scene Explorer marks each object id it + // probes via bulk select/deselect; the id is retired when the full + // ObjectProperties reply lands (or explicitly, when the requester decides + // the reply was lost and re-requests). LLSelectMgr consults this to avoid + // warning about reply blocks that legitimately match no select node — + // including replies that arrive after the requesting floater has closed. + void markPending(const LLUUID& id) { mPendingRequests.insert(id); } + void clearPending(const LLUUID& id) { mPendingRequests.erase(id); } + bool isPending(const LLUUID& id) const { return mPendingRequests.count(id) != 0; } + static bool isExpectedReply(const LLUUID& id) + { + return instanceExists() && instance().isPending(id); + } + + // Drop all cached entries. Bound to the agent region-change signal so we + // never serve stale name/owner data after crossing into a new region. + void clear() + { + mCache.clear(); + mPendingRequests.clear(); + } + +private: + boost::unordered_map mCache; + boost::unordered_set mPendingRequests; + change_signal_t mChangeSignal; + boost::signals2::connection mRegionChangedConn; +}; + +#endif // AL_OBJECTPROPERTIES_H diff --git a/indra/newview/alsceneexplorermodel.cpp b/indra/newview/alsceneexplorermodel.cpp new file mode 100644 index 0000000000..30a589971d --- /dev/null +++ b/indra/newview/alsceneexplorermodel.cpp @@ -0,0 +1,376 @@ +/** + * @file alsceneexplorermodel.cpp + * @brief Folder-view model for the Scene Explorer tree + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ + +#include "llviewerprecompiledheaders.h" + +#include "alsceneexplorermodel.h" + +#include "alfloatersceneexplorer.h" +#include "llagent.h" +#include "llui.h" + +// ============================================================================ +// ALSceneExplorerSort +// ============================================================================ +bool ALSceneExplorerSort::operator()(const ALSceneExplorerItem* a, const ALSceneExplorerItem* b) const +{ + if (!a || !b) + return false; + + // Container nodes (region / categories / attachment points) keep a fixed + // structural order. Sibling attachment-point folders share a type, so order + // those alphabetically by point name ("Chest", "Right Hand", ...). + if (a->isContainer() || b->isContainer()) + { + if (a->getItemType() == ALSceneExplorerItem::TYPE_ATTACHMENT_POINT + && b->getItemType() == ALSceneExplorerItem::TYPE_ATTACHMENT_POINT) + { + return LLStringUtil::compareInsensitive(a->getName(), b->getName()) < 0; + } + return a->getItemType() < b->getItemType(); + } + + // Child prims of a linkset keep their native link order so a linkset's + // contents read top-down as they're linked, not scattered by distance/name. + // Every other node here is a root-level object (world linkset, single prim, + // avatar, or attachment root) and honours the active sort key. Siblings are + // always the same kind (the comparator only sees one folder's children). + if (a->getItemType() == ALSceneExplorerItem::TYPE_PRIM) + return a->getLinkOrder() < b->getLinkOrder(); + + const ALObjectProperties::Record& ra = a->getRecord(); + const ALObjectProperties::Record& rb = b->getRecord(); + + switch (mMode) + { + case SORT_NAME: + break; + case SORT_LAND_IMPACT: + if (ra.mLandImpact != rb.mLandImpact) + return ra.mLandImpact > rb.mLandImpact; + break; + case SORT_TRIANGLES: + if (ra.mNumTriangles != rb.mNumTriangles) + return ra.mNumTriangles > rb.mNumTriangles; + break; + case SORT_TYPE: + if (ra.mGeom != rb.mGeom) + return ra.mGeom < rb.mGeom; + break; + case SORT_DISTANCE: + default: + if (ra.mDistance != rb.mDistance) + return ra.mDistance < rb.mDistance; + break; + } + + return LLStringUtil::compareInsensitive(a->getName(), b->getName()) < 0; +} + +// ============================================================================ +// ALSceneExplorerFilter +// ============================================================================ +ALSceneExplorerFilter::ALSceneExplorerFilter() +: mName("scene_explorer") +{ +} + +void ALSceneExplorerFilter::setFilterSubString(const std::string& string) +{ + std::string lower(string); + LLStringUtil::toLower(lower); + if (lower != mFilterSubString) + { + mFilterSubString = lower; + setModified(FILTER_RESTART); + } +} + +void ALSceneExplorerFilter::setOwnerMode(EOwnerMode mode) +{ + if (mode != mOwnerMode) + { + mOwnerMode = mode; + setModified(FILTER_RESTART); + } +} + +void ALSceneExplorerFilter::setGeomMask(U32 mask) +{ + if (mask != mGeomMask) + { + mGeomMask = mask; + setModified(FILTER_RESTART); + } +} + +void ALSceneExplorerFilter::setFlagMask(U32 mask) +{ + if (mask != mFlagMask) + { + mFlagMask = mask; + setModified(FILTER_RESTART); + } +} + +void ALSceneExplorerFilter::setRadius(F32 radius, bool limit) +{ + if (radius != mRadius || limit != mLimitRadius) + { + mRadius = radius; + mLimitRadius = limit; + setModified(FILTER_RESTART); + } +} + +bool ALSceneExplorerFilter::isActive() const +{ + return !mFilterSubString.empty() + || mOwnerMode != OWNER_ANY + || mGeomMask != 0 + || mFlagMask != 0 + || mLimitRadius; +} + +void ALSceneExplorerFilter::setModified(EFilterModified behavior) +{ + mModified = true; + ++mGeneration; +} + +bool ALSceneExplorerFilter::check(const LLFolderViewModelItem* item) +{ + if (!isActive()) + return true; + + const ALSceneExplorerItem* sit = static_cast(item); + if (!sit || sit->isContainer()) + return false; // containers are only shown through matching descendants + + return matches(sit); +} + +bool ALSceneExplorerFilter::matches(const ALSceneExplorerItem* item) const +{ + const ALObjectProperties::Record& rec = item->getRecord(); + + if (mLimitRadius && rec.mDistance > mRadius) + return false; + + if (mGeomMask != 0 && !(mGeomMask & (1u << rec.mGeom))) + return false; + + if (mFlagMask != 0 && (rec.mFlags & mFlagMask) != mFlagMask) + return false; + + // Owner predicate only applies once we actually know the owner. + if (mOwnerMode != OWNER_ANY && rec.mPropsValid) + { + switch (mOwnerMode) + { + case OWNER_MINE: if (rec.mOwnerId != gAgentID) return false; break; + case OWNER_GROUP: if (!rec.mGroupOwned) return false; break; + case OWNER_OTHERS: if (rec.mOwnerId == gAgentID || rec.mGroupOwned) return false; break; + default: break; + } + } + + if (!mFilterSubString.empty() + && item->getSearchableText().find(mFilterSubString) == std::string::npos) + { + return false; + } + + return true; +} + +// ============================================================================ +// ALSceneExplorerItem +// ============================================================================ +ALSceneExplorerItem::ALSceneExplorerItem(EItemType type, const LLUUID& id, const std::string& name, + LLFolderViewModelInterface& root_view_model, ALFloaterSceneExplorer* floater) +: LLFolderViewModelItemCommon(root_view_model), + mItemType(type), + mUUID(id), + mName(name), + mFloater(floater) +{ + mRecord.mId = id; + rebuildSearchable(); +} + +void ALSceneExplorerItem::setName(const std::string& name) +{ + if (name != mName) + { + mName = name; + rebuildSearchable(); + dirtyFilter(); + } +} + +void ALSceneExplorerItem::updateRecord(const ALObjectProperties::Record& rec) +{ + updateRecord(rec, LLStringUtil::null); +} + +void ALSceneExplorerItem::updateRecord(const ALObjectProperties::Record& rec, const std::string& display_name) +{ + mRecord = rec; + mRecord.mId = mUUID; + if (!display_name.empty()) + { + mName = display_name; + } + rebuildSearchable(); + dirtyFilter(); +} + +void ALSceneExplorerItem::rebuildSearchable() +{ + mSearchable = mName + " " + mRecord.mDescription + " " + mUUID.asString(); + LLStringUtil::toLower(mSearchable); +} + +LLPointer ALSceneExplorerItem::getIcon() const +{ + return LLUI::getUIImage(ALObjectProperties::iconName(mRecord)); +} + +LLFontGL::StyleFlags ALSceneExplorerItem::getLabelStyle() const +{ + return LLFontGL::NORMAL; +} + +std::string ALSceneExplorerItem::getLabelSuffix() const +{ + if (isContainer()) + return LLStringUtil::null; + + std::string suffix = llformat("%.0fm", mRecord.mDistance); + if ((mItemType == TYPE_LINKSET || mItemType == TYPE_AVATAR) && mRecord.mLandImpact > 0.f) + { + suffix += llformat(" LI %.0f", mRecord.mLandImpact); + } + if (mRecord.mNumTriangles > 0) + { + suffix += llformat(" %u tris", mRecord.mNumTriangles); + } + return suffix; +} + +void ALSceneExplorerItem::activate() +{ + if (mFloater) + { + mFloater->activateItem(mUUID); + } +} + +void ALSceneExplorerItem::openItem(void) +{ + // openItem() fires from leaf double-click AND from + // LLFolderViewFolder::setOpenArrangeRecursively whenever a folder is + // expanded (disclosure arrow, keyboard, ancestor auto-open). Expanding a + // linkset must never move the camera or open an editor, so only leaf prims + // activate here; folder-typed scene objects activate from + // ALSceneExplorerFolder::handleDoubleClick. + if (mItemType == TYPE_PRIM) + { + activate(); + } +} + +bool ALSceneExplorerItem::filter(LLFolderViewFilter& filter) +{ + const S32 filter_generation = filter.getCurrentGeneration(); + const S32 must_pass_generation = filter.getFirstRequiredGeneration(); + + if (getLastFilterGeneration() >= must_pass_generation + && getLastFolderFilterGeneration() >= must_pass_generation + && !passedFilter(must_pass_generation)) + { + // Already failed a filter at least as strict as this one. + setPassedFilter(false, filter_generation); + setPassedFolderFilter(false, filter_generation); + return true; + } + + const bool passed_filter_folder = isFolderType() ? filter.checkFolder(this) : true; + setPassedFolderFilter(passed_filter_folder, filter_generation); + + bool continue_filtering = true; + if (!mChildren.empty() + && (getLastFilterGeneration() < must_pass_generation + || descendantsPassedFilter(must_pass_generation))) + { + for (auto& childp : mChildren) + { + ALSceneExplorerItem* child = static_cast(childp.get()); + if (child->getLastFilterGeneration() < filter_generation) + { + // Child returns false when the per-pass time budget is spent; + // stop here and resume from this child on the next idle. + continue_filtering = child->filter(filter); + } + if (child->passedFilter()) + { + ALSceneExplorerItem* vm = this; + while (vm && vm->mMostFilteredDescendantGeneration < filter_generation) + { + vm->mMostFilteredDescendantGeneration = filter_generation; + vm = static_cast(vm->mParent); + } + } + if (!continue_filtering) + break; + } + } + + if (continue_filtering) + { + const bool passed = filter.check(this); + setPassedFilter(passed, filter_generation); + continue_filtering = !filter.isTimedOut(); + } + + return continue_filtering; +} + +// ============================================================================ +// ALSceneExplorerFolder +// ============================================================================ +bool ALSceneExplorerFolder::handleDoubleClick(S32 x, S32 y, MASK mask) +{ + ALSceneExplorerItem* item = static_cast(getViewModelItem()); + + // Structural containers (region / category / attachment point) keep the + // standard expand-on-double-click behaviour. + if (!item || item->isContainer()) + return LLFolderViewFolder::handleDoubleClick(x, y, mask); + + // A real scene object: let an open folder's children claim the click first, + // otherwise act on the object (focus/edit/inspect per the activate-action + // setting) the way a leaf prim does, so single- and multi-prim objects + // behave identically. The disclosure arrow still expands children. Note: + // activate() rather than openItem() — the latter also fires on folder + // expansion and is therefore a no-op for folder-typed scene objects. + if (isOpen() && childrenHandleDoubleClick(x, y, mask) != nullptr) + return true; + + if (getRoot()) + getRoot()->setSelection(this, false); + item->activate(); + return true; +} diff --git a/indra/newview/alsceneexplorermodel.h b/indra/newview/alsceneexplorermodel.h new file mode 100644 index 0000000000..44f2a7f1fd --- /dev/null +++ b/indra/newview/alsceneexplorermodel.h @@ -0,0 +1,296 @@ +/** + * @file alsceneexplorermodel.h + * @brief Folder-view model for the Scene Explorer tree (region -> linkset -> prim, avatars) + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ +#ifndef AL_SCENEEXPLORERMODEL_H +#define AL_SCENEEXPLORERMODEL_H + +#include "../llui/llfolderviewitem.h" +#include "../llui/llfolderviewmodel.h" + +#include "alobjectproperties.h" +#include "lltimer.h" +#include "lluuid.h" + +class ALFloaterSceneExplorer; +class ALSceneExplorerItem; + +// ============================================================================ +// Sort comparator +// ============================================================================ +class ALSceneExplorerSort +{ +public: + enum ESortMode : U32 + { + SORT_DISTANCE = 0, + SORT_NAME, + SORT_LAND_IMPACT, + SORT_TRIANGLES, + SORT_TYPE + }; + + ALSceneExplorerSort(ESortMode mode = SORT_DISTANCE) : mMode(mode) {} + + void setMode(ESortMode mode) { mMode = mode; } + ESortMode getMode() const { return mMode; } + + bool operator()(const ALSceneExplorerItem* a, const ALSceneExplorerItem* b) const; + +private: + ESortMode mMode; +}; + +// ============================================================================ +// Filter +// ============================================================================ +class ALSceneExplorerFilter final : public LLFolderViewFilter +{ +public: + enum EOwnerMode : U32 + { + OWNER_ANY = 0, + OWNER_MINE, + OWNER_GROUP, + OWNER_OTHERS + }; + + ALSceneExplorerFilter(); + ~ALSceneExplorerFilter() override = default; + + // Predicate setters (called by the floater); each bumps the generation. + void setFilterSubString(const std::string& string); + void setOwnerMode(EOwnerMode mode); + void setGeomMask(U32 mask); // bits: 1 << ALObjectProperties::EGeom + void setFlagMask(U32 mask); // bits: ALObjectProperties::EFlag (require all) + void setRadius(F32 radius, bool limit); + bool isLimitRadiusActive() const { return mLimitRadius; } + + // LLFolderViewFilter + bool check(const LLFolderViewModelItem* item) override; + bool checkFolder(const LLFolderViewModelItem* folder) const override { return true; } + + void setEmptyLookupMessage(const std::string& message) override { mEmptyLookupMessage = message; } + std::string getEmptyLookupMessage(bool is_empty_folder = false) const override { return mEmptyLookupMessage; } + + bool showAllResults() const override { return false; } + + std::string::size_type getStringMatchOffset(LLFolderViewModelItem* item) const override { return std::string::npos; } + std::string::size_type getFilterStringSize() const override { return mFilterSubString.size(); } + + bool isActive() const override; + bool isModified() const override { return mModified; } + void clearModified() override { mModified = false; } + const std::string& getName() const override { return mName; } + const std::string& getFilterText() override { return mFilterSubString; } + void setModified(EFilterModified behavior = FILTER_RESTART) override; + + // Time-slice filtering the way LLInventoryFilter does: each pass gets a few + // ms, then bails and resumes next idle, so filtering a 60k-node tree stays + // responsive instead of stalling on one frame. Must be a real-time LLTimer: + // an LLFrameTimer's clock only advances once per frame, so it could never + // expire inside a single pass and the budget would be a no-op. + void resetTime(S32 timeout) override + { + mFilterTime.reset(); + mFilterTime.setTimerExpirySec((F32)timeout / 1000.f); + } + bool isTimedOut() override { return mFilterTime.hasExpired(); } + + bool isDefault() const override { return !isActive(); } + bool isNotDefault() const override { return isActive(); } + void markDefault() override {} + void resetDefault() override {} + + S32 getCurrentGeneration() const override { return mGeneration; } + S32 getFirstSuccessGeneration() const override { return mGeneration; } + S32 getFirstRequiredGeneration() const override { return mGeneration; } + +private: + bool matches(const ALSceneExplorerItem* item) const; + + std::string mName; + std::string mEmptyLookupMessage; + std::string mFilterSubString; // lowercased + EOwnerMode mOwnerMode = OWNER_ANY; + U32 mGeomMask = 0; + U32 mFlagMask = 0; + F32 mRadius = 64.f; + bool mLimitRadius = false; + bool mModified = false; + S32 mGeneration = 1; + LLTimer mFilterTime; // per-pass time budget for filter() +}; + +// ============================================================================ +// View model +// ============================================================================ +class ALSceneExplorerViewModel final +: public LLFolderViewModel +{ +public: + typedef LLFolderViewModel base_t; + + ALSceneExplorerViewModel() : base_t(new ALSceneExplorerSort(), new ALSceneExplorerFilter()) {} + + bool startDrag(std::vector& items) override { return false; } +}; + +// ============================================================================ +// Model item +// ============================================================================ +class ALSceneExplorerItem final : public LLFolderViewModelItemCommon +{ +public: + enum EItemType : U8 + { + TYPE_REGION = 0, + TYPE_CATEGORY_OBJECTS, + TYPE_CATEGORY_AVATARS, + TYPE_CATEGORY_DERENDERED, + TYPE_LINKSET, + TYPE_PRIM, + TYPE_AVATAR, + TYPE_ATTACHMENT_POINT, // per-avatar attachment-point grouping folder + TYPE_ATTACHMENT // attachment linkset root + }; + + ALSceneExplorerItem(EItemType type, const LLUUID& id, const std::string& name, + LLFolderViewModelInterface& root_view_model, ALFloaterSceneExplorer* floater); + + // Accessors used by filter / sort / floater + EItemType getItemType() const { return mItemType; } + bool isContainer() const + { + return mItemType == TYPE_REGION || isCategory() || mItemType == TYPE_ATTACHMENT_POINT; + } + bool isCategory() const + { + return mItemType == TYPE_CATEGORY_OBJECTS + || mItemType == TYPE_CATEGORY_AVATARS + || mItemType == TYPE_CATEGORY_DERENDERED; + } + // Folder-ness is fully derived from the item type: structural containers + // plus every root scene object (so single- and multi-prim objects sort + // together in one widget list). Only child prims are leaf widgets. + bool isFolderType() const + { + return isContainer() + || mItemType == TYPE_LINKSET + || mItemType == TYPE_AVATAR + || mItemType == TYPE_ATTACHMENT; + } + const LLUUID& getUUID() const { return mUUID; } + const ALObjectProperties::Record& getRecord() const { return mRecord; } + ALObjectProperties::Record& getRecordRef() { return mRecord; } + const std::string& getSearchableText() const { return mSearchable; } + + void setName(const std::string& name); + void updateRecord(const ALObjectProperties::Record& rec); + // Same, but also adopts @display_name (when non-empty) in the one + // rebuildSearchable()/dirtyFilter() pass instead of dirtying twice. + void updateRecord(const ALObjectProperties::Record& rec, const std::string& display_name); + + // Index within the parent linkset/avatar child list (1-based; 0 = unset/root). + // Used to keep child prims in native link order regardless of the active sort. + void setLinkOrder(S32 order) { mLinkOrder = order; } + S32 getLinkOrder() const { return mLinkOrder; } + + // Bounded re-request bookkeeping for the floater's property fetch. + S32 getPropsRetries() const { return mPropsRetries; } + void notePropsRetry() { ++mPropsRetries; } + + // Run the configured activate action (focus/edit/inspect) on this object. + // Invoked from the view's double-click paths only — openItem() also fires + // when a folder is expanded, which must never trigger activation. + void activate(); + + // LLFolderViewModelItem (non-Common) + const std::string& getName() const override { return mName; } + const std::string& getDisplayName() const override { return mName; } + const std::string& getSearchableName() const override { return mSearchable; } + std::string getSearchableDescription() const override { return mRecord.mDescription; } + std::string getSearchableCreatorName() const override { return LLStringUtil::null; } + std::string getSearchableUUIDString() const override { return mUUID.asString(); } + + LLPointer getIcon() const override; + LLFontGL::StyleFlags getLabelStyle() const override; + std::string getLabelSuffix() const override; + + void openItem(void) override; + void closeItem(void) override {} + void selectItem(void) override {} + void navigateToFolder(bool new_window = false, bool change_mode = false) override {} + + bool isFavorite() const override { return false; } + bool isItemRenameable() const override { return false; } + bool renameItem(const std::string& new_name) override { return false; } + bool isItemMovable(void) const override { return false; } + void move(LLFolderViewModelItem* parent_listener) override {} + bool isItemRemovable(bool check_worn = true) const override { return false; } + bool isItemInTrash(void) const override { return false; } + bool removeItem() override { return false; } + void removeBatch(std::vector& batch) override {} + bool isItemCopyable(bool can_copy_as_link = true) const override { return true; } + bool copyToClipboard() const override { return false; } + bool cutToClipboard() override { return false; } + bool isClipboardPasteable() const override { return false; } + void pasteFromClipboard() override {} + void pasteLinkFromClipboard() override {} + bool isAgentInventory() const override { return false; } + bool isAgentInventoryRoot() const override { return false; } + void buildContextMenu(LLMenuGL& menu, U32 flags) override {} + + bool hasChildren() const override { return getChildrenCount() > 0; } + + bool dragOrDrop(MASK mask, bool drop, EDragAndDropType cargo_type, + void* cargo_data, std::string& tooltip_msg) override { return false; } + + // Real filtering (mirrors LLFolderViewModelItemInventory::filter). + bool filter(LLFolderViewFilter& filter) override; + +private: + void rebuildSearchable(); + + EItemType mItemType; + LLUUID mUUID; + std::string mName; + std::string mSearchable; // lowercased name+desc+uuid + S32 mLinkOrder = 0; + S32 mPropsRetries = 0; + ALObjectProperties::Record mRecord; + ALFloaterSceneExplorer* mFloater; +}; + +// ============================================================================ +// Folder widget +// +// Scene objects (linksets, single prims, avatars, attachment roots) are all +// represented as folders so they sort together in one list regardless of prim +// count (LLFolderViewFolder lays out sub-folders above leaf items, which would +// otherwise force multi-prim linksets above single prims). Childless folders +// simply draw no disclosure arrow. This subclass restores leaf-style activation: +// a double-click acts on the object (focus/edit/inspect) instead of toggling, +// while the disclosure arrow still expands multi-prim linksets. Structural +// containers (region/category/attachment-point) keep the default toggle. +// ============================================================================ +class ALSceneExplorerFolder final : public LLFolderViewFolder +{ +public: + typedef LLFolderViewFolder::Params Params; // LLFolderViewFolder has no Params of its own + ALSceneExplorerFolder(const Params& p) : LLFolderViewFolder(p) {} + ~ALSceneExplorerFolder() override = default; + + bool handleDoubleClick(S32 x, S32 y, MASK mask) override; +}; + +#endif // AL_SCENEEXPLORERMODEL_H diff --git a/indra/newview/app_settings/settings_alchemy.xml b/indra/newview/app_settings/settings_alchemy.xml index 48f2c098e5..1647c0c41a 100644 --- a/indra/newview/app_settings/settings_alchemy.xml +++ b/indra/newview/app_settings/settings_alchemy.xml @@ -2,6 +2,83 @@ + ALSceneExplorerActivateAction + + Comment + Scene Explorer: double-click / Enter action (0=focus, 1=edit, 2=inspect, 3=select). + Persist + 1 + Type + U32 + Value + 0 + + ALSceneExplorerFlagFilter + + Comment + Scene Explorer: feature-flag filter mask (ALObjectProperties::EFlag bits; all set bits required). + Persist + 1 + Type + U32 + Value + 0 + + ALSceneExplorerLimitRadius + + Comment + Scene Explorer: only show objects within ALSceneExplorerRadius meters of the agent. + Persist + 1 + Type + Boolean + Value + 0 + + ALSceneExplorerOwnerFilter + + Comment + Scene Explorer: owner filter (0=any, 1=mine, 2=group, 3=others). + Persist + 1 + Type + U32 + Value + 0 + + ALSceneExplorerRadius + + Comment + Scene Explorer: radius in meters for the limit-radius filter. + Persist + 1 + Type + F32 + Value + 64.0 + + ALSceneExplorerShowAvatars + + Comment + Scene Explorer: include avatars and their attachments in the tree. + Persist + 1 + Type + Boolean + Value + 1 + + ALSceneExplorerSortOrder + + Comment + Scene Explorer: tree sort order (0=distance, 1=name, 2=land impact, 3=triangles, 4=type). + Persist + 1 + Type + U32 + Value + 0 + NumpadControl Comment diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp index c35130d22a..bd8214202b 100644 --- a/indra/newview/llselectmgr.cpp +++ b/indra/newview/llselectmgr.cpp @@ -51,6 +51,7 @@ #include "llquaternion.h" // viewer includes +#include "alobjectproperties.h" #include "llagent.h" #include "llagentcamera.h" #include "llattachmentsmgr.h" @@ -6327,7 +6328,14 @@ void LLSelectMgr::processObjectProperties(LLMessageSystem* msg, void** user_data if (!node) { - LL_WARNS() << "Couldn't find object " << id << " selected." << LL_ENDL; + // The Scene Explorer probes properties via bulk select/deselect + // without creating select nodes; replies it is awaiting (tracked + // per-id, so late arrivals after it closes are covered too) are + // expected to have no node and warrant no warning. + if (!ALObjectPropertiesCache::isExpectedReply(id)) + { + LL_WARNS() << "Couldn't find object " << id << " selected." << LL_ENDL; + } } else { diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index 7c46e9980a..f0729376ae 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -151,6 +151,7 @@ #include "llproductinforequest.h" #include "llqueryflags.h" #include "llsecapi.h" +#include "alobjectproperties.h" #include "llselectmgr.h" #include "llsky.h" #include "llstatview.h" @@ -3019,8 +3020,10 @@ void register_viewer_callbacks(LLMessageSystem* msg) msg->setHandlerFuncFast(_PREHASH_ImprovedInstantMessage, process_improved_im); msg->setHandlerFuncFast(_PREHASH_ScriptQuestion, process_script_question); - msg->setHandlerFuncFast(_PREHASH_ObjectProperties, LLSelectMgr::processObjectProperties); - msg->setHandlerFuncFast(_PREHASH_ObjectPropertiesFamily, LLSelectMgr::processObjectPropertiesFamily); + // ALObjectPropertiesCache caches object properties for the Scene Explorer + // (and other non-selection consumers), then forwards to LLSelectMgr. + msg->setHandlerFuncFast(_PREHASH_ObjectProperties, ALObjectPropertiesCache::processObjectProperties); + msg->setHandlerFuncFast(_PREHASH_ObjectPropertiesFamily, ALObjectPropertiesCache::processObjectPropertiesFamily); msg->setHandlerFuncFast(_PREHASH_ForceObjectSelect, LLSelectMgr::processForceObjectSelect); msg->setHandlerFuncFast(_PREHASH_MoneyBalanceReply, process_money_balance_reply); diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp index 818f4d4a97..8d1bb0a9b5 100644 --- a/indra/newview/llviewerfloaterreg.cpp +++ b/indra/newview/llviewerfloaterreg.cpp @@ -115,6 +115,7 @@ #include "llfloaterimagepreview.h" #include "llfloaterimsession.h" #include "llfloaterinspect.h" +#include "alfloatersceneexplorer.h" #include "llfloaterinventorysettings.h" #include "llfloaterinventorythumbnailshelper.h" #include "llfloaterjoystick.h" @@ -437,6 +438,7 @@ void LLViewerFloaterReg::registerFloaters() LLFloaterReg::add("incoming_call", "floater_incoming_call.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("inventory", "floater_my_inventory.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("inspect", "floater_inspect.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); + LLFloaterReg::add("scene_explorer", "floater_scene_explorer.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("inventory_thumbnails_helper", "floater_inventory_thumbnails_helper.xml", (LLFloaterBuildFunc) &LLFloaterReg::build); LLFloaterReg::add("item_properties", "floater_item_properties.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("task_properties", "floater_task_properties.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); diff --git a/indra/newview/llviewerobjectlist.cpp b/indra/newview/llviewerobjectlist.cpp index 5fde88cc65..0017af7993 100644 --- a/indra/newview/llviewerobjectlist.cpp +++ b/indra/newview/llviewerobjectlist.cpp @@ -1128,67 +1128,94 @@ void LLViewerObjectList::fetchObjectCostsCoro(std::string url) return; } - LLSD idList(LLSD::emptyArray()); - - for (uuid_set_t::iterator it = diff.begin(); it != diff.end(); ++it) - { - idList.append(*it); - } - mPendingObjectCost.insert(diff.begin(), diff.end()); - LLSD postData = LLSD::emptyMap(); - - postData["object_ids"] = idList; - - LLSD result = httpAdapter->postAndSuspend(httpRequest, url, postData); + // The GetObjectCost capability rejects requests that ask for too many + // objects at once ("Maximum number of resource cost requests allowed is + // 500"), so split large requests into chunks issued sequentially. Stay + // comfortably under the limit since it is enforced at the boundary. + constexpr size_t MAX_OBJECTS_PER_COST_REQUEST = 256; - LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; - LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); - - if (!status || result.has("error")) + for (uuid_set_t::iterator chunk_begin = diff.begin(); chunk_begin != diff.end(); ) { - if (result.has("error")) + LLSD idList(LLSD::emptyArray()); + uuid_set_t::iterator it = chunk_begin; + for (size_t count = 0; count < MAX_OBJECTS_PER_COST_REQUEST && it != diff.end(); ++count, ++it) { - LL_WARNS() << "Application level error when fetching object " - << "cost. Message: " << result["error"]["message"].asString() - << ", identifier: " << result["error"]["identifier"].asString() - << LL_ENDL; - - // TODO*: Adaptively adjust request size if the - // service says we've requested too many and retry + idList.append(*it); } - reportObjectCostFailure(idList); + chunk_begin = it; - return; - } + LLSD postData = LLSD::emptyMap(); + postData["object_ids"] = idList; - // Success, grab the resource cost and linked set costs - // for an object if one was returned - for (LLSD::array_iterator it = idList.beginArray(); it != idList.endArray(); ++it) - { - LLUUID objectId = it->asUUID(); + LLSD result; + try + { + result = httpAdapter->postAndSuspend(httpRequest, url, postData); + } + catch (...) + { + // Coroutine teardown mid-sequence (e.g. LLCoros::Stop at disconnect). + // Retire this run's unresolved ids so they aren't stranded in + // mPendingObjectCost, which would exclude them from every future + // fetch via the set_difference above. + for (LLSD::array_const_iterator cit = idList.beginArray(); cit != idList.endArray(); ++cit) + { + mPendingObjectCost.erase(cit->asUUID()); + } + for (uuid_set_t::iterator rest = chunk_begin; rest != diff.end(); ++rest) + { + mPendingObjectCost.erase(*rest); + } + throw; + } - // Object could have been added to the mStaleObjectCost after request started - mStaleObjectCost.erase(objectId); - mPendingObjectCost.erase(objectId); + LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; + LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); - // Check to see if the request contains data for the object - if (result.has(it->asString())) + if (!status || result.has("error")) { - LLSD objectData = result[it->asString()]; - - F32 linkCost = (F32)objectData["linked_set_resource_cost"].asReal(); - F32 objectCost = (F32)objectData["resource_cost"].asReal(); - F32 physicsCost = (F32)objectData["physics_cost"].asReal(); - F32 linkPhysicsCost = (F32)objectData["linked_set_physics_cost"].asReal(); + if (result.has("error")) + { + LL_WARNS() << "Application level error when fetching object " + << "cost. Message: " << result["error"]["message"].asString() + << ", identifier: " << result["error"]["identifier"].asString() + << LL_ENDL; + } + reportObjectCostFailure(idList); - gObjectList.updateObjectCost(objectId, objectCost, linkCost, physicsCost, linkPhysicsCost); + // A failed chunk shouldn't abort the remaining ones. + continue; } - else + + // Success, grab the resource cost and linked set costs + // for an object if one was returned + for (LLSD::array_iterator result_it = idList.beginArray(); result_it != idList.endArray(); ++result_it) { - // TODO*: Give user feedback about the missing data? - gObjectList.onObjectCostFetchFailure(objectId); + LLUUID objectId = result_it->asUUID(); + + // Object could have been added to the mStaleObjectCost after request started + mStaleObjectCost.erase(objectId); + mPendingObjectCost.erase(objectId); + + // Check to see if the request contains data for the object + if (result.has(result_it->asString())) + { + LLSD objectData = result[result_it->asString()]; + + F32 linkCost = (F32)objectData["linked_set_resource_cost"].asReal(); + F32 objectCost = (F32)objectData["resource_cost"].asReal(); + F32 physicsCost = (F32)objectData["physics_cost"].asReal(); + F32 linkPhysicsCost = (F32)objectData["linked_set_physics_cost"].asReal(); + + gObjectList.updateObjectCost(objectId, objectCost, linkCost, physicsCost, linkPhysicsCost); + } + else + { + // TODO*: Give user feedback about the missing data? + gObjectList.onObjectCostFetchFailure(objectId); + } } } diff --git a/indra/newview/skins/default/xui/en/floater_scene_explorer.xml b/indra/newview/skins/default/xui/en/floater_scene_explorer.xml new file mode 100644 index 0000000000..25f97dad98 --- /dev/null +++ b/indra/newview/skins/default/xui/en/floater_scene_explorer.xml @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + +