summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRider Linden <rider@lindenlab.com>2026-08-13 09:03:27 -0700
committerGitHub <noreply@github.com>2026-08-13 09:03:27 -0700
commitc8730531ee1a030c4e71f2b7ddb7d3394c9af7b7 (patch)
treebfbdb7e46233b91581db5b8ead7b4ac9a9c4e111
parent21bb60fc65e7fa6c3cf472a29cda220f653af1d8 (diff)
parent222f568123c0ac9b6e4ed7be0dae020c8cb3f832 (diff)
Merge pull request #6104 from secondlife/rider/explore_selection
Rider/explore selection
-rw-r--r--doc/external-editor-json-rpc.md140
-rw-r--r--indra/newview/CMakeLists.txt2
-rw-r--r--indra/newview/llpublishedobjectmgr.cpp932
-rw-r--r--indra/newview/llpublishedobjectmgr.h209
-rw-r--r--indra/newview/llscripteditorws.cpp923
-rw-r--r--indra/newview/llscripteditorws.h64
-rw-r--r--indra/newview/llselectmgr.cpp2
-rw-r--r--indra/newview/llviewermenu.cpp34
-rw-r--r--indra/newview/llviewermenu.h2
-rw-r--r--indra/newview/skins/default/xui/en/notifications.xml21
10 files changed, 1805 insertions, 524 deletions
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<string, unknown>; // 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<string, CommandParamInfo>;
+}
+
+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/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..e9d13c1213
--- /dev/null
+++ b/indra/newview/llpublishedobjectmgr.cpp
@@ -0,0 +1,932 @@
+/**
+ * @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);
+ if (node && !node->mName.empty())
+ {
+ return node->mName;
+ }
+
+ // Never emit an empty prim/object name to downstream tooling.
+ return obj->getID().asString();
+ }
+}
+
+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<LLViewerObject*>& prims)
+{
+ PendingPublish pending;
+ pending.mObjectID = object_id;
+ for (LLViewerObject* prim : prims)
+ {
+ pending.mPendingPrims.insert(prim->getID());
+ auto listener = std::make_unique<LLPublishedPrimListener>(
+ 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();
+}
+
+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<std::unique_ptr<LLPublishedPrimListener>> 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<LLInventoryItem*>(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<S32>(subtype);
+
+ const std::string& runtime = item->getRuntime();
+ if (!runtime.empty())
+ {
+ entry["vm"] = runtime;
+ }
+
+ LLViewerInventoryItem* viewer_item = dynamic_cast<LLViewerInventoryItem*>(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<S32>(perms.getMaskOwner());
+ perm_entry["next_owner"] = static_cast<S32>(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["can_save_back"] = info.mCanSaveBackToContents;
+ 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["link_description"] = prim_info.mPrimDescription;
+ 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;
+ }
+ 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);
+ }
+
+ 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.mPrimDescription = nv_string(child, "Desc");
+ prim_info.mLinkNumber = static_cast<S32>(info->mPrims.size()) + 1;
+ prim_info.mInventorySerial = -1;
+ info->mPrims.push_back(prim_info);
+
+ auto listener = std::make_unique<LLPublishedPrimListener>(
+ 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<LLPublishedPrimListener>& 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.mPrimDescription = nv_string(prim, "Desc");
+ 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 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)
+ {
+ 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;
+ }
+ 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;
+ }
+
+ LLSD modified_entry;
+ 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;
+ 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<LLEventTimer>& 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];
+ auto pending_it = mPendingPublishes.find(object_id);
+ published_info = std::move(info);
+
+ 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;
+}
+
+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..99ffa34b39
--- /dev/null
+++ b/indra/newview/llpublishedobjectmgr.h
@@ -0,0 +1,209 @@
+/**
+ * @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 <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <vector>
+
+class LLScriptEditorWSServer;
+class LLPublishedPrimListener;
+class LLViewerObject;
+
+class LLPublishedObjectMgr
+{
+public:
+ struct PublishedPrimInfo
+ {
+ LLUUID mPrimID;
+ std::string mPrimName;
+ S32 mLinkNumber;
+ std::string mPrimDescription;
+ 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;
+ bool mCanSaveBackToContents{ false };
+ LLUUID mSourceTaskID;
+ std::vector<PublishedPrimInfo> mPrims;
+ std::vector<std::unique_ptr<LLPublishedPrimListener>> 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<LLUUID> mPendingPrims;
+ std::vector<std::unique_ptr<LLPublishedPrimListener>> mListeners;
+ bool mHasRootProperties{ false };
+ std::string mObjectName;
+ std::string mObjectDescription;
+ std::map<LLUUID, std::string> mPrimNames;
+ std::map<LLUUID, std::string> mPrimDescriptions;
+ };
+
+ 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 <typename Fn>
+ 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<LLViewerObject*>& prims);
+ bool hasPendingPublish(const LLUUID& object_id) const;
+ bool markPendingPublishPrimReady(const LLUUID& object_id, const LLUUID& prim_id);
+ std::vector<std::unique_ptr<LLPublishedPrimListener>> 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);
+ 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<LLEventTimer>& 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<LLUUID, PublishedObjectInfo>;
+ using pending_publish_map_t = std::map<LLUUID, PendingPublish>;
+ using pending_item_create_map_t = std::map<LLUUID, std::string>;
+ using new_child_prims_map_t = std::map<LLUUID, std::set<LLUUID>>;
+ using linkset_flush_timer_map_t = std::map<LLUUID, std::weak_ptr<LLEventTimer>>;
+ using inventory_request_start_map_t = std::map<LLUUID, F64>;
+
+ 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..0487ebd06d 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"
@@ -44,6 +45,7 @@
#include "llinventorytype.h"
#include "llinventorydefines.h"
#include "llnotecard.h"
+#include "llnotificationsutil.h"
#include "llpreviewnotecard.h"
#include "llpreviewscript.h"
#include "llprocess.h"
@@ -61,6 +63,7 @@
#include "llviewerobject.h"
#include "llviewerobjectlist.h"
#include "llviewerregion.h"
+#include "llviewermenu.h"
#include "llviewertexteditor.h"
#include "llvoinventorylistener.h"
#include "roles_constants.h"
@@ -159,52 +162,59 @@ namespace
}
-class LLPublishedPrimListener : public LLVOInventoryListener
+//========================================================================
+LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only):
+ LLJSONRPCServer(name, port, local_only),
+ mPublishedObjectManager(this)
{
-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);
- }
+ LL_INFOS("ScriptEditorWS") << "Created JSON-RPC script editor server: " << name
+ << " on port " << port << LL_ENDL;
- ~LLPublishedPrimListener() override = default;
+ 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");
- void inventoryChanged(LLViewerObject* object,
- LLInventoryObject::object_list_t* inventory,
- S32 serial_num, void* user_data) override
- {
- if (mServer)
+ 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
{
- if (mServer->isObjectPublished(mObjectID))
- {
- mServer->onPrimInventoryChanged(mObjectID, mPrimID);
- }
- else
+ 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))
{
- mServer->onPrimInventoryReady(mObjectID, mPrimID);
+ LLSD response;
+ response["success"] = false;
+ response["error_code"] = WSCommandError::ExecutionError;
+ response["message"] = "Object not found or not reachable";
+ return response;
}
- }
- }
- 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
-};
+ LLSD response;
+ response["success"] = true;
+ return response;
+ });
-//========================================================================
-LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port, bool local_only):
- LLJSONRPCServer(name, port, local_only)
-{
- LL_INFOS("ScriptEditorWS") << "Created JSON-RPC script editor server: " << name
- << " on port " << port << LL_ENDL;
+ 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()
@@ -252,11 +262,18 @@ LLScriptEditorWSServer::ptr_t LLScriptEditorWSServer::ensureServerRunning()
if (!server->isRunning())
{
+ U16 port = static_cast<U16>(gSavedSettings.getS32("ExternalWebsocketSyncPort"));
+ LLSD args;
+ args["PORT"] = static_cast<S32>(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;
@@ -362,22 +379,14 @@ 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();
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)
@@ -672,59 +681,25 @@ 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();
+ }));
}
}
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 +791,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 +804,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 +816,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 +864,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 +873,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 +885,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<LLViewerInventoryItem> new_item =
new LLViewerInventoryItem(static_cast<LLViewerInventoryItem*>(v.item));
@@ -952,14 +913,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();
@@ -967,6 +924,138 @@ 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::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();
+ 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();
@@ -1205,6 +1294,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 +1614,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 +1643,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 +1762,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<void> pending_guard(nullptr, [this, prim_id](void*)
{
- mPendingItemCreates.erase(prim_id);
+ mPublishedObjectManager.clearPendingItemCreate(prim_id);
});
if (has_cap)
@@ -1681,7 +1792,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<LLViewerInventoryItem> new_item =
new LLViewerInventoryItem(
LLUUID::null, LLUUID::null, perms, LLUUID::null,
@@ -1894,17 +2005,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
@@ -1917,6 +2062,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;
@@ -1950,25 +2096,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)
@@ -1985,10 +2133,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
@@ -2014,6 +2159,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();
@@ -2049,70 +2195,13 @@ std::string LLScriptEditorWSServer::getPrimName(LLViewerObject* obj)
}
LLSelectNode* node = LLSelectMgr::instance().getSelection()->findNode(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)
+ if (node && !node->mName.empty())
{
- LLInventoryItem* item = dynamic_cast<LLInventoryItem*>(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<S32>(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<LLViewerInventoryItem*>(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<S32>(perms.getMaskOwner());
- perm_entry["next_owner"] = static_cast<S32>(perms.getMaskNextOwner());
- entry["permissions"] = perm_entry;
-
- entry["creator_id"] = perms.getCreator();
-
- items.append(entry);
+ return node->mName;
}
- return items;
+ // Never emit an empty prim/object name to downstream tooling.
+ return obj->getID().asString();
}
bool LLScriptEditorWSServer::publishObject(const LLUUID& object_id)
@@ -2140,33 +2229,30 @@ bool LLScriptEditorWSServer::publishObject(const LLUUID& object_id)
// Collect root + all children
std::vector<LLViewerObject*> 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
// 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<LLPublishedPrimListener>(
- 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 +2261,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 +2287,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();
@@ -2257,12 +2303,26 @@ 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<LLViewerObject*> 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 +2330,33 @@ 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));
+
+ // 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"))
+ {
+ 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;
@@ -2282,38 +2366,34 @@ 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)
{
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<S32>(info.mPrims.size()) + 1;
- prim_info.mInventorySerial = -1; // sentinel: not yet loaded
- info.mPrims.push_back(prim_info);
+ if (!child)
+ {
+ return;
+ }
- // Register a listener so we are notified when the child's inventory arrives.
- // The listener constructor calls registerVOInventoryListener internally.
- auto listener = std::make_unique<LLPublishedPrimListener>(this, root_id, child_id, child);
- info.mListeners.push_back(std::move(listener));
+ if (!mPublishedObjectManager.reconcileLinksetChildAdded(
+ root_id,
+ child,
+ LLTimer::getTotalSeconds().value()))
+ {
+ return;
+ }
- // Request inventory (async; fires onPrimInventoryChanged when ready)
+ // Request inventory (async; fires onPrimInventoryChanged when ready).
child->requestInventory();
- // Mark as pending — the flush waits until this is cleared
- mNewChildPrims[root_id].insert(child_id);
-
// Start safety-timeout timer (no-op if one is already pending for this root)
scheduleLinksetFlush(root_id, LINKSET_ADD_FLUSH_DELAY);
}
@@ -2321,115 +2401,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<LLPublishedPrimListener>& 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())
- {
- 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 (!mPublishedObjectManager.reconcileLinksetChildRemoved(root_id, child_id))
{
- 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<LLScriptEditorWSServer>(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 +2454,94 @@ 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);
-}
+ mPublishedObjectManager.recordPendingPropertyChange(root_id, prim_id, name, desc);
-void LLScriptEditorWSServer::cleanupPrimListeners(const LLUUID& object_id)
-{
- // Clear any pending publish listeners
- auto pending_it = mPendingPublishes.find(object_id);
- if (pending_it != mPendingPublishes.end())
+ bool should_refresh_inventory = mPublishedObjectManager.markPrimInventorySerialAndDetectChange(
+ root_id,
+ prim_id,
+ inventory_serial);
+
+ 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())
@@ -2645,6 +2598,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();
@@ -2780,3 +2734,4 @@ std::string LLScriptEditorWSConnection::generateChallenge()
return mChallengeFile;
}
+
diff --git a/indra/newview/llscripteditorws.h b/indra/newview/llscripteditorws.h
index 69c7e1a8e1..ded83e6369 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"
@@ -38,6 +39,7 @@
#include <map>
#include <set>
#include <atomic>
+#include <functional>
// Forward declarations
class LLLiveLSLEditor;
@@ -45,7 +47,6 @@ class LLScriptEdContainer;
class LLScriptEditorWSServer;
class LLChat;
class LLPanel;
-class LLPublishedPrimListener;
class LLViewerObject;
class LLInventoryItem;
@@ -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<std::string>;
@@ -205,9 +207,11 @@ 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);
+ 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 +246,10 @@ 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;
+ 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);
struct ValidatedItem
{
@@ -255,10 +262,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 +308,6 @@ private:
};
using subscriptions_t = std::unordered_map<std::string, EditorSubscription>;
- 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<PublishedPrimInfo> mPrims; // root + all children
- std::vector<std::unique_ptr<LLPublishedPrimListener>> mListeners;
- };
-
- struct PendingPublish
- {
- LLUUID mObjectID;
- std::set<LLUUID> mPendingPrims; // prims whose inventory we're still waiting for
- std::vector<std::unique_ptr<LLPublishedPrimListener>> mListeners; // listeners for pending prims
- };
-
SubscriptionError updateScriptSubscription(const std::string &script_id, U32 connection_id);
void unsubscribeConnection(U32 connection_id);
@@ -340,11 +319,24 @@ private:
std::unordered_map<U32, S32> mConnectionSubscriptionCounts;
std::map<U32, LLScriptEditorWSConnection::wptr_t> mActiveConnections;
- std::map<LLUUID, PublishedObjectInfo> mPublishedObjects; // keyed by root object_id
- std::map<LLUUID, PendingPublish> mPendingPublishes; // keyed by root object_id
- std::map<LLUUID, std::string> mPendingItemCreates; // prim_id -> pump name awaiting inventory update
- std::map<LLUUID, std::set<LLUUID>> mNewChildPrims; // root_id → children awaiting first inventory response
- std::map<LLUUID, std::weak_ptr<LLEventTimer>> mLinksetFlushTimers; // root_id → pending coalesce timer
+ 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<LLSD(U32 connection_id, const LLSD& params)>;
+ std::unordered_map<std::string, std::pair<WSCommandInfo, WSCommandHandler>> mCommandRegistry;
+
+ void registerCommand(const WSCommandInfo& info, WSCommandHandler handler);
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);
}
}
}
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<LLViewerObjectPtr> 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<LLViewerObjectPtr> 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();
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]
</notification>
+
+ <notification
+ icon="notifytip.tga"
+ name="ExternalEditorServerStarted"
+ type="notifytip">
+External editor server started on port [PORT].
+ </notification>
+
+ <notification
+ icon="notifytip.tga"
+ name="ExternalEditorServerFailed"
+ type="notifytip">
+Failed to start external editor server on port [PORT].
+ </notification>
+
+ <notification
+ icon="notifytip.tga"
+ name="ExternalEditorServerStopped"
+ type="notifytip">
+External editor server stopped.
+ </notification>
<notification
icon="notifytip.tga"