From ebf9cd31d789c0556d741987e95a961edd2955d0 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Wed, 5 Aug 2026 17:15:43 -0700 Subject: Move object publishing tracking to its own manager in the viewer. --- indra/newview/CMakeLists.txt | 2 + indra/newview/llpublishedobjectmgr.cpp | 838 +++++++++++++++++++++++++++++++++ indra/newview/llpublishedobjectmgr.h | 196 ++++++++ indra/newview/llscripteditorws.cpp | 594 +++++------------------ indra/newview/llscripteditorws.h | 39 +- indra/newview/llselectmgr.cpp | 2 +- 6 files changed, 1165 insertions(+), 506 deletions(-) create mode 100644 indra/newview/llpublishedobjectmgr.cpp create mode 100644 indra/newview/llpublishedobjectmgr.h diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 0b62bdf0b0..362199b1ca 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -547,6 +547,7 @@ set(viewer_SOURCE_FILES llpreview.cpp llpreviewanim.cpp llpreviewgesture.cpp + llpublishedobjectmgr.cpp llpreviewnotecard.cpp llpreviewscript.cpp llpreviewsound.cpp @@ -1218,6 +1219,7 @@ set(viewer_HEADER_FILES llpreview.h llpreviewanim.h llpreviewgesture.h + llpublishedobjectmgr.h llpreviewnotecard.h llpreviewscript.h llpreviewsound.h diff --git a/indra/newview/llpublishedobjectmgr.cpp b/indra/newview/llpublishedobjectmgr.cpp new file mode 100644 index 0000000000..2c2eff58a5 --- /dev/null +++ b/indra/newview/llpublishedobjectmgr.cpp @@ -0,0 +1,838 @@ +/** + * @file llpublishedobjectmgr.cpp + * @brief Published object state/logic manager extracted from llscripteditorws + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" +#include "llpublishedobjectmgr.h" + +#include "llscripteditorws.h" + +#include "llinventorydefines.h" +#include "llselectmgr.h" +#include "llviewerinventory.h" +#include "llviewerobject.h" +#include "llviewerobjectlist.h" +#include "llviewerregion.h" +#include "llvoinventorylistener.h" + +namespace +{ + std::string nv_string(LLViewerObject* obj, const char* key) + { + if (!obj) + { + return std::string(); + } + LLNameValue* nv = obj->getNVPair(key); + if (!nv) + { + return std::string(); + } + const char* s = nv->getString(); + if (!s || s[0] == '\0') + { + return std::string(); + } + return std::string(s); + } + + std::string get_prim_name(LLViewerObject* obj) + { + std::string name = nv_string(obj, "Name"); + if (!name.empty()) + { + return name; + } + + if (!obj) + { + return std::string(); + } + + LLSelectNode* node = LLSelectMgr::instance().getSelection()->findNode(obj); + return (node && !node->mName.empty()) ? node->mName : std::string(); + } +} + +class LLPublishedPrimListener : public LLVOInventoryListener +{ +public: + LLPublishedPrimListener(LLScriptEditorWSServer* server, const LLUUID& object_id, const LLUUID& prim_id, + LLViewerObject* object) + : mServer(server) + , mObjectID(object_id) + , mPrimID(prim_id) + { + registerVOInventoryListener(object, nullptr); + } + + ~LLPublishedPrimListener() override = default; + + void inventoryChanged(LLViewerObject* object, + LLInventoryObject::object_list_t* inventory, + S32 serial_num, void* user_data) override + { + if (mServer) + { + if (mServer->isObjectPublished(mObjectID)) + { + mServer->onPrimInventoryChanged(mObjectID, mPrimID); + } + else + { + mServer->onPrimInventoryReady(mObjectID, mPrimID); + } + } + } + + const LLUUID& getObjectID() const { return mObjectID; } + const LLUUID& getPrimID() const { return mPrimID; } + +private: + LLScriptEditorWSServer* mServer; + LLUUID mObjectID; + LLUUID mPrimID; +}; + +LLPublishedObjectMgr::LLPublishedObjectMgr(LLScriptEditorWSServer* server) + : mServer(server) +{ +} + +LLPublishedObjectMgr::~LLPublishedObjectMgr() = default; + +LLPublishedObjectMgr::PublishedObjectInfo::PublishedObjectInfo() = default; +LLPublishedObjectMgr::PublishedObjectInfo::~PublishedObjectInfo() = default; +LLPublishedObjectMgr::PublishedObjectInfo::PublishedObjectInfo(PublishedObjectInfo&&) noexcept = default; +LLPublishedObjectMgr::PublishedObjectInfo& LLPublishedObjectMgr::PublishedObjectInfo::operator=(PublishedObjectInfo&&) noexcept = default; + +LLPublishedObjectMgr::PendingPublish::PendingPublish() = default; +LLPublishedObjectMgr::PendingPublish::~PendingPublish() = default; +LLPublishedObjectMgr::PendingPublish::PendingPublish(PendingPublish&&) noexcept = default; +LLPublishedObjectMgr::PendingPublish& LLPublishedObjectMgr::PendingPublish::operator=(PendingPublish&&) noexcept = default; + +void LLPublishedObjectMgr::beginPendingPublish(const LLUUID& object_id, const std::vector& prims) +{ + PendingPublish pending; + pending.mObjectID = object_id; + for (LLViewerObject* prim : prims) + { + pending.mPendingPrims.insert(prim->getID()); + auto listener = std::make_unique( + mServer, object_id, prim->getID(), prim); + pending.mListeners.push_back(std::move(listener)); + } + mPendingPublishes[object_id] = std::move(pending); +} + +bool LLPublishedObjectMgr::hasPendingPublish(const LLUUID& object_id) const +{ + return mPendingPublishes.find(object_id) != mPendingPublishes.end(); +} + +bool LLPublishedObjectMgr::markPendingPublishPrimReady(const LLUUID& object_id, const LLUUID& prim_id) +{ + auto it = mPendingPublishes.find(object_id); + if (it == mPendingPublishes.end()) + { + return false; + } + + it->second.mPendingPrims.erase(prim_id); + return it->second.mPendingPrims.empty(); +} + +std::vector> LLPublishedObjectMgr::takePendingPublishListeners(const LLUUID& object_id) +{ + auto it = mPendingPublishes.find(object_id); + if (it == mPendingPublishes.end()) + { + return {}; + } + + auto listeners = std::move(it->second.mListeners); + mPendingPublishes.erase(it); + return listeners; +} + +void LLPublishedObjectMgr::cancelPendingPublish(const LLUUID& object_id) +{ + mPendingPublishes.erase(object_id); +} + +void LLPublishedObjectMgr::cancelPendingPublishWithCleanup(const LLUUID& object_id) +{ + auto it = mPendingPublishes.find(object_id); + if (it == mPendingPublishes.end()) + { + return; + } + + it->second.mListeners.clear(); + mPendingPublishes.erase(it); +} + +LLPublishedObjectMgr::PublishedObjectInfo* LLPublishedObjectMgr::getPublished(const LLUUID& object_id) +{ + auto it = mPublishedObjects.find(object_id); + if (it == mPublishedObjects.end()) + { + return nullptr; + } + + return &it->second; +} + +const LLPublishedObjectMgr::PublishedObjectInfo* LLPublishedObjectMgr::getPublished(const LLUUID& object_id) const +{ + auto it = mPublishedObjects.find(object_id); + if (it == mPublishedObjects.end()) + { + return nullptr; + } + + return &it->second; +} + +bool LLPublishedObjectMgr::reservePendingItemCreate(const LLUUID& prim_id, std::string&& pump_name) +{ + auto it = mPendingItemCreates.find(prim_id); + if (it != mPendingItemCreates.end()) + { + return false; + } + + mPendingItemCreates[prim_id] = std::move(pump_name); + return true; +} + +bool LLPublishedObjectMgr::consumePendingItemCreate(const LLUUID& prim_id, std::string& pump_name) +{ + auto it = mPendingItemCreates.find(prim_id); + if (it == mPendingItemCreates.end()) + { + return false; + } + + pump_name = it->second; + mPendingItemCreates.erase(it); + return true; +} + +void LLPublishedObjectMgr::clearPendingItemCreate(const LLUUID& prim_id) +{ + mPendingItemCreates.erase(prim_id); +} + +LLSD LLPublishedObjectMgr::buildPrimInventoryLLSD(LLViewerObject* object) const +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; + LLSD items = LLSD::emptyArray(); + if (!object) + { + return items; + } + + LLInventoryObject::object_list_t contents; + object->getInventoryContents(contents); + + for (const auto& obj : contents) + { + LLInventoryItem* item = dynamic_cast(obj.get()); + if (!item) + { + continue; + } + + LLAssetType::EType type = item->getType(); + if (type != LLAssetType::AT_LSL_TEXT && type != LLAssetType::AT_NOTECARD) + { + continue; + } + + LLSD entry; + entry["item_id"] = item->getUUID(); + entry["name"] = item->getName(); + entry["description"] = item->getDescription(); + entry["type"] = (type == LLAssetType::AT_LSL_TEXT) ? "script" : "notecard"; + + if (type == LLAssetType::AT_LSL_TEXT) + { + U8 subtype = item->getInventorySubType(); + entry["subtype"] = static_cast(subtype); + + const std::string& runtime = item->getRuntime(); + if (!runtime.empty()) + { + entry["vm"] = runtime; + } + + LLViewerInventoryItem* viewer_item = dynamic_cast(item); + if (viewer_item) + { + entry["running"] = viewer_item->getIsRunning(); + entry["faulted"] = viewer_item->getIsFaulted(); + } + } + + const LLPermissions& perms = item->getPermissions(); + LLSD perm_entry; + perm_entry["owner"] = static_cast(perms.getMaskOwner()); + perm_entry["next_owner"] = static_cast(perms.getMaskNextOwner()); + entry["permissions"] = perm_entry; + + entry["creator_id"] = perms.getCreator(); + + items.append(entry); + } + + return items; +} + +LLSD LLPublishedObjectMgr::buildPublishedObjectLLSD(LLViewerObject* root) const +{ + LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; + LLSD pub; + pub["object_id"] = root->getID(); + pub["object_name"] = get_prim_name(root); + pub["object_description"] = nv_string(root, "Desc"); + pub["owner_id"] = root->mOwnerID; + if (root->getRegion()) + { + pub["region"] = root->getRegion()->getName(); + } + pub["inventory"] = buildPrimInventoryLLSD(root); + + LLSD linked_objects = LLSD::emptyArray(); + S32 link_number = 2; + for (LLViewerObject* child : root->getChildren()) + { + LLSD link; + link["link_id"] = child->getID(); + link["link_number"] = link_number++; + link["link_name"] = get_prim_name(child); + link["link_description"] = nv_string(child, "Desc"); + link["inventory"] = buildPrimInventoryLLSD(child); + linked_objects.append(link); + } + if (linked_objects.size() > 0) + { + pub["linked_objects"] = linked_objects; + } + + return pub; +} + +LLSD LLPublishedObjectMgr::buildObjectListLLSD() const +{ + LLSD objects = LLSD::emptyArray(); + for (const auto& [object_id, info] : mPublishedObjects) + { + LLViewerObject* root = gObjectList.findObject(object_id); + if (!root) + { + LL_DEBUGS("ScriptEditorWS") << "object.list: skipping " << object_id + << " (no longer in scene)" << LL_ENDL; + continue; + } + + LLSD pub; + pub["object_id"] = info.mObjectID; + pub["object_name"] = info.mObjectName; + pub["object_description"] = info.mObjectDescription; + pub["owner_id"] = info.mOwnerID; + if (!info.mRegionName.empty()) + { + pub["region"] = info.mRegionName; + } + pub["inventory"] = buildPrimInventoryLLSD(root); + + LLSD linked_objects = LLSD::emptyArray(); + for (const auto& prim_info : info.mPrims) + { + if (prim_info.mLinkNumber == 1) + { + continue; + } + + LLViewerObject* child = gObjectList.findObject(prim_info.mPrimID); + if (!child) + { + continue; + } + + LLSD link; + link["link_id"] = prim_info.mPrimID; + link["link_number"] = prim_info.mLinkNumber; + link["link_name"] = prim_info.mPrimName; + link["inventory"] = buildPrimInventoryLLSD(child); + linked_objects.append(link); + } + if (linked_objects.size() > 0) + { + pub["linked_objects"] = linked_objects; + } + + objects.append(pub); + } + + return objects; +} + +bool LLPublishedObjectMgr::buildLinksetUpdateLLSD( + const LLUUID& root_id, LLSD& update) const +{ + const PublishedObjectInfo* info = getPublished(root_id); + if (!info) + { + return false; + } + + LLSD linked_objects = LLSD::emptyArray(); + for (const PublishedPrimInfo& prim_info : info->mPrims) + { + if (prim_info.mPrimID == root_id) + { + continue; + } + + LLSD entry; + entry["link_id"] = prim_info.mPrimID; + entry["link_number"] = prim_info.mLinkNumber; + + LLViewerObject* prim = gObjectList.findObject(prim_info.mPrimID); + std::string link_name = prim ? get_prim_name(prim) : std::string(); + if (link_name.empty()) + { + link_name = prim_info.mPrimName; + } + entry["link_name"] = link_name; + entry["inventory"] = prim ? buildPrimInventoryLLSD(prim) : LLSD::emptyArray(); + + linked_objects.append(entry); + } + + update = LLSD(); + update["object_id"] = root_id; + update["linked_objects"] = linked_objects; + return true; +} + +bool LLPublishedObjectMgr::reconcileLinksetChildAdded( + const LLUUID& root_id, + LLViewerObject* child, + F64 request_start_sec) +{ + PublishedObjectInfo* info = getPublished(root_id); + if (!info || !child) + { + return false; + } + + const LLUUID child_id = child->getID(); + + info->mPrims.erase( + std::remove_if( + info->mPrims.begin(), + info->mPrims.end(), + [&](const PublishedPrimInfo& p) { return p.mPrimID == child_id; }), + info->mPrims.end()); + + PublishedPrimInfo prim_info; + prim_info.mPrimID = child_id; + prim_info.mPrimName = get_prim_name(child); + prim_info.mLinkNumber = static_cast(info->mPrims.size()) + 1; + prim_info.mInventorySerial = -1; + info->mPrims.push_back(prim_info); + + auto listener = std::make_unique( + mServer, root_id, child_id, child); + info->mListeners.push_back(std::move(listener)); + + mInventoryRequestStartSec[child_id] = request_start_sec; + mNewChildPrims[root_id].insert(child_id); + return true; +} + +bool LLPublishedObjectMgr::reconcileLinksetChildRemoved( + const LLUUID& root_id, const LLUUID& child_id) +{ + PublishedObjectInfo* info = getPublished(root_id); + if (!info) + { + return false; + } + + info->mPrims.erase( + std::remove_if( + info->mPrims.begin(), + info->mPrims.end(), + [&](const PublishedPrimInfo& p) { return p.mPrimID == child_id; }), + info->mPrims.end()); + + info->mListeners.erase( + std::remove_if( + info->mListeners.begin(), + info->mListeners.end(), + [&](const std::unique_ptr& l) + { + return l->getPrimID() == child_id; + }), + info->mListeners.end()); + + bool root_empty_after_remove = false; + consumePendingNewChild(root_id, child_id, root_empty_after_remove); + + mInventoryRequestStartSec.erase(child_id); + + S32 link_num = 2; + for (auto& p : info->mPrims) + { + if (p.mPrimID != root_id) + { + p.mLinkNumber = link_num++; + } + } + + return true; +} + +bool LLPublishedObjectMgr::handlePrimInventoryReadyEvent( + const LLUUID& object_id, const LLUUID& prim_id) +{ + return markPendingPublishPrimReady(object_id, prim_id); +} + +LLPublishedObjectMgr::PrimInventoryEventResult +LLPublishedObjectMgr::handlePrimInventoryChangedEvent( + const LLUUID& object_id, + const LLUUID& prim_id, + LLViewerObject* prim, + F64 now_sec) +{ + PrimInventoryEventResult result; + if (!hasPublished(object_id) || !prim) + { + return result; + } + + F64 request_start_sec = 0.0; + if (consumeInventoryRequestStart(prim_id, request_start_sec)) + { + result.mTimingConsumed = true; + result.mTimingElapsedSec = llmax(0.0, now_sec - request_start_sec); + } + + InventoryChangeResult inv_result = reconcileInventoryChanged(object_id, prim_id, prim); + result.mKind = inv_result.mKind; + result.mUpdate = inv_result.mUpdate; + + if (result.mKind == InventoryChangeKind::ROOT_INVENTORY_UPDATE || + result.mKind == InventoryChangeKind::CHILD_INVENTORY_UPDATE) + { + std::string pending_item_create_pump; + if (consumePendingItemCreate(prim_id, pending_item_create_pump)) + { + result.mHasPendingItemCreate = true; + result.mPendingItemCreatePump = pending_item_create_pump; + } + } + + return result; +} + +LLPublishedObjectMgr::InventoryChangeResult +LLPublishedObjectMgr::reconcileInventoryChanged( + const LLUUID& object_id, + const LLUUID& prim_id, + LLViewerObject* prim) +{ + InventoryChangeResult result; + PublishedObjectInfo* pub_info = getPublished(object_id); + if (!pub_info || !prim) + { + return result; + } + + bool root_empty_after_remove = false; + if (consumePendingNewChild(object_id, prim_id, root_empty_after_remove)) + { + for (auto& p : pub_info->mPrims) + { + if (p.mPrimID == prim_id) + { + p.mPrimName = get_prim_name(prim); + p.mInventorySerial = 0; + break; + } + } + result.mKind = root_empty_after_remove + ? InventoryChangeKind::CHILD_READY_FLUSH_NOW + : InventoryChangeKind::CHILD_READY_WAIT; + return result; + } + + result.mUpdate = LLSD(); + result.mUpdate["object_id"] = object_id; + LLSD inv = buildPrimInventoryLLSD(prim); + if (prim_id == object_id) + { + result.mUpdate["inventory"] = inv; + result.mKind = InventoryChangeKind::ROOT_INVENTORY_UPDATE; + } + else + { + LLSD modified_entry; + modified_entry["link_id"] = prim_id; + modified_entry["inventory"] = inv; + LLSD modified_arr = LLSD::emptyArray(); + modified_arr.append(modified_entry); + result.mUpdate["changes"]["linked_objects"]["modified"] = modified_arr; + result.mKind = InventoryChangeKind::CHILD_INVENTORY_UPDATE; + } + return result; +} + +bool LLPublishedObjectMgr::applyPropertyChange( + const LLUUID& root_id, + const LLUUID& prim_id, + const std::string& name, + const std::string& desc, + LLSD& update) +{ + PublishedObjectInfo* pub_info = getPublished(root_id); + if (!pub_info) + { + return false; + } + + update = LLSD(); + update["object_id"] = root_id; + + if (prim_id == root_id) + { + bool name_changed = (pub_info->mObjectName != name); + bool desc_changed = (pub_info->mObjectDescription != desc); + if (!name_changed && !desc_changed) + { + return false; + } + + if (name_changed) + { + pub_info->mObjectName = name; + update["object_name"] = name; + } + if (desc_changed) + { + pub_info->mObjectDescription = desc; + update["object_description"] = desc; + } + return true; + } + + auto prim_it = std::find_if(pub_info->mPrims.begin(), pub_info->mPrims.end(), + [&](const PublishedPrimInfo& p) { return p.mPrimID == prim_id; }); + if (prim_it == pub_info->mPrims.end()) + { + return false; + } + if (prim_it->mPrimName == name) + { + return false; + } + + prim_it->mPrimName = name; + LLSD modified_entry; + modified_entry["link_id"] = prim_id; + modified_entry["link_name"] = name; + LLSD modified_arr = LLSD::emptyArray(); + modified_arr.append(modified_entry); + update["changes"]["linked_objects"]["modified"] = modified_arr; + return true; +} + +bool LLPublishedObjectMgr::hasActiveLinksetFlushTimer(const LLUUID& root_id) const +{ + auto it = mLinksetFlushTimers.find(root_id); + if (it == mLinksetFlushTimers.end()) + { + return false; + } + + return !it->second.expired(); +} + +void LLPublishedObjectMgr::setLinksetFlushTimer( + const LLUUID& root_id, const std::weak_ptr& timer) +{ + mLinksetFlushTimers[root_id] = timer; +} + +bool LLPublishedObjectMgr::cancelLinksetFlushTimer(const LLUUID& root_id) +{ + auto it = mLinksetFlushTimers.find(root_id); + if (it == mLinksetFlushTimers.end()) + { + return false; + } + + if (auto locked = it->second.lock()) + { + delete locked.get(); + } + + mLinksetFlushTimers.erase(it); + return true; +} + +void LLPublishedObjectMgr::clearLinksetFlushTimer(const LLUUID& root_id) +{ + mLinksetFlushTimers.erase(root_id); +} + +bool LLPublishedObjectMgr::consumeInventoryRequestStart( + const LLUUID& prim_id, F64& start_sec) +{ + auto it = mInventoryRequestStartSec.find(prim_id); + if (it == mInventoryRequestStartSec.end()) + { + return false; + } + + start_sec = it->second; + mInventoryRequestStartSec.erase(it); + return true; +} + +bool LLPublishedObjectMgr::markPrimInventorySerialAndDetectChange( + const LLUUID& root_id, const LLUUID& prim_id, S16 inventory_serial) +{ + if (inventory_serial < 0) + { + return false; + } + + PublishedObjectInfo* info = getPublished(root_id); + if (!info) + { + return false; + } + + auto it = std::find_if( + info->mPrims.begin(), + info->mPrims.end(), + [&](const PublishedPrimInfo& p) + { + return p.mPrimID == prim_id; + }); + if (it == info->mPrims.end()) + { + return false; + } + + if (it->mInventorySerial == inventory_serial) + { + return false; + } + + it->mInventorySerial = inventory_serial; + return true; +} + +bool LLPublishedObjectMgr::consumePendingNewChild( + const LLUUID& root_id, const LLUUID& child_id, bool& root_empty_after_remove) +{ + root_empty_after_remove = false; + auto root_it = mNewChildPrims.find(root_id); + if (root_it == mNewChildPrims.end()) + { + return false; + } + + auto child_it = root_it->second.find(child_id); + if (child_it == root_it->second.end()) + { + return false; + } + + root_it->second.erase(child_it); + if (root_it->second.empty()) + { + root_empty_after_remove = true; + mNewChildPrims.erase(root_it); + } + + return true; +} + +LLPublishedObjectMgr::PublishedObjectInfo& LLPublishedObjectMgr::finalizePendingPublish( + const LLUUID& object_id, PublishedObjectInfo&& info) +{ + PublishedObjectInfo& published_info = mPublishedObjects[object_id]; + published_info = std::move(info); + published_info.mListeners = takePendingPublishListeners(object_id); + return published_info; +} + +bool LLPublishedObjectMgr::cleanupObjectStateForUnpublish(const LLUUID& object_id) +{ + const bool was_published = hasPublished(object_id); + + cancelPendingPublishWithCleanup(object_id); + clearPublishedListeners(object_id); + erasePublished(object_id); + + cancelLinksetFlushTimer(object_id); + clearPendingNewChildren(object_id); + + return was_published; +} + +void LLPublishedObjectMgr::clearPublishedListeners(const LLUUID& object_id) +{ + auto pub_info = getPublished(object_id); + if (!pub_info) + { + return; + } + + pub_info->mListeners.clear(); +} + +void LLPublishedObjectMgr::clearAllStateWithListenerCleanup() +{ + for (auto& [id, pending] : mPendingPublishes) + { + pending.mListeners.clear(); + } + mPendingPublishes.clear(); + + for (auto& [id, info] : mPublishedObjects) + { + info.mListeners.clear(); + } + mPublishedObjects.clear(); +} diff --git a/indra/newview/llpublishedobjectmgr.h b/indra/newview/llpublishedobjectmgr.h new file mode 100644 index 0000000000..4d4bd55aff --- /dev/null +++ b/indra/newview/llpublishedobjectmgr.h @@ -0,0 +1,196 @@ +/** + * @file llpublishedobjectmgr.h + * @brief Published object state/logic manager extracted from llscripteditorws + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "llsd.h" +#include "lluuid.h" +#include "lleventtimer.h" +#include "stdtypes.h" + +#include +#include +#include +#include +#include + +class LLScriptEditorWSServer; +class LLPublishedPrimListener; +class LLViewerObject; + +class LLPublishedObjectMgr +{ +public: + struct PublishedPrimInfo + { + LLUUID mPrimID; + std::string mPrimName; + S32 mLinkNumber; + S16 mInventorySerial; + }; + + struct PublishedObjectInfo + { + PublishedObjectInfo(); + ~PublishedObjectInfo(); + PublishedObjectInfo(PublishedObjectInfo&&) noexcept; + PublishedObjectInfo& operator=(PublishedObjectInfo&&) noexcept; + PublishedObjectInfo(const PublishedObjectInfo&) = delete; + PublishedObjectInfo& operator=(const PublishedObjectInfo&) = delete; + + LLUUID mObjectID; + LLUUID mOwnerID; + std::string mObjectName; + std::string mObjectDescription; + std::string mRegionName; + std::vector mPrims; + std::vector> mListeners; + }; + + struct PendingPublish + { + PendingPublish(); + ~PendingPublish(); + PendingPublish(PendingPublish&&) noexcept; + PendingPublish& operator=(PendingPublish&&) noexcept; + PendingPublish(const PendingPublish&) = delete; + PendingPublish& operator=(const PendingPublish&) = delete; + + LLUUID mObjectID; + std::set mPendingPrims; + std::vector> mListeners; + }; + + enum class InventoryChangeKind + { + NOT_PUBLISHED, + CHILD_READY_WAIT, + CHILD_READY_FLUSH_NOW, + ROOT_INVENTORY_UPDATE, + CHILD_INVENTORY_UPDATE + }; + + struct InventoryChangeResult + { + InventoryChangeKind mKind{ InventoryChangeKind::NOT_PUBLISHED }; + LLSD mUpdate; + }; + + struct PrimInventoryEventResult + { + InventoryChangeKind mKind{ InventoryChangeKind::NOT_PUBLISHED }; + LLSD mUpdate; + bool mTimingConsumed{ false }; + F64 mTimingElapsedSec{ 0.0 }; + bool mHasPendingItemCreate{ false }; + std::string mPendingItemCreatePump; + }; + + explicit LLPublishedObjectMgr(LLScriptEditorWSServer* server = nullptr); + ~LLPublishedObjectMgr(); + + bool hasPublished(const LLUUID& object_id) const { return mPublishedObjects.find(object_id) != mPublishedObjects.end(); } + void erasePublished(const LLUUID& object_id) { mPublishedObjects.erase(object_id); } + PublishedObjectInfo* getPublished(const LLUUID& object_id); + const PublishedObjectInfo* getPublished(const LLUUID& object_id) const; + + template + void forEachPublished(Fn&& fn) const + { + for (const auto& [id, info] : mPublishedObjects) + { + fn(id, info); + } + } + + LLSD buildPrimInventoryLLSD(LLViewerObject* object) const; + LLSD buildPublishedObjectLLSD(LLViewerObject* root) const; + LLSD buildObjectListLLSD() const; + bool buildLinksetUpdateLLSD(const LLUUID& root_id, LLSD& update) const; + bool reconcileLinksetChildAdded(const LLUUID& root_id, LLViewerObject* child, F64 request_start_sec); + bool reconcileLinksetChildRemoved(const LLUUID& root_id, const LLUUID& child_id); + bool handlePrimInventoryReadyEvent(const LLUUID& object_id, const LLUUID& prim_id); + PrimInventoryEventResult handlePrimInventoryChangedEvent( + const LLUUID& object_id, + const LLUUID& prim_id, + LLViewerObject* prim, + F64 now_sec); + InventoryChangeResult reconcileInventoryChanged( + const LLUUID& object_id, + const LLUUID& prim_id, + LLViewerObject* prim); + bool applyPropertyChange( + const LLUUID& root_id, + const LLUUID& prim_id, + const std::string& name, + const std::string& desc, + LLSD& update); + + void beginPendingPublish(const LLUUID& object_id, const std::vector& prims); + bool hasPendingPublish(const LLUUID& object_id) const; + bool markPendingPublishPrimReady(const LLUUID& object_id, const LLUUID& prim_id); + std::vector> takePendingPublishListeners(const LLUUID& object_id); + void cancelPendingPublish(const LLUUID& object_id); + PublishedObjectInfo& finalizePendingPublish(const LLUUID& object_id, PublishedObjectInfo&& info); + bool cleanupObjectStateForUnpublish(const LLUUID& object_id); + void clearAllStateWithListenerCleanup(); + + bool reservePendingItemCreate(const LLUUID& prim_id, std::string&& pump_name); + bool consumePendingItemCreate(const LLUUID& prim_id, std::string& pump_name); + void clearPendingItemCreate(const LLUUID& prim_id); + + bool consumePendingNewChild(const LLUUID& root_id, const LLUUID& child_id, bool& root_empty_after_remove); + void clearPendingNewChildren(const LLUUID& root_id) { mNewChildPrims.erase(root_id); } + + bool hasActiveLinksetFlushTimer(const LLUUID& root_id) const; + void setLinksetFlushTimer(const LLUUID& root_id, const std::weak_ptr& timer); + bool cancelLinksetFlushTimer(const LLUUID& root_id); + void clearLinksetFlushTimer(const LLUUID& root_id); + + void setInventoryRequestStart(const LLUUID& prim_id, F64 start_sec) { mInventoryRequestStartSec[prim_id] = start_sec; } + bool hasInventoryRequestStart(const LLUUID& prim_id) const { return mInventoryRequestStartSec.find(prim_id) != mInventoryRequestStartSec.end(); } + bool consumeInventoryRequestStart(const LLUUID& prim_id, F64& start_sec); + bool markPrimInventorySerialAndDetectChange(const LLUUID& root_id, const LLUUID& prim_id, S16 inventory_serial); + +private: + LLScriptEditorWSServer* mServer{ nullptr }; + void cancelPendingPublishWithCleanup(const LLUUID& object_id); + void clearPublishedListeners(const LLUUID& object_id); + + using published_map_t = std::map; + using pending_publish_map_t = std::map; + using pending_item_create_map_t = std::map; + using new_child_prims_map_t = std::map>; + using linkset_flush_timer_map_t = std::map>; + using inventory_request_start_map_t = std::map; + + published_map_t mPublishedObjects; + pending_publish_map_t mPendingPublishes; + pending_item_create_map_t mPendingItemCreates; + new_child_prims_map_t mNewChildPrims; + linkset_flush_timer_map_t mLinksetFlushTimers; + inventory_request_start_map_t mInventoryRequestStartSec; +}; diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp index 252a5d8bed..8f45d14c36 100644 --- a/indra/newview/llscripteditorws.cpp +++ b/indra/newview/llscripteditorws.cpp @@ -1,4 +1,4 @@ -/** +/** * @file llscripteditorws.cpp * @brief JSON-RPC 2.0 WebSocket server implementation for external script editor integration * @@ -159,49 +159,10 @@ namespace } -class LLPublishedPrimListener : public LLVOInventoryListener -{ -public: - LLPublishedPrimListener(LLScriptEditorWSServer* server, const LLUUID& object_id, const LLUUID& prim_id, - LLViewerObject* object) - : mServer(server) - , mObjectID(object_id) - , mPrimID(prim_id) - { - registerVOInventoryListener(object, nullptr); - } - - ~LLPublishedPrimListener() override = default; - - void inventoryChanged(LLViewerObject* object, - LLInventoryObject::object_list_t* inventory, - S32 serial_num, void* user_data) override - { - if (mServer) - { - if (mServer->isObjectPublished(mObjectID)) - { - mServer->onPrimInventoryChanged(mObjectID, mPrimID); - } - else - { - mServer->onPrimInventoryReady(mObjectID, mPrimID); - } - } - } - - const LLUUID& getObjectID() const { return mObjectID; } - const LLUUID& getPrimID() const { return mPrimID; } - -private: - LLScriptEditorWSServer* mServer; // non-owning; server always outlives listeners - LLUUID mObjectID; // root object this prim belongs to - LLUUID mPrimID; // this specific prim -}; - //======================================================================== LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only): - LLJSONRPCServer(name, port, local_only) + LLJSONRPCServer(name, port, local_only), + mPublishedObjectManager(this) { LL_INFOS("ScriptEditorWS") << "Created JSON-RPC script editor server: " << name << " on port " << port << LL_ENDL; @@ -362,17 +323,7 @@ void LLScriptEditorWSServer::onStopped() // Connections are already closed -- clean up all internal state silently. // Do not attempt to send notifications; the sockets are gone. - for (auto& [id, pending] : mPendingPublishes) - { - pending.mListeners.clear(); - } - mPendingPublishes.clear(); - - for (auto& [id, info] : mPublishedObjects) - { - info.mListeners.clear(); - } - mPublishedObjects.clear(); + mPublishedObjectManager.clearAllStateWithListenerCleanup(); mSubscriptions.clear(); mActiveConnections.clear(); @@ -677,54 +628,8 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c LLSD LLScriptEditorWSServer::handleObjectList() const { - LLSD objects = LLSD::emptyArray(); - for (const auto& [object_id, info] : mPublishedObjects) - { - LLViewerObject* root = gObjectList.findObject(object_id); - if (!root) - { - LL_DEBUGS("ScriptEditorWS") << "object.list: skipping " << object_id - << " (no longer in scene)" << LL_ENDL; - continue; - } - - // Use cached names from PublishedObjectInfo, but fetch live inventory - LLSD pub; - pub["object_id"] = info.mObjectID; - pub["object_name"] = info.mObjectName; - pub["object_description"] = info.mObjectDescription; - pub["owner_id"] = info.mOwnerID; - if (!info.mRegionName.empty()) - { - pub["region"] = info.mRegionName; - } - pub["inventory"] = buildPrimInventoryLLSD(root); - - LLSD linked_objects = LLSD::emptyArray(); - for (const auto& prim_info : info.mPrims) - { - if (prim_info.mLinkNumber == 1) continue; // skip root - - LLViewerObject* child = gObjectList.findObject(prim_info.mPrimID); - if (!child) continue; - - LLSD link; - link["link_id"] = prim_info.mPrimID; - link["link_number"] = prim_info.mLinkNumber; - link["link_name"] = prim_info.mPrimName; // Cached name - link["inventory"] = buildPrimInventoryLLSD(child); - linked_objects.append(link); - } - if (linked_objects.size() > 0) - { - pub["linked_objects"] = linked_objects; - } - - objects.append(pub); - } - LLSD response; - response["objects"] = objects; + response["objects"] = mPublishedObjectManager.buildObjectListLLSD(); return response; } @@ -816,9 +721,7 @@ LLSD LLScriptEditorWSServer::handleObjectScriptReset(U32 connection_id, const LL LLSD LLScriptEditorWSServer::handleObjectModify(U32 connection_id, const LLSD& params) { - // ───────────────────────────────────────────────────────────── // Step 1: Parameter Validation - // ───────────────────────────────────────────────────────────── LLUUID prim_id = params["prim_id"].asUUID(); if (prim_id.isNull()) throw LLJSONRPCConnection::InvalidParams("prim_id is required"); @@ -831,9 +734,7 @@ LLSD LLScriptEditorWSServer::handleObjectModify(U32 connection_id, const LLSD& p throw LLJSONRPCConnection::InvalidParams( "At least one property (name, description, or permissions) must be specified"); - // ───────────────────────────────────────────────────────────── // Step 2: Find and Validate Object - // ───────────────────────────────────────────────────────────── LLViewerObject* prim = gObjectList.findObject(prim_id); if (!prim) throw LLJSONRPCConnection::InvalidParams("Prim not found"); @@ -845,9 +746,7 @@ LLSD LLScriptEditorWSServer::handleObjectModify(U32 connection_id, const LLSD& p if (!prim->permModify()) throw LLJSONRPCConnection::ForbiddenError("No modify permission on object"); - // ───────────────────────────────────────────────────────────── // Step 3: Send Property Update Messages - // ───────────────────────────────────────────────────────────── LLMessageSystem* msg = gMessageSystem; LLHost host = prim->getRegion()->getHost(); U32 local_id = prim->getLocalID(); @@ -895,9 +794,7 @@ LLSD LLScriptEditorWSServer::handleObjectModify(U32 connection_id, const LLSD& p msg->sendReliable(host); } - // ───────────────────────────────────────────────────────────── // Step 4: Return Success Response - // ───────────────────────────────────────────────────────────── LLSD response; response["success"] = true; response["prim_id"] = prim_id.asString(); @@ -906,9 +803,7 @@ LLSD LLScriptEditorWSServer::handleObjectModify(U32 connection_id, const LLSD& p LLSD LLScriptEditorWSServer::handleObjectItemModify(U32 connection_id, const LLSD& params) { - // ───────────────────────────────────────────────────────────── // Step 1: Parameter Validation - // ───────────────────────────────────────────────────────────── if (!params.has("prim_id") || !params.has("item_id")) throw LLJSONRPCConnection::InvalidParams("prim_id and item_id are required"); @@ -920,17 +815,13 @@ LLSD LLScriptEditorWSServer::handleObjectItemModify(U32 connection_id, const LLS throw LLJSONRPCConnection::InvalidParams( "At least one property (name, description, or permissions) must be specified"); - // ───────────────────────────────────────────────────────────── // Step 2: Validate Published Item (reuse existing helper) - // ───────────────────────────────────────────────────────────── ValidatedItem v = validatePublishedItem(params, PERM_MODIFY); LLUUID prim_id = params["prim_id"].asUUID(); LLUUID item_id = params["item_id"].asUUID(); - // ───────────────────────────────────────────────────────────── // Step 3: Create Modified Item Copy - // ───────────────────────────────────────────────────────────── LLPointer new_item = new LLViewerInventoryItem(static_cast(v.item)); @@ -952,14 +843,10 @@ LLSD LLScriptEditorWSServer::handleObjectItemModify(U32 connection_id, const LLS new_item->setPermissions(perm); } - // ───────────────────────────────────────────────────────────── // Step 4: Send UpdateTaskInventory Message - // ───────────────────────────────────────────────────────────── v.prim->updateInventory(new_item, TASK_INVENTORY_ITEM_KEY, false); - // ───────────────────────────────────────────────────────────── // Step 5: Return Success Response - // ───────────────────────────────────────────────────────────── LLSD response; response["success"] = true; response["prim_id"] = prim_id.asString(); @@ -1205,6 +1092,9 @@ LLSD LLScriptEditorWSServer::handleObjectRequest(U32 connection_id, const LLSD& return response; } +// Helper function to validate that the specified prim and +// item are valid, published, and have the required permissions. +// Throws JSON-RPC exceptions if validation fails. LLScriptEditorWSServer::ValidatedItem LLScriptEditorWSServer::validatePublishedItem( const LLSD& params, U32 permMask) const { @@ -1522,12 +1412,28 @@ LLSD LLScriptEditorWSServer::handleObjectItemDelete(U32 connection_id, const LLS { auto v = validatePublishedItem(params, PERM_MODIFY); - v.prim->removeInventory(v.item->getUUID()); + const LLUUID prim_id = v.prim->getID(); + const LLUUID root_id = v.root->getID(); + const LLUUID item_id = v.item->getUUID(); + + // Optimistic local delete then emit immediate update + // for published clients and request authoritative server refresh. + v.prim->removeInventory(item_id); + onPrimInventoryChanged(root_id, prim_id); + + if (!mPublishedObjectManager.hasInventoryRequestStart(prim_id)) + { + v.prim->dirtyInventory(); + mPublishedObjectManager.setInventoryRequestStart( + prim_id, + LLTimer::getTotalSeconds().value()); + v.prim->requestInventory(); + } LLSD response; response["success"] = true; - response["prim_id"] = params["prim_id"].asUUID(); - response["item_id"] = params["item_id"].asUUID(); + response["prim_id"] = prim_id; + response["item_id"] = item_id; return response; } @@ -1535,11 +1441,15 @@ LLSD LLScriptEditorWSServer::handleObjectUnpublish(U32 connection_id, const LLSD { LLUUID object_id = params["object_id"].asUUID(); if (object_id.isNull()) + { throw LLJSONRPCConnection::InvalidParams("object_id is required"); + } - auto it = mPublishedObjects.find(object_id); - if (it == mPublishedObjects.end()) + if (!mPublishedObjectManager.hasPublished(object_id)) + { throw LLJSONRPCConnection::InvalidParams("Object is not published"); + } + unpublishObject(object_id, "manual"); LLSD response; @@ -1650,25 +1560,24 @@ LLSD LLScriptEditorWSServer::handleObjectItemCreate(const std::string& method, c } } + // Set up event pump to wait for inventory change + LLEventMailDrop result_pump("objectItemCreate." + LLUUID::generateNewID().asString(), true); + // Reject if another item.create is already in flight for this prim; the // map keys by prim, so two concurrent creates would clobber one another. - if (mPendingItemCreates.find(prim_id) != mPendingItemCreates.end()) + if (!mPublishedObjectManager.reservePendingItemCreate(prim_id, result_pump.getName())) { throw LLJSONRPCConnection::InvalidRequest( "An item.create is already in flight for this prim"); } - // Set up event pump to wait for inventory change - LLEventMailDrop result_pump("objectItemCreate." + LLUUID::generateNewID().asString(), true); - mPendingItemCreates[prim_id] = result_pump.getName(); - // RAII: guarantee the pending entry is cleared on every exit path (throw // or normal return), so no exception between here and the erase-on-post // in onPrimInventoryChanged can leave a stale entry behind. Uses a // shared_ptr custom deleter as a lightweight scope guard. std::shared_ptr pending_guard(nullptr, [this, prim_id](void*) { - mPendingItemCreates.erase(prim_id); + mPublishedObjectManager.clearPendingItemCreate(prim_id); }); if (has_cap) @@ -1681,7 +1590,7 @@ LLSD LLScriptEditorWSServer::handleObjectItemCreate(const std::string& method, c } else { - // Fallback: legacy RezScript UDP (scripts only — notecards already rejected above) + // Fallback: legacy RezScript UDP (scripts only -- notecards already rejected above) LLPointer new_item = new LLViewerInventoryItem( LLUUID::null, LLUUID::null, perms, LLUUID::null, @@ -2014,6 +1923,7 @@ void LLScriptEditorWSServer::notifyAll(const std::string& method, const LLSD& pa LLSD(), method, params, LLSD(), LLSD()); std::string payload = boost::json::serialize(LlsdToJson(envelope)); + for (const auto& pair : mActiveConnections) { auto connection = pair.second.lock(); @@ -2052,69 +1962,6 @@ std::string LLScriptEditorWSServer::getPrimName(LLViewerObject* obj) return (node && !node->mName.empty()) ? node->mName : std::string(); } -LLSD LLScriptEditorWSServer::buildPrimInventoryLLSD(LLViewerObject* object) const -{ - LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - LLSD items = LLSD::emptyArray(); - if (!object) return items; - - LLInventoryObject::object_list_t contents; - object->getInventoryContents(contents); - - for (const auto& obj : contents) - { - LLInventoryItem* item = dynamic_cast(obj.get()); - if (!item) continue; - - LLAssetType::EType type = item->getType(); - - // Filter: only scripts and notecards - if (type != LLAssetType::AT_LSL_TEXT && type != LLAssetType::AT_NOTECARD) - { - continue; - } - - LLSD entry; - entry["item_id"] = item->getUUID(); - entry["name"] = item->getName(); - entry["description"] = item->getDescription(); - entry["type"] = (type == LLAssetType::AT_LSL_TEXT) ? "script" : "notecard"; - - if (type == LLAssetType::AT_LSL_TEXT) - { - U8 subtype = item->getInventorySubType(); - entry["subtype"] = static_cast(subtype); // 0=LSL, 1=Luau - - const std::string& runtime = item->getRuntime(); - if (!runtime.empty()) - { - entry["vm"] = runtime; - } - - // Script runtime state from task inventory cap - LLViewerInventoryItem* viewer_item = dynamic_cast(item); - if (viewer_item) - { - entry["running"] = viewer_item->getIsRunning(); - entry["faulted"] = viewer_item->getIsFaulted(); - } - } - - // Permissions - const LLPermissions& perms = item->getPermissions(); - LLSD perm_entry; - perm_entry["owner"] = static_cast(perms.getMaskOwner()); - perm_entry["next_owner"] = static_cast(perms.getMaskNextOwner()); - entry["permissions"] = perm_entry; - - entry["creator_id"] = perms.getCreator(); - - items.append(entry); - } - - return items; -} - bool LLScriptEditorWSServer::publishObject(const LLUUID& object_id) { LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; @@ -2145,28 +1992,18 @@ bool LLScriptEditorWSServer::publishObject(const LLUUID& object_id) // If inventory is already loaded, requestInventory() fires the callback // synchronously via doInventoryCallback(), so all_ready will naturally // become true before this function returns in the common case. - PendingPublish pending; - pending.mObjectID = object_id; - - for (LLViewerObject* prim : prims) - { - pending.mPendingPrims.insert(prim->getID()); - auto listener = std::make_unique( - this, object_id, prim->getID(), prim); - pending.mListeners.push_back(std::move(listener)); - } - - mPendingPublishes[object_id] = std::move(pending); + mPublishedObjectManager.beginPendingPublish(object_id, prims); // Request inventory for each prim. If already loaded, onPrimInventoryReady() // will be called immediately (possibly building and sending the publish // before this loop even finishes). for (LLViewerObject* prim : prims) { - if (mPendingPublishes.find(object_id) == mPendingPublishes.end()) + if (!mPublishedObjectManager.hasPendingPublish(object_id)) { break; // publish completed synchronously during a previous iteration } + mPublishedObjectManager.setInventoryRequestStart(prim->getID(), LLTimer::getTotalSeconds().value()); prim->requestInventory(); } @@ -2175,63 +2012,23 @@ bool LLScriptEditorWSServer::publishObject(const LLUUID& object_id) bool LLScriptEditorWSServer::isObjectPublished(const LLUUID& object_id) const { - return mPublishedObjects.find(object_id) != mPublishedObjects.end(); + return mPublishedObjectManager.hasPublished(object_id); } void LLScriptEditorWSServer::onPrimInventoryReady(const LLUUID& object_id, const LLUUID& prim_id) { LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - auto it = mPendingPublishes.find(object_id); - if (it == mPendingPublishes.end()) return; - - it->second.mPendingPrims.erase(prim_id); - - if (it->second.mPendingPrims.empty()) + if (mPublishedObjectManager.handlePrimInventoryReadyEvent(object_id, prim_id)) { LL_DEBUGS("ScriptEditorWS") << "All prim inventories ready for object " << object_id << LL_ENDL; buildAndSendPublish(object_id); } } -LLSD LLScriptEditorWSServer::buildPublishedObjectLLSD(LLViewerObject* root) const -{ - LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - LLSD pub; - pub["object_id"] = root->getID(); - pub["object_name"] = getPrimName(root); - pub["object_description"] = nv_string(root, "Desc"); - pub["owner_id"] = root->mOwnerID; - if (root->getRegion()) - { - pub["region"] = root->getRegion()->getName(); - } - pub["inventory"] = buildPrimInventoryLLSD(root); - - LLSD linked_objects = LLSD::emptyArray(); - S32 link_number = 2; - for (LLViewerObject* child : root->getChildren()) - { - LLSD link; - link["link_id"] = child->getID(); - link["link_number"] = link_number++; - link["link_name"] = getPrimName(child); - link["link_description"] = nv_string(child, "Desc"); - link["inventory"] = buildPrimInventoryLLSD(child); - linked_objects.append(link); - } - if (linked_objects.size() > 0) - { - pub["linked_objects"] = linked_objects; - } - - return pub; -} - void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& object_id) { LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - auto pending_it = mPendingPublishes.find(object_id); - if (pending_it == mPendingPublishes.end()) + if (!mPublishedObjectManager.hasPendingPublish(object_id)) { LL_WARNS("ScriptEditorWS") << "buildAndSendPublish: no pending publish for " << object_id << LL_ENDL; return; @@ -2241,14 +2038,14 @@ void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& object_id) if (!root) { LL_WARNS("ScriptEditorWS") << "buildAndSendPublish: root object gone: " << object_id << LL_ENDL; - mPendingPublishes.erase(pending_it); + mPublishedObjectManager.cancelPendingPublish(object_id); return; } - LLSD pub = buildPublishedObjectLLSD(root); + LLSD pub = mPublishedObjectManager.buildPublishedObjectLLSD(root); // Store in the published registry - PublishedObjectInfo info; + LLPublishedObjectMgr::PublishedObjectInfo info; info.mObjectID = root->getID(); info.mOwnerID = root->mOwnerID; info.mObjectName = pub["object_name"].asString(); @@ -2262,7 +2059,7 @@ void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& object_id) std::vector prims = collect_linkset(root); for (LLViewerObject* prim : prims) { - PublishedPrimInfo prim_info; + LLPublishedObjectMgr::PublishedPrimInfo prim_info; prim_info.mPrimID = prim->getID(); prim_info.mPrimName = getPrimName(prim); // Use helper with selection fallback prim_info.mLinkNumber = link_num++; @@ -2270,9 +2067,7 @@ void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& object_id) info.mPrims.push_back(prim_info); } - mPublishedObjects[object_id] = std::move(info); - mPublishedObjects[object_id].mListeners = std::move(pending_it->second.mListeners); - mPendingPublishes.erase(pending_it); + LLPublishedObjectMgr::PublishedObjectInfo& published_info = mPublishedObjectManager.finalizePendingPublish(object_id, std::move(info)); // Send notification LLSD message; @@ -2287,32 +2082,21 @@ void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& object_id) void LLScriptEditorWSServer::onLinksetChildAdded(const LLUUID& root_id, LLViewerObject* child) { LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - auto obj_it = mPublishedObjects.find(root_id); - if (obj_it == mPublishedObjects.end()) return; - - const LLUUID child_id = child->getID(); - PublishedObjectInfo& info = obj_it->second; - - // Add a placeholder slot so the flush can enumerate the full linkset - // even before inventory arrives. flushLinksetUpdate renumbers from the - // live mPrims list, so the tentative link_number here is just informational. - PublishedPrimInfo prim_info; - prim_info.mPrimID = child_id; - prim_info.mPrimName = getPrimName(child); - prim_info.mLinkNumber = static_cast(info.mPrims.size()) + 1; - prim_info.mInventorySerial = -1; // sentinel: not yet loaded - info.mPrims.push_back(prim_info); - - // Register a listener so we are notified when the child's inventory arrives. - // The listener constructor calls registerVOInventoryListener internally. - auto listener = std::make_unique(this, root_id, child_id, child); - info.mListeners.push_back(std::move(listener)); - - // Request inventory (async; fires onPrimInventoryChanged when ready) - child->requestInventory(); + if (!child) + { + return; + } - // Mark as pending — the flush waits until this is cleared - mNewChildPrims[root_id].insert(child_id); + if (!mPublishedObjectManager.reconcileLinksetChildAdded( + root_id, + child, + LLTimer::getTotalSeconds().value())) + { + return; + } + + // Request inventory (async; fires onPrimInventoryChanged when ready). + child->requestInventory(); // Start safety-timeout timer (no-op if one is already pending for this root) scheduleLinksetFlush(root_id, LINKSET_ADD_FLUSH_DELAY); @@ -2321,115 +2105,52 @@ void LLScriptEditorWSServer::onLinksetChildAdded(const LLUUID& root_id, LLViewer void LLScriptEditorWSServer::onLinksetChildRemoved(const LLUUID& root_id, const LLUUID& child_id) { LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - auto obj_it = mPublishedObjects.find(root_id); - if (obj_it == mPublishedObjects.end()) return; - - PublishedObjectInfo& info = obj_it->second; - - // Remove prim slot - info.mPrims.erase( - std::remove_if(info.mPrims.begin(), info.mPrims.end(), - [&](const PublishedPrimInfo& p) { return p.mPrimID == child_id; }), - info.mPrims.end()); - - // Destroy the prim's inventory listener - info.mListeners.erase( - std::remove_if(info.mListeners.begin(), info.mListeners.end(), - [&](const std::unique_ptr& l) - { return l->getPrimID() == child_id; }), - info.mListeners.end()); - - // Remove from pending-inventory set (child may have been added then removed - // before its inventory ever arrived) - auto nc_it = mNewChildPrims.find(root_id); - if (nc_it != mNewChildPrims.end()) + if (!mPublishedObjectManager.reconcileLinksetChildRemoved(root_id, child_id)) { - nc_it->second.erase(child_id); - if (nc_it->second.empty()) - mNewChildPrims.erase(nc_it); - } - - // Re-number remaining children (root stays 1, children get 2..N in order) - S32 link_num = 2; - for (auto& p : info.mPrims) - { - if (p.mPrimID != root_id) - p.mLinkNumber = link_num++; + return; } - // Schedule coalesced flush — multiple simultaneous removes share one timer + // Schedule coalesced flush - multiple simultaneous removes share one timer scheduleLinksetFlush(root_id, LINKSET_REMOVE_FLUSH_DELAY); } void LLScriptEditorWSServer::scheduleLinksetFlush(const LLUUID& root_id, F32 delay) { // No-op if a timer is already pending for this root_id - auto it = mLinksetFlushTimers.find(root_id); - if (it != mLinksetFlushTimers.end() && !it->second.expired()) + if (mPublishedObjectManager.hasActiveLinksetFlushTimer(root_id)) + { return; + } wptr_t weak = std::static_pointer_cast(shared_from_this()); LLEventTimer* t = LLEventTimer::run_after(delay, [weak, root_id]() { if (auto self = weak.lock()) { - self->mLinksetFlushTimers.erase(root_id); - self->mNewChildPrims.erase(root_id); // clear any remaining pending children (timeout path) + self->mPublishedObjectManager.clearLinksetFlushTimer(root_id); + self->mPublishedObjectManager.clearPendingNewChildren(root_id); // clear any remaining pending children (timeout path) self->flushLinksetUpdate(root_id); } }); - mLinksetFlushTimers[root_id] = t->getWeak(); + mPublishedObjectManager.setLinksetFlushTimer(root_id, t->getWeak()); } void LLScriptEditorWSServer::cancelLinksetFlushTimer(const LLUUID& root_id) { - auto it = mLinksetFlushTimers.find(root_id); - if (it == mLinksetFlushTimers.end()) - return; - if (auto locked = it->second.lock()) - { - // LLEventTimer contract (see lleventtimer.h): the shared_ptr held by - // LLInstanceTracker uses a no-op deleter, so this raw delete is safe - // and is the documented way to cancel a pending timer. - delete locked.get(); - } - mLinksetFlushTimers.erase(it); + mPublishedObjectManager.cancelLinksetFlushTimer(root_id); } void LLScriptEditorWSServer::flushLinksetUpdate(const LLUUID& root_id) { LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - auto obj_it = mPublishedObjects.find(root_id); - if (obj_it == mPublishedObjects.end()) return; - - const PublishedObjectInfo& info = obj_it->second; - - // Build full linked_objects replacement (children only, in link_number order) - LLSD linked_objects(LLSD::TypeArray); - for (const PublishedPrimInfo& prim_info : info.mPrims) + LLSD update; + if (!mPublishedObjectManager.buildLinksetUpdateLLSD(root_id, update)) { - if (prim_info.mPrimID == root_id) continue; // root is not in linked_objects - - LLSD entry; - entry["link_id"] = prim_info.mPrimID; - entry["link_number"] = prim_info.mLinkNumber; - - LLViewerObject* prim = gObjectList.findObject(prim_info.mPrimID); - // Always read the name fresh from the live object so newly-linked prims - // whose NV pair was not yet available at addChild time still get a name. - std::string link_name = prim ? getPrimName(prim) : std::string(); - if (link_name.empty()) link_name = prim_info.mPrimName; // fallback to stored name - entry["link_name"] = link_name; - entry["inventory"] = prim ? buildPrimInventoryLLSD(prim) : LLSD(LLSD::TypeArray); - - linked_objects.append(entry); + return; } - - LLSD update; - update["object_id"] = root_id; - update["linked_objects"] = linked_objects; notifyAll("object.update", update); + const LLSD linked_objects = update["linked_objects"]; LL_INFOS("ScriptEditorWS") << "Linkset update for " << root_id << ": " << linked_objects.size() << " child(ren)" << LL_ENDL; } @@ -2437,158 +2158,92 @@ void LLScriptEditorWSServer::flushLinksetUpdate(const LLUUID& root_id) void LLScriptEditorWSServer::onPrimInventoryChanged(const LLUUID& object_id, const LLUUID& prim_id) { LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - auto pub_it = mPublishedObjects.find(object_id); - if (pub_it == mPublishedObjects.end()) + if (!mPublishedObjectManager.hasPublished(object_id)) + { return; + } LLViewerObject* prim = gObjectList.findObject(prim_id); if (!prim) - return; - - // ── New-child path ──────────────────────────────────────────────────── - // When a child was linked in via onLinksetChildAdded it is placed in - // mNewChildPrims until its first inventory response arrives here. - auto nc_root_it = mNewChildPrims.find(object_id); - if (nc_root_it != mNewChildPrims.end() && nc_root_it->second.count(prim_id)) { - // Update the placeholder with the real prim name now that we have data - for (auto& p : pub_it->second.mPrims) - { - if (p.mPrimID == prim_id) - { - p.mPrimName = getPrimName(prim); - p.mInventorySerial = 0; // mark as loaded - break; - } - } - - nc_root_it->second.erase(prim_id); - - if (nc_root_it->second.empty()) - { - // All new children have inventory — cancel timeout, flush now - mNewChildPrims.erase(nc_root_it); - cancelLinksetFlushTimer(object_id); - flushLinksetUpdate(object_id); - } - // else: still waiting for other new children return; } - // ── Normal inventory-change path ────────────────────────────────────── - LLSD update; - update["object_id"] = object_id; + auto inv_result = mPublishedObjectManager.handlePrimInventoryChangedEvent( + object_id, prim_id, prim, LLTimer::getTotalSeconds().value()); - LLSD inv = buildPrimInventoryLLSD(prim); - if (prim_id == object_id) + if (inv_result.mTimingConsumed) { - // Root prim — use top-level inventory field (full replacement) - update["inventory"] = inv; + LL_DEBUGS("ScriptEditorWS") << "[Phase0] inventory refresh object_id=" << object_id + << " prim_id=" << prim_id + << " elapsed_sec=" << inv_result.mTimingElapsedSec << LL_ENDL; } - else + + if (inv_result.mKind == LLPublishedObjectMgr::InventoryChangeKind::CHILD_READY_WAIT) { - // Child prim — wrap in changes.linked_objects.modified so the extension - // routes the update to the correct linked prim directory - LLSD modified_entry; - modified_entry["link_id"] = prim_id; - modified_entry["inventory"] = inv; - LLSD modified_arr = LLSD::emptyArray(); - modified_arr.append(modified_entry); - update["changes"]["linked_objects"]["modified"] = modified_arr; + return; } - - notifyAll("object.update", update); - - // Signal any pending item.create coroutine waiting on this prim - auto create_it = mPendingItemCreates.find(prim_id); - if (create_it != mPendingItemCreates.end()) + if (inv_result.mKind == LLPublishedObjectMgr::InventoryChangeKind::CHILD_READY_FLUSH_NOW) { - LLEventPumps::instance().post(create_it->second, LLSD().with("prim_id", prim_id)); - mPendingItemCreates.erase(create_it); + cancelLinksetFlushTimer(object_id); + flushLinksetUpdate(object_id); + return; } + if (inv_result.mKind == LLPublishedObjectMgr::InventoryChangeKind::ROOT_INVENTORY_UPDATE || + inv_result.mKind == LLPublishedObjectMgr::InventoryChangeKind::CHILD_INVENTORY_UPDATE) + { + notifyAll("object.update", inv_result.mUpdate); + if (inv_result.mHasPendingItemCreate) + { + LLEventPumps::instance().post( + inv_result.mPendingItemCreatePump, + LLSD().with("prim_id", prim_id)); + } - LL_DEBUGS("ScriptEditorWS") << "Sent object.update for prim " << prim_id - << " in object " << object_id << LL_ENDL; + LL_DEBUGS("ScriptEditorWS") << "Sent object.update for prim " << prim_id + << " in object " << object_id << LL_ENDL; + } } void LLScriptEditorWSServer::onObjectPropertyChanged( - const LLUUID& prim_id, const std::string& name, const std::string& desc) + const LLUUID& prim_id, const std::string& name, const std::string& desc, S16 inventory_serial) { LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; LLViewerObject* prim = gObjectList.findObject(prim_id); - if (!prim) return; - LLUUID root_id = prim->getRootEdit()->getID(); - - auto pub_it = mPublishedObjects.find(root_id); - if (pub_it == mPublishedObjects.end()) return; - - LLSD update; - update["object_id"] = root_id; - - if (prim_id == root_id) + if (!prim) { - bool name_changed = (pub_it->second.mObjectName != name); - bool desc_changed = (pub_it->second.mObjectDescription != desc); - if (!name_changed && !desc_changed) return; - - if (name_changed) { pub_it->second.mObjectName = name; update["object_name"] = name; } - if (desc_changed) { pub_it->second.mObjectDescription = desc; update["object_description"] = desc; } + return; } - else - { - auto prim_it = std::find_if(pub_it->second.mPrims.begin(), pub_it->second.mPrims.end(), - [&](const PublishedPrimInfo& p) { return p.mPrimID == prim_id; }); - if (prim_it == pub_it->second.mPrims.end()) return; - if (prim_it->mPrimName == name) return; - prim_it->mPrimName = name; - LLSD modified_entry; - modified_entry["link_id"] = prim_id; - modified_entry["link_name"] = name; - LLSD modified_arr = LLSD::emptyArray(); - modified_arr.append(modified_entry); - update["changes"]["linked_objects"]["modified"] = modified_arr; - } + LLUUID root_id = prim->getRootEdit()->getID(); - notifyAll("object.update", update); -} + bool should_refresh_inventory = mPublishedObjectManager.markPrimInventorySerialAndDetectChange( + root_id, + prim_id, + inventory_serial); -void LLScriptEditorWSServer::cleanupPrimListeners(const LLUUID& object_id) -{ - // Clear any pending publish listeners - auto pending_it = mPendingPublishes.find(object_id); - if (pending_it != mPendingPublishes.end()) + LLSD update; + if (mPublishedObjectManager.applyPropertyChange(root_id, prim_id, name, desc, update)) { - pending_it->second.mListeners.clear(); // unique_ptrs call removeVOInventoryListener() - mPendingPublishes.erase(pending_it); + notifyAll("object.update", update); } - // Clear published object listeners (Phase 4) - auto pub_it = mPublishedObjects.find(object_id); - if (pub_it != mPublishedObjects.end()) + if (should_refresh_inventory && !mPublishedObjectManager.hasInventoryRequestStart(prim_id)) { - pub_it->second.mListeners.clear(); + prim->dirtyInventory(); + mPublishedObjectManager.setInventoryRequestStart(prim_id, LLTimer::getTotalSeconds().value()); + prim->requestInventory(); } } void LLScriptEditorWSServer::unpublishObject(const LLUUID& object_id, const std::string& reason) { LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - auto it = mPublishedObjects.find(object_id); - if (it == mPublishedObjects.end()) + if (!mPublishedObjectManager.cleanupObjectStateForUnpublish(object_id)) { - // May still have a pending publish in progress -- cancel it - cleanupPrimListeners(object_id); return; } - cleanupPrimListeners(object_id); - mPublishedObjects.erase(it); - - // Cancel any pending linkset flush so it cannot fire after removal - cancelLinksetFlushTimer(object_id); - mNewChildPrims.erase(object_id); - LLSD message; message["object_id"] = object_id; if (!reason.empty()) @@ -2780,3 +2435,4 @@ std::string LLScriptEditorWSConnection::generateChallenge() return mChallengeFile; } + diff --git a/indra/newview/llscripteditorws.h b/indra/newview/llscripteditorws.h index 69c7e1a8e1..475a8049ee 100644 --- a/indra/newview/llscripteditorws.h +++ b/indra/newview/llscripteditorws.h @@ -27,6 +27,7 @@ #pragma once #include "lljsonrpcws.h" +#include "llpublishedobjectmgr.h" #include "llsd.h" #include "lluuid.h" #include "llhandle.h" @@ -45,7 +46,6 @@ class LLScriptEdContainer; class LLScriptEditorWSServer; class LLChat; class LLPanel; -class LLPublishedPrimListener; class LLViewerObject; class LLInventoryItem; @@ -207,7 +207,7 @@ public: bool isObjectPublished(const LLUUID& object_id) const; void onPrimInventoryReady(const LLUUID& object_id, const LLUUID& prim_id); void onPrimInventoryChanged(const LLUUID& object_id, const LLUUID& prim_id); - void onObjectPropertyChanged(const LLUUID& prim_id, const std::string& name, const std::string& desc); + void onObjectPropertyChanged(const LLUUID& prim_id, const std::string& name, const std::string& desc, S16 inventory_serial = -1); void onLinksetChildAdded(const LLUUID& root_id, LLViewerObject* child); void onLinksetChildRemoved(const LLUUID& root_id, const LLUUID& child_id); @@ -242,7 +242,6 @@ protected: LLSD handleObjectScriptReset(U32 connection_id, const LLSD& params); LLSD handleObjectModify(U32 connection_id, const LLSD& params); LLSD handleObjectItemModify(U32 connection_id, const LLSD& params); - LLSD buildPublishedObjectLLSD(LLViewerObject* root) const; struct ValidatedItem { @@ -255,10 +254,8 @@ protected: // --- Object Content Publishing (helpers) --- static std::string getPrimName(LLViewerObject* obj); - LLSD buildPrimInventoryLLSD(LLViewerObject* object) const; void notifyConnection(U32 connection_id, const std::string& method, const LLSD& params) const; void notifyAll(const std::string& method, const LLSD& params) const; - void cleanupPrimListeners(const LLUUID& object_id); void buildAndSendPublish(const LLUUID& object_id); void scheduleLinksetFlush(const LLUUID& root_id, F32 delay); void cancelLinksetFlushTimer(const LLUUID& root_id); @@ -303,32 +300,6 @@ private: }; using subscriptions_t = std::unordered_map; - struct PublishedPrimInfo - { - LLUUID mPrimID; - std::string mPrimName; - S32 mLinkNumber; // 1=root, >=2=child - S16 mInventorySerial; // last-seen serial for change detection (Phase 4) - }; - - struct PublishedObjectInfo - { - LLUUID mObjectID; // root prim UUID - LLUUID mOwnerID; - std::string mObjectName; - std::string mObjectDescription; - std::string mRegionName; - std::vector mPrims; // root + all children - std::vector> mListeners; - }; - - struct PendingPublish - { - LLUUID mObjectID; - std::set mPendingPrims; // prims whose inventory we're still waiting for - std::vector> mListeners; // listeners for pending prims - }; - SubscriptionError updateScriptSubscription(const std::string &script_id, U32 connection_id); void unsubscribeConnection(U32 connection_id); @@ -340,11 +311,7 @@ private: std::unordered_map mConnectionSubscriptionCounts; std::map mActiveConnections; - std::map mPublishedObjects; // keyed by root object_id - std::map mPendingPublishes; // keyed by root object_id - std::map mPendingItemCreates; // prim_id -> pump name awaiting inventory update - std::map> mNewChildPrims; // root_id → children awaiting first inventory response - std::map> mLinksetFlushTimers; // root_id → pending coalesce timer + mutable LLPublishedObjectMgr mPublishedObjectManager; boost::signals2::connection mLanguageChangeSignal; LLUUID mLastSyntaxId; diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp index d13b7296f4..cf64cec9b2 100644 --- a/indra/newview/llselectmgr.cpp +++ b/indra/newview/llselectmgr.cpp @@ -6112,7 +6112,7 @@ void LLSelectMgr::processObjectProperties(LLMessageSystem* msg, void** user_data if (auto ws_server = LLScriptEditorWSServer::getServer()) { - ws_server->onObjectPropertyChanged(id, name, desc); + ws_server->onObjectPropertyChanged(id, name, desc, inv_serial); } } } -- cgit v1.3 From 309fc5b4c21151e3664f5bdab99f57bae24266a0 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Thu, 6 Aug 2026 14:25:45 -0700 Subject: JSON-RPC command.execute instruction for arbitrary 1-off commands and extensions. --- doc/external-editor-json-rpc.md | 140 +++++++++++++++++++++++++++++++++++++ indra/newview/llscripteditorws.cpp | 128 +++++++++++++++++++++++++++++++++ indra/newview/llscripteditorws.h | 24 +++++++ 3 files changed, 292 insertions(+) diff --git a/doc/external-editor-json-rpc.md b/doc/external-editor-json-rpc.md index a3404ce1eb..6c041cf70b 100644 --- a/doc/external-editor-json-rpc.md +++ b/doc/external-editor-json-rpc.md @@ -46,6 +46,9 @@ This document describes all the message interfaces defined for WebSocket communi - [ObjectRequest](#objectrequest) - [ObjectModify](#objectmodify) - [ObjectItemModify](#objectitemmodify) +- [Command Interfaces](#command-interfaces) + - [CommandExecute](#commandexecute) + - [CommandList](#commandlist) ## Usage Flow @@ -196,6 +199,10 @@ WebSocket connects -> session.handshake -> session.ok | `object.modify` (response) | Viewer -> Extension | Response | `ObjectModifyResponse` | | `object.item.modify` | Extension -> Viewer | Call | `ObjectItemModifyParams` | | `object.item.modify` (response) | Viewer -> Extension | Response | `ObjectItemModifyResponse` | +| `command.execute` | Bidirectional | Call | `CommandExecuteParams` | +| `command.execute` (response) | Bidirectional | Response | `CommandExecuteResponse` | +| `command.list` | Bidirectional | Call | _(no params)_ | +| `command.list` (response) | Bidirectional | Response | `CommandListResponse` | ## Session Management Interfaces @@ -235,6 +242,7 @@ interface SessionHandshake { - `live_sync`: Viewer supports live script synchronisation with the external editor - `compilation`: Viewer will forward compilation results via `script.compiled` - `syntax_cache`: Viewer supports `language.syntax.cache` and `language.syntax.get` for retrieving syntax definition files + - `commands`: Both sides support `command.execute` and `command.list` ### SessionHandshakeResponse @@ -1218,3 +1226,135 @@ interface ObjectItemModifyResponse { - An `object.update` notification will fire after successful modification. - Owner permissions cannot be modified directly — only `next_owner` can be changed. - If the item is renamed, the virtual filesystem path will change and the extension must handle the rename appropriately. + +--- + +## Command Interfaces + +These interfaces provide a general-purpose, bidirectional command channel. Either side may invoke a named command on the other side and receive a structured result. The feature is optional and must be negotiated via the `commands` flag in the session handshake. + +### CommandExecute + +**JSON-RPC Method:** `command.execute` (call, bidirectional) + +Invokes a named command on the receiving side. Commands are identified by a namespaced string and carry an optional freeform parameter map. + +```typescript +interface CommandExecuteParams { + command: string; // namespaced command id, e.g. "viewer.teleport" + params?: Record; // command-specific arguments +} + +interface CommandExecuteResponse { + success: boolean; + result?: unknown; // optional command-specific return value + error_code?: number; + message?: string; +} +``` + +**Fields:** + +- `command`: Namespaced command identifier. The prefix before the first `.` identifies the side that owns and executes the command: + - `viewer.*` - commands executed by the viewer (e.g. `viewer.teleport`, `viewer.script.recompile_all`) + - `editor.*` - commands executed by the extension (e.g. `editor.open_file`, `editor.show_message`) +- `params` (optional): Command-specific argument map. Structure varies by command. +- `success`: Whether the command was found and executed without error. +- `result` (optional): Command-specific return value. Only present when `success` is `true` and the command produces output. +- `error_code` (optional): Numeric failure code. Only present when `success` is `false`: + - `1` - Unknown command + - `2` - Invalid or missing parameters + - `3` - Not permitted + - `4` - Execution error +- `message` (optional): Human-readable error description. Only present when `success` is `false`. + +**Capability gate:** A side MUST NOT send `command.execute` unless the peer advertised `commands: true` in the handshake. A receiver that receives the call without having negotiated the feature MUST respond with `success: false, error_code: 1`. + +**Example - extension asks viewer to teleport:** + +```json +{ + "jsonrpc": "2.0", + "method": "command.execute", + "id": 7, + "params": { + "command": "viewer.teleport", + "params": { "region": "Aditi", "position": [128, 128, 25] } + } +} +``` + +```json +{ + "jsonrpc": "2.0", + "id": 7, + "result": { "success": true } +} +``` + +**Example - viewer asks extension to show a message:** + +```json +{ + "jsonrpc": "2.0", + "method": "command.execute", + "id": 8, + "params": { + "command": "editor.show_message", + "params": { "message": "Script reset complete", "level": "info" } + } +} +``` + +--- + +### CommandList + +**JSON-RPC Method:** `command.list` (call, bidirectional) + +Requests the list of commands the receiving side supports. Intended for tooling and autocomplete; implementations may omit this method and return an error response if discovery is not needed. + +This method takes no parameters. + +**Response:** + +```typescript +interface CommandListResponse { + commands: CommandInfo[]; +} + +interface CommandInfo { + command: string; + description?: string; + params?: Record; +} + +interface CommandParamInfo { + type: "string" | "number" | "boolean" | "object" | "array"; + required?: boolean; + description?: string; +} +``` + +**Response Fields:** + +- `commands`: Array of commands the responder supports. Each entry describes one command. + - `command`: The namespaced command identifier. + - `description` (optional): Human-readable description of what the command does. + - `params` (optional): Map of parameter names to their type descriptors. + +**Known viewer commands:** + +| Command | Required params | Description | +|---------|----------------|-------------| +| `viewer.teleport` | `region: string` | Teleport agent to a region. Optional `position: [x, y, z]`. | +| `viewer.script.recompile_all` | `object_id: string` | Recompile all scripts in an object. | +| `viewer.script.reset_all` | `object_id: string` | Reset all scripts in an object. | +| `viewer.camera.focus` | `object_id: string` | Move camera focus to an in-world object. | + +**Known extension commands:** + +| Command | Required params | Description | +|---------|----------------|-------------| +| `editor.open_file` | `path: string` | Open a file in the editor. Optional `line: number`. | +| `editor.show_message` | `message: string` | Show a notification. Optional `level: "info" | "warn" | "error"`. | diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp index 8f45d14c36..3fbcd5c384 100644 --- a/indra/newview/llscripteditorws.cpp +++ b/indra/newview/llscripteditorws.cpp @@ -31,6 +31,7 @@ #include "llscripteditorws.h" #include "llagent.h" +#include "llagentcamera.h" #include "llappviewer.h" #include "llchat.h" #include "lldate.h" @@ -61,6 +62,7 @@ #include "llviewerobject.h" #include "llviewerobjectlist.h" #include "llviewerregion.h" +#include "llviewermenu.h" #include "llviewertexteditor.h" #include "llvoinventorylistener.h" #include "roles_constants.h" @@ -166,6 +168,46 @@ LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port { LL_INFOS("ScriptEditorWS") << "Created JSON-RPC script editor server: " << name << " on port " << port << LL_ENDL; + + registerCommand({ "viewer.teleport", "Teleport agent to an in-world object" }, + [](U32, const LLSD& p) -> LLSD + { + LLUUID object_id = p["object_id"].asUUID(); + if (object_id.isNull()) + throw LLJSONRPCConnection::InvalidParams("object_id is required"); + + LLViewerObject* object = gObjectList.findObject(object_id); + if (!object) + throw LLJSONRPCConnection::InvalidParams("object_id not found"); + + LLVector3d global_pos = object->getPositionGlobal(); + gAgent.teleportViaLocation(global_pos); + + LLSD response; + response["success"] = true; + return response; + }); + + registerCommand({ "viewer.camera.focus", "Zoom camera to an in-world object (same behavior as context menu Zoom In)" }, + [](U32, const LLSD& p) -> LLSD + { + LLUUID object_id = p["object_id"].asUUID(); + if (object_id.isNull()) + throw LLJSONRPCConnection::InvalidParams("object_id is required"); + + if (!handle_zoom_to_object(object_id)) + { + LLSD response; + response["success"] = false; + response["error_code"] = WSCommandError::ExecutionError; + response["message"] = "Object not found or not reachable"; + return response; + } + + LLSD response; + response["success"] = true; + return response; + }); } LLScriptEditorWSServer::ptr_t LLScriptEditorWSServer::getServer() @@ -623,6 +665,18 @@ void LLScriptEditorWSServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t c { return s.handleObjectItemModify(connection_id, params); })); + + script_connection->registerAsyncMethod("command.execute", + bindHandler([connection_id](LLScriptEditorWSServer& s, auto&, auto&, const LLSD& params) + { + return s.handleCommandExecute(connection_id, params); + })); + + script_connection->registerMethod("command.list", + bindHandler([](LLScriptEditorWSServer& s, auto&, auto&, auto&) + { + return s.handleCommandList(); + })); } } @@ -854,6 +908,79 @@ LLSD LLScriptEditorWSServer::handleObjectItemModify(U32 connection_id, const LLS return response; } +void LLScriptEditorWSServer::registerCommand(const WSCommandInfo& info, WSCommandHandler handler) +{ + mCommandRegistry.emplace(info.command, std::make_pair(info, std::move(handler))); +} + +bool LLScriptEditorWSConnection::hasFeature(const std::string& feature) const +{ + return mFeatures.count(feature) > 0; +} + +LLSD LLScriptEditorWSServer::handleCommandExecute(U32 connection_id, const LLSD& params) +{ + const std::string command = params["command"].asString(); + if (command.empty()) + { + throw LLJSONRPCConnection::InvalidParams("command is required"); + } + + auto it = mCommandRegistry.find(command); + if (it == mCommandRegistry.end()) + { + LLSD response; + response["success"] = false; + response["error_code"] = WSCommandError::UnknownCommand; + response["message"] = "Unknown command: " + command; + return response; + } + + return it->second.second(connection_id, params["params"]); +} + +LLSD LLScriptEditorWSServer::handleCommandList() +{ + LLSD commands(LLSD::emptyArray()); + for (const auto& [name, entry] : mCommandRegistry) + { + LLSD info; + info["command"] = entry.first.command; + info["description"] = entry.first.description; + commands.append(info); + } + LLSD response; + response["commands"] = commands; + return response; +} + +void LLScriptEditorWSServer::sendCommandExecute( + U32 connection_id, const std::string& command, const LLSD& params) +{ + auto it = mActiveConnections.find(connection_id); + if (it == mActiveConnections.end()) + { + return; + } + + auto connection = it->second.lock(); + if (!connection || !connection->hasFeature("commands")) + { + return; + } + + LLSD call_params; + call_params["command"] = command; + call_params["params"] = params; + + connection->call("command.execute", call_params, + [command](const LLSD& result, const LLSD& error) + { + LL_WARNS_IF(!error.isUndefined() || !result["success"].asBoolean(), "WSCommand") + << "command.execute failed for " << command << LL_ENDL; + }); +} + void LLScriptEditorWSServer::broadcastLanguageChange() { LLUUID syntax_id = LLSyntaxDefCache::instance().getSyntaxID(); @@ -2300,6 +2427,7 @@ void LLScriptEditorWSConnection::onOpen() features["live_sync"] = true; features["compilation"] = true; features["syntax_cache"] = true; + features["commands"] = true; handshake["features"] = features; wptr_t that = weak_from_this(); diff --git a/indra/newview/llscripteditorws.h b/indra/newview/llscripteditorws.h index 475a8049ee..63b60335ff 100644 --- a/indra/newview/llscripteditorws.h +++ b/indra/newview/llscripteditorws.h @@ -39,6 +39,7 @@ #include #include #include +#include // Forward declarations class LLLiveLSLEditor; @@ -87,6 +88,7 @@ public: void onClose() override; void sendDisconnect(DisconnectReason reason = DisconnectReason::NORMAL, const std::string& message = "Goodbye"); + bool hasFeature(const std::string& feature) const; private: using string_set_t = std::set; @@ -205,6 +207,8 @@ public: bool publishObject(const LLUUID& object_id); void unpublishObject(const LLUUID& object_id, const std::string& reason = ""); bool isObjectPublished(const LLUUID& object_id) const; + + // *TODO*: These should be moved to LLPublishedObjectMgr at some point. void onPrimInventoryReady(const LLUUID& object_id, const LLUUID& prim_id); void onPrimInventoryChanged(const LLUUID& object_id, const LLUUID& prim_id); void onObjectPropertyChanged(const LLUUID& prim_id, const std::string& name, const std::string& desc, S16 inventory_serial = -1); @@ -242,6 +246,9 @@ protected: LLSD handleObjectScriptReset(U32 connection_id, const LLSD& params); LLSD handleObjectModify(U32 connection_id, const LLSD& params); LLSD handleObjectItemModify(U32 connection_id, const LLSD& params); + LLSD handleCommandExecute(U32 connection_id, const LLSD& params); + LLSD handleCommandList(); + void sendCommandExecute(U32 connection_id, const std::string& command, const LLSD& params); struct ValidatedItem { @@ -313,6 +320,23 @@ private: mutable LLPublishedObjectMgr mPublishedObjectManager; + struct WSCommandInfo + { + std::string command; + std::string description; + }; + enum WSCommandError + { + UnknownCommand = 1, + InvalidParams = 2, + NotPermitted = 3, + ExecutionError = 4, + }; + using WSCommandHandler = std::function; + std::unordered_map> mCommandRegistry; + + void registerCommand(const WSCommandInfo& info, WSCommandHandler handler); + boost::signals2::connection mLanguageChangeSignal; LLUUID mLastSyntaxId; -- cgit v1.3 From 5e2d671895b292726f923db5f8e7375097b6e159 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Mon, 10 Aug 2026 15:45:03 -0700 Subject: Refresh the object's data when it is published. (link children seem to be an issue still) --- indra/newview/llpublishedobjectmgr.cpp | 107 ++++++++++++++++++++++++++++++--- indra/newview/llpublishedobjectmgr.h | 11 ++++ indra/newview/llscripteditorws.cpp | 49 ++++++++++++++- 3 files changed, 159 insertions(+), 8 deletions(-) diff --git a/indra/newview/llpublishedobjectmgr.cpp b/indra/newview/llpublishedobjectmgr.cpp index 2c2eff58a5..6f78ba82d9 100644 --- a/indra/newview/llpublishedobjectmgr.cpp +++ b/indra/newview/llpublishedobjectmgr.cpp @@ -72,7 +72,13 @@ namespace } LLSelectNode* node = LLSelectMgr::instance().getSelection()->findNode(obj); - return (node && !node->mName.empty()) ? node->mName : std::string(); + if (node && !node->mName.empty()) + { + return node->mName; + } + + // Never emit an empty prim/object name to downstream tooling. + return obj->getID().asString(); } } @@ -164,6 +170,37 @@ bool LLPublishedObjectMgr::markPendingPublishPrimReady(const LLUUID& object_id, return it->second.mPendingPrims.empty(); } +void LLPublishedObjectMgr::recordPendingPropertyChange( + const LLUUID& root_id, + const LLUUID& prim_id, + const std::string& name, + const std::string& desc) +{ + auto it = mPendingPublishes.find(root_id); + if (it == mPendingPublishes.end()) + { + return; + } + + PendingPublish& pending = it->second; + if (prim_id == root_id) + { + pending.mHasRootProperties = true; + pending.mObjectDescription = desc; + if (!name.empty()) + { + pending.mObjectName = name; + } + return; + } + + if (!name.empty()) + { + pending.mPrimNames[prim_id] = name; + } + pending.mPrimDescriptions[prim_id] = desc; +} + std::vector> LLPublishedObjectMgr::takePendingPublishListeners(const LLUUID& object_id) { auto it = mPendingPublishes.find(object_id); @@ -387,6 +424,7 @@ LLSD LLPublishedObjectMgr::buildObjectListLLSD() const link["link_id"] = prim_info.mPrimID; link["link_number"] = prim_info.mLinkNumber; link["link_name"] = prim_info.mPrimName; + link["link_description"] = prim_info.mPrimDescription; link["inventory"] = buildPrimInventoryLLSD(child); linked_objects.append(link); } @@ -428,7 +466,13 @@ bool LLPublishedObjectMgr::buildLinksetUpdateLLSD( { link_name = prim_info.mPrimName; } + std::string link_desc = prim ? nv_string(prim, "Desc") : std::string(); + if (link_desc.empty()) + { + link_desc = prim_info.mPrimDescription; + } entry["link_name"] = link_name; + entry["link_description"] = link_desc; entry["inventory"] = prim ? buildPrimInventoryLLSD(prim) : LLSD::emptyArray(); linked_objects.append(entry); @@ -463,6 +507,7 @@ bool LLPublishedObjectMgr::reconcileLinksetChildAdded( PublishedPrimInfo prim_info; prim_info.mPrimID = child_id; prim_info.mPrimName = get_prim_name(child); + prim_info.mPrimDescription = nv_string(child, "Desc"); prim_info.mLinkNumber = static_cast(info->mPrims.size()) + 1; prim_info.mInventorySerial = -1; info->mPrims.push_back(prim_info); @@ -584,6 +629,7 @@ LLPublishedObjectMgr::reconcileInventoryChanged( if (p.mPrimID == prim_id) { p.mPrimName = get_prim_name(prim); + p.mPrimDescription = nv_string(prim, "Desc"); p.mInventorySerial = 0; break; } @@ -633,7 +679,8 @@ bool LLPublishedObjectMgr::applyPropertyChange( if (prim_id == root_id) { - bool name_changed = (pub_info->mObjectName != name); + bool has_name = !name.empty(); + bool name_changed = has_name && (pub_info->mObjectName != name); bool desc_changed = (pub_info->mObjectDescription != desc); if (!name_changed && !desc_changed) { @@ -659,15 +706,25 @@ bool LLPublishedObjectMgr::applyPropertyChange( { return false; } - if (prim_it->mPrimName == name) + const bool name_changed = !name.empty() && prim_it->mPrimName != name; + const bool desc_changed = prim_it->mPrimDescription != desc; + if (!name_changed && !desc_changed) { return false; } - prim_it->mPrimName = name; LLSD modified_entry; - modified_entry["link_id"] = prim_id; - modified_entry["link_name"] = name; + modified_entry["link_id"] = prim_id; + if (name_changed) + { + prim_it->mPrimName = name; + modified_entry["link_name"] = name; + } + if (desc_changed) + { + prim_it->mPrimDescription = desc; + modified_entry["link_description"] = desc; + } LLSD modified_arr = LLSD::emptyArray(); modified_arr.append(modified_entry); update["changes"]["linked_objects"]["modified"] = modified_arr; @@ -792,8 +849,44 @@ LLPublishedObjectMgr::PublishedObjectInfo& LLPublishedObjectMgr::finalizePending const LLUUID& object_id, PublishedObjectInfo&& info) { PublishedObjectInfo& published_info = mPublishedObjects[object_id]; + auto pending_it = mPendingPublishes.find(object_id); published_info = std::move(info); - published_info.mListeners = takePendingPublishListeners(object_id); + + if (pending_it != mPendingPublishes.end()) + { + PendingPublish& pending = pending_it->second; + if (pending.mHasRootProperties) + { + if (!pending.mObjectName.empty()) + { + published_info.mObjectName = pending.mObjectName; + } + published_info.mObjectDescription = pending.mObjectDescription; + } + + for (PublishedPrimInfo& prim_info : published_info.mPrims) + { + auto name_it = pending.mPrimNames.find(prim_info.mPrimID); + if (name_it != pending.mPrimNames.end() && !name_it->second.empty()) + { + prim_info.mPrimName = name_it->second; + } + + auto desc_it = pending.mPrimDescriptions.find(prim_info.mPrimID); + if (desc_it != pending.mPrimDescriptions.end()) + { + prim_info.mPrimDescription = desc_it->second; + } + } + + published_info.mListeners = std::move(pending.mListeners); + mPendingPublishes.erase(pending_it); + } + else + { + published_info.mListeners = takePendingPublishListeners(object_id); + } + return published_info; } diff --git a/indra/newview/llpublishedobjectmgr.h b/indra/newview/llpublishedobjectmgr.h index 4d4bd55aff..d7eca09d80 100644 --- a/indra/newview/llpublishedobjectmgr.h +++ b/indra/newview/llpublishedobjectmgr.h @@ -49,6 +49,7 @@ public: LLUUID mPrimID; std::string mPrimName; S32 mLinkNumber; + std::string mPrimDescription; S16 mInventorySerial; }; @@ -82,6 +83,11 @@ public: LLUUID mObjectID; std::set mPendingPrims; std::vector> mListeners; + bool mHasRootProperties{ false }; + std::string mObjectName; + std::string mObjectDescription; + std::map mPrimNames; + std::map mPrimDescriptions; }; enum class InventoryChangeKind @@ -153,6 +159,11 @@ public: bool hasPendingPublish(const LLUUID& object_id) const; bool markPendingPublishPrimReady(const LLUUID& object_id, const LLUUID& prim_id); std::vector> takePendingPublishListeners(const LLUUID& object_id); + void recordPendingPropertyChange( + const LLUUID& root_id, + const LLUUID& prim_id, + const std::string& name, + const std::string& desc); void cancelPendingPublish(const LLUUID& object_id); PublishedObjectInfo& finalizePendingPublish(const LLUUID& object_id, PublishedObjectInfo&& info); bool cleanupObjectStateForUnpublish(const LLUUID& object_id); diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp index 3fbcd5c384..95e06583d3 100644 --- a/indra/newview/llscripteditorws.cpp +++ b/indra/newview/llscripteditorws.cpp @@ -2086,7 +2086,13 @@ std::string LLScriptEditorWSServer::getPrimName(LLViewerObject* obj) } LLSelectNode* node = LLSelectMgr::instance().getSelection()->findNode(obj); - return (node && !node->mName.empty()) ? node->mName : std::string(); + if (node && !node->mName.empty()) + { + return node->mName; + } + + // Never emit an empty prim/object name to downstream tooling. + return obj->getID().asString(); } bool LLScriptEditorWSServer::publishObject(const LLUUID& object_id) @@ -2114,6 +2120,13 @@ bool LLScriptEditorWSServer::publishObject(const LLUUID& object_id) // Collect root + all children std::vector prims = collect_linkset(root); + // Request object properties for each prim in the linkset (root + children), + // matching the hover path so name/description metadata is refreshed. + for (LLViewerObject* prim : prims) + { + LLSelectMgr::instance().requestObjectPropertiesFamily(prim); + } + // Set up a PendingPublish to coordinate inventory loading across all prims. // We register a listener and call requestInventory() on every prim. // If inventory is already loaded, requestInventory() fires the callback @@ -2196,6 +2209,31 @@ void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& object_id) LLPublishedObjectMgr::PublishedObjectInfo& published_info = mPublishedObjectManager.finalizePendingPublish(object_id, std::move(info)); + // Align outgoing publish payload with any property responses that arrived + // while inventory-gated publish was still pending. + pub["object_name"] = published_info.mObjectName; + pub["object_description"] = published_info.mObjectDescription; + if (pub.has("linked_objects")) + { + LLSD& linked_objects = pub["linked_objects"]; + for (S32 i = 0; i < linked_objects.size(); ++i) + { + const LLUUID link_id = linked_objects[i]["link_id"].asUUID(); + auto prim_it = std::find_if( + published_info.mPrims.begin(), + published_info.mPrims.end(), + [&](const LLPublishedObjectMgr::PublishedPrimInfo& p) + { + return p.mPrimID == link_id; + }); + if (prim_it != published_info.mPrims.end()) + { + linked_objects[i]["link_name"] = prim_it->mPrimName; + linked_objects[i]["link_description"] = prim_it->mPrimDescription; + } + } + } + // Send notification LLSD message; message["object"] = pub; @@ -2204,6 +2242,13 @@ void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& object_id) LL_INFOS("ScriptEditorWS") << "Published object " << object_id << " (" << pub["object_name"].asString() << ") with " << (prims.size() - 1) << " linked prim(s)" << LL_ENDL; + + // Re-request object properties now that the object is published so + // onObjectPropertyChanged can emit object.update for root and linked prims. + for (LLViewerObject* prim : prims) + { + LLSelectMgr::instance().requestObjectPropertiesFamily(prim); + } } void LLScriptEditorWSServer::onLinksetChildAdded(const LLUUID& root_id, LLViewerObject* child) @@ -2344,6 +2389,8 @@ void LLScriptEditorWSServer::onObjectPropertyChanged( LLUUID root_id = prim->getRootEdit()->getID(); + mPublishedObjectManager.recordPendingPropertyChange(root_id, prim_id, name, desc); + bool should_refresh_inventory = mPublishedObjectManager.markPrimInventorySerialAndDetectChange( root_id, prim_id, -- cgit v1.3 From 93af7beaff8e942e9adf2325ab1ee157d6e2cf8d Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Wed, 12 Aug 2026 09:04:12 -0700 Subject: Issue #https://github.com/secondlife/sl-vscode-plugin/issues/93: This is a temporary fix for the issue. It should be addressed more robustly with #https://github.com/secondlife/sl-vscode-plugin/issues/98. --- indra/newview/llscripteditorws.cpp | 82 +++++++++++++++++++++++++++----------- 1 file changed, 58 insertions(+), 24 deletions(-) diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp index 95e06583d3..d9a4e4b064 100644 --- a/indra/newview/llscripteditorws.cpp +++ b/indra/newview/llscripteditorws.cpp @@ -1,4 +1,4 @@ -/** +/** * @file llscripteditorws.cpp * @brief JSON-RPC 2.0 WebSocket server implementation for external script editor integration * @@ -1930,17 +1930,51 @@ void LLScriptEditorWSServer::sendCompileResults(const std::string &script_id, co void LLScriptEditorWSServer::forwardChatToIDE(const LLChat& chat_msg) const { LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; - auto it = std::find_if(mSubscriptions.begin(), mSubscriptions.end(), - [&chat_msg](const auto& pair) { return (pair.second.mObjectID == chat_msg.mFromID); }); - if (it == mSubscriptions.end()) - { // Not a script we are tracking + LLUUID object_id = chat_msg.mFromID; + bool tracking = false; + bool publish = false; + + LLUUID publish_id = object_id; + LLViewerObject* objectp = gObjectList.findObject(object_id); + if (objectp) + { + LLViewerObject* root = objectp->getRootEdit(); + if (root) + { + publish_id = root->getID(); + } + } + + const EditorSubscription* subinfo = nullptr; + std::string script_id; + // have we either published or subscribed to this object? + if (isObjectPublished(publish_id)) + { + tracking = true; + publish = true; + } + else + { + // If the object is not published, we may still be tracking it if it is a script we are subscribed to + auto it = std::find_if(mSubscriptions.begin(), mSubscriptions.end(), + [&object_id](const auto& pair) { return (pair.second.mObjectID == object_id); }); + if (it != mSubscriptions.end()) + { + tracking = true; + subinfo = &it->second; + script_id = it->first; + } + } + + if (!tracking) + { // Not a script we are tracking return; } bool is_error = false; std::string error_message; - std::string object_name; + std::string object_name = chat_msg.mFromName; std::string script_name; S32 line_number = 0; // We have at least one script from this object, we will forward the message to the IDE @@ -1953,6 +1987,7 @@ void LLScriptEditorWSServer::forwardChatToIDE(const LLChat& chat_msg) const return s.size() >= suffix.size() && std::equal(suffix.rbegin(), suffix.rend(), s.rbegin()); }; + if (!lines.empty() && ends_with(lines.front(), runtime_error_marker)) { is_error = true; @@ -1986,25 +2021,27 @@ void LLScriptEditorWSServer::forwardChatToIDE(const LLChat& chat_msg) const lines.clear(); } - // We should also check that the script name matches one of our subscriptions - if (!script_name.empty() && (it->second.mScriptName != script_name)) - { // right object, wrong script - auto sit = std::find_if(mSubscriptions.begin(), mSubscriptions.end(), - [&chat_msg, &script_name](const auto& pair) - { - return (pair.second.mScriptName == script_name) && (pair.second.mObjectID == chat_msg.mFromID); - }); - if (sit != mSubscriptions.end()) - { // We have a better match - it = sit; + if (subinfo) + { + // We should also check that the script name matches one of our subscriptions + if (!script_name.empty() && (subinfo->mScriptName != script_name)) + { // right object, wrong script + auto sit = + std::find_if(mSubscriptions.begin(), mSubscriptions.end(), [&chat_msg, &script_name](const auto& pair) + { return (pair.second.mScriptName == script_name) && (pair.second.mObjectID == chat_msg.mFromID); }); + if (sit != mSubscriptions.end()) + { // We have a better match + subinfo = &sit->second; + script_id = sit->first; + } } } } - std::string script_id = it->first; + LLSD message; message["script_id"] = script_id; - message["object_id"] = chat_msg.mFromID; - message["object_name"] = chat_msg.mFromName; + message["object_id"] = object_id; + message["object_name"] = object_name; message["message"] = chat_msg.mText; if (is_error) @@ -2021,10 +2058,7 @@ void LLScriptEditorWSServer::forwardChatToIDE(const LLChat& chat_msg) const } } - if (!it->second.mConnection.expired()) - { - it->second.mConnection.lock()->notify(is_error ? "runtime.error" : "runtime.debug", message); - } + notifyAll(is_error ? "runtime.error" : "runtime.debug", message); } void LLScriptEditorWSServer::notifyConnection(U32 connection_id, const std::string& method, const LLSD& params) const -- cgit v1.3 From 32a8200a788b286073a11c81255aac471aa37b67 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Wed, 12 Aug 2026 15:25:20 -0700 Subject: Issue https://github.com/secondlife/sl-vscode-plugin/issues/99: Allow rezzed objects to be saved back to their rezzer's inventory. --- indra/newview/llpublishedobjectmgr.cpp | 1 + indra/newview/llpublishedobjectmgr.h | 2 + indra/newview/llscripteditorws.cpp | 80 ++++++++++++++++++++++++++++++++++ indra/newview/llscripteditorws.h | 1 + indra/newview/llviewermenu.cpp | 34 +++++++++++++-- indra/newview/llviewermenu.h | 2 + 6 files changed, 117 insertions(+), 3 deletions(-) diff --git a/indra/newview/llpublishedobjectmgr.cpp b/indra/newview/llpublishedobjectmgr.cpp index 6f78ba82d9..e9d13c1213 100644 --- a/indra/newview/llpublishedobjectmgr.cpp +++ b/indra/newview/llpublishedobjectmgr.cpp @@ -404,6 +404,7 @@ LLSD LLPublishedObjectMgr::buildObjectListLLSD() const { pub["region"] = info.mRegionName; } + pub["can_save_back"] = info.mCanSaveBackToContents; pub["inventory"] = buildPrimInventoryLLSD(root); LLSD linked_objects = LLSD::emptyArray(); diff --git a/indra/newview/llpublishedobjectmgr.h b/indra/newview/llpublishedobjectmgr.h index d7eca09d80..99ffa34b39 100644 --- a/indra/newview/llpublishedobjectmgr.h +++ b/indra/newview/llpublishedobjectmgr.h @@ -67,6 +67,8 @@ public: std::string mObjectName; std::string mObjectDescription; std::string mRegionName; + bool mCanSaveBackToContents{ false }; + LLUUID mSourceTaskID; std::vector mPrims; std::vector> mListeners; }; diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp index d9a4e4b064..2b77ff1481 100644 --- a/indra/newview/llscripteditorws.cpp +++ b/indra/newview/llscripteditorws.cpp @@ -208,6 +208,12 @@ LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port response["success"] = true; return response; }); + + registerCommand({ "viewer.object.save_back_to_contents", "Save an in-world object back to source object contents" }, + [this](U32 connection_id, const LLSD& p) -> LLSD + { + return this->handleSaveBackToObjectContents(connection_id, p); + }); } LLScriptEditorWSServer::ptr_t LLScriptEditorWSServer::getServer() @@ -918,6 +924,65 @@ bool LLScriptEditorWSConnection::hasFeature(const std::string& feature) const return mFeatures.count(feature) > 0; } +LLSD LLScriptEditorWSServer::handleSaveBackToObjectContents(U32 connection_id, const LLSD& params) +{ + LLUUID object_id = params["object_id"].asUUID(); + if (object_id.isNull()) + { + throw LLJSONRPCConnection::InvalidParams("object_id is required"); + } + + const LLPublishedObjectMgr::PublishedObjectInfo* published_info = + mPublishedObjectManager.getPublished(object_id); + if (!published_info) + { + LLSD response; + response["success"] = false; + response["error_code"] = WSCommandError::InvalidParams; + response["message"] = "Object is not published"; + return response; + } + + if (!published_info->mCanSaveBackToContents || published_info->mSourceTaskID.isNull()) + { + LLSD response; + response["success"] = false; + response["error_code"] = WSCommandError::NotPermitted; + response["message"] = "Save back is not available for this object"; + return response; + } + + LLViewerObject* root = gObjectList.findObject(object_id); + if (!root) + { + LLSD response; + response["success"] = false; + response["error_code"] = WSCommandError::InvalidParams; + response["message"] = "object_id not found"; + return response; + } + + if (!save_object_back_to_contents(root, published_info->mSourceTaskID)) + { + LLSD response; + response["success"] = false; + response["error_code"] = WSCommandError::ExecutionError; + response["message"] = "Failed to save object back to contents"; + return response; + } + + LL_DEBUGS("ScriptEditorWS") << "Save-back requested via command for object " + << object_id << " on connection " << connection_id << LL_ENDL; + + LLSD response; + response["success"] = true; + + LLSD result; + result["object_id"] = object_id; + response["result"] = result; + return response; +} + LLSD LLScriptEditorWSServer::handleCommandExecute(U32 connection_id, const LLSD& params) { const std::string command = params["command"].asString(); @@ -2228,6 +2293,20 @@ void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& object_id) { info.mRegionName = root->getRegion()->getName(); } + LLSelectNode* root_select_node = LLSelectMgr::instance().getSelection()->findNode(root); + if (root_select_node + && root_select_node->mValid + && !root_select_node->mFromTaskID.isNull() + && !root->isAttachment()) + { + info.mCanSaveBackToContents = true; + info.mSourceTaskID = root_select_node->mFromTaskID; + } + else + { + info.mCanSaveBackToContents = false; + info.mSourceTaskID.setNull(); + } S32 link_num = 1; std::vector prims = collect_linkset(root); @@ -2246,6 +2325,7 @@ void LLScriptEditorWSServer::buildAndSendPublish(const LLUUID& object_id) // Align outgoing publish payload with any property responses that arrived // while inventory-gated publish was still pending. pub["object_name"] = published_info.mObjectName; + pub["can_save_back"] = published_info.mCanSaveBackToContents; pub["object_description"] = published_info.mObjectDescription; if (pub.has("linked_objects")) { diff --git a/indra/newview/llscripteditorws.h b/indra/newview/llscripteditorws.h index 63b60335ff..ded83e6369 100644 --- a/indra/newview/llscripteditorws.h +++ b/indra/newview/llscripteditorws.h @@ -247,6 +247,7 @@ protected: LLSD handleObjectModify(U32 connection_id, const LLSD& params); LLSD handleObjectItemModify(U32 connection_id, const LLSD& params); LLSD handleCommandExecute(U32 connection_id, const LLSD& params); + LLSD handleSaveBackToObjectContents(U32 connection_id, const LLSD& params); LLSD handleCommandList(); void sendCommandExecute(U32 connection_id, const std::string& command, const LLSD& params); diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index dc509479c4..eaaa923b59 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -5136,6 +5136,31 @@ static void derez_objects(EDeRezDestination dest, const LLUUID& dest_id) derez_objects(dest, dest_id, first_region, error, NULL); } +bool save_object_back_to_contents(LLViewerObject* object, const LLUUID& source_task_id) +{ + if (!object || source_task_id.isNull()) + { + return false; + } + + LLViewerRegion* first_region = object->getRegion(); + if (!first_region) + { + return false; + } + + std::vector objects; + objects.push_back(object); + + std::string error; + derez_objects(DRD_SAVE_INTO_TASK_INVENTORY, source_task_id, first_region, error, &objects); + if (!error.empty()) + { + return false; + } + return true; +} + static void derez_objects_separate(EDeRezDestination dest, const LLUUID &dest_id) { std::vector derez_object_list; @@ -5710,10 +5735,13 @@ class LLToolsSaveToObjectInventory : public view_listener_t bool handleEvent(const LLSD& userdata) { LLSelectNode* node = LLSelectMgr::getInstance()->getSelection()->getFirstRootNode(); - if(node && (node->mValid) && (!node->mFromTaskID.isNull())) + if (node && node->mValid && !node->mFromTaskID.isNull()) { - // *TODO: check to see if the fromtaskid object exists. - derez_objects(DRD_SAVE_INTO_TASK_INVENTORY, node->mFromTaskID); + LLViewerObject* object = node->getObject(); + if (object) + { + save_object_back_to_contents(object, node->mFromTaskID); + } } return true; } diff --git a/indra/newview/llviewermenu.h b/indra/newview/llviewermenu.h index 522c7e8109..6699780a76 100644 --- a/indra/newview/llviewermenu.h +++ b/indra/newview/llviewermenu.h @@ -37,6 +37,7 @@ class LLView; class LLParcelSelection; class LLObjectSelection; class LLSelectNode; +class LLViewerObject; void initialize_edit_menu(); void initialize_spellcheck_menu(); @@ -74,6 +75,7 @@ void handle_take(bool take_separate = false); void handle_take_copy(); void handle_look_at_selection(const LLSD& param); bool handle_zoom_to_object(const LLUUID& object_id); +bool save_object_back_to_contents(LLViewerObject* object, const LLUUID& source_task_id); void handle_object_return(); void handle_object_delete(); void handle_object_edit(); -- cgit v1.3 From 222f568123c0ac9b6e4ed7be0dae020c8cb3f832 Mon Sep 17 00:00:00 2001 From: Rider Linden Date: Wed, 12 Aug 2026 16:08:52 -0700 Subject: Issue 6131: Show toast when starting and stopping the script edit server. --- indra/newview/llscripteditorws.cpp | 10 ++++++++++ .../newview/skins/default/xui/en/notifications.xml | 21 +++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp index 2b77ff1481..0487ebd06d 100644 --- a/indra/newview/llscripteditorws.cpp +++ b/indra/newview/llscripteditorws.cpp @@ -45,6 +45,7 @@ #include "llinventorytype.h" #include "llinventorydefines.h" #include "llnotecard.h" +#include "llnotificationsutil.h" #include "llpreviewnotecard.h" #include "llpreviewscript.h" #include "llprocess.h" @@ -261,11 +262,18 @@ LLScriptEditorWSServer::ptr_t LLScriptEditorWSServer::ensureServerRunning() if (!server->isRunning()) { + U16 port = static_cast(gSavedSettings.getS32("ExternalWebsocketSyncPort")); + LLSD args; + args["PORT"] = static_cast(port); + if (!wsmgr.startServer(DEFAULT_SERVER_NAME)) { LL_WARNS("ScriptEditorWS") << "Failed to start script editor websocket server" << LL_ENDL; + LLNotificationsUtil::add("ExternalEditorServerFailed", args); return nullptr; } + + LLNotificationsUtil::add("ExternalEditorServerStarted", args); } return server; @@ -377,6 +385,8 @@ void LLScriptEditorWSServer::onStopped() mActiveConnections.clear(); LL_INFOS("ScriptEditorWS") << "Script editor WebSocket server stopped, all state cleaned up" << LL_ENDL; + + LLNotificationsUtil::add("ExternalEditorServerStopped"); } void LLScriptEditorWSServer::onConnectionOpened(const LLWebsocketMgr::WSConnection::ptr_t& connection) diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml index a9adb772e5..d45dcfc87e 100644 --- a/indra/newview/skins/default/xui/en/notifications.xml +++ b/indra/newview/skins/default/xui/en/notifications.xml @@ -7056,6 +7056,27 @@ The string [STRING_NAME] is missing from strings.xml type="notifytip"> [MESSAGE] + + +External editor server started on port [PORT]. + + + +Failed to start external editor server on port [PORT]. + + + +External editor server stopped. +