summaryrefslogtreecommitdiff
path: root/indra/newview/llpublishedobjectmgr.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'indra/newview/llpublishedobjectmgr.cpp')
-rw-r--r--indra/newview/llpublishedobjectmgr.cpp1309
1 files changed, 1309 insertions, 0 deletions
diff --git a/indra/newview/llpublishedobjectmgr.cpp b/indra/newview/llpublishedobjectmgr.cpp
new file mode 100644
index 0000000000..d92bb382de
--- /dev/null
+++ b/indra/newview/llpublishedobjectmgr.cpp
@@ -0,0 +1,1309 @@
+/**
+ * @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 "llchat.h"
+#include "llinventorydefines.h"
+#include "llregex.h"
+#include "llselectmgr.h"
+#include "llviewerinventory.h"
+#include "llviewerobject.h"
+#include "llviewerobjectlist.h"
+#include "llviewerregion.h"
+#include "llvoinventorylistener.h"
+
+namespace
+{
+ static const boost::regex LUAU_LOCATION_PATTERN(
+ R"(^([^:]*):([0-9]+):\s*(.*)$)");
+
+ static const boost::regex LSL_LOCATION_PATTERN(
+ R"(\((\d+), (\d+)\) : ([^:]+) : (.+))");
+
+ 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,
+ RuntimeEventCallback runtime_event_callback):
+ mServer(server),
+ mRuntimeEventCallback(std::move(runtime_event_callback)),
+ mRuntimeEventAggregator(std::make_unique<RuntimeEventAggregator>(
+ [this](const RuntimeEventAggregator::RuntimeEvent& runtime_event)
+ {
+ std::optional<RuntimeChatEvent> event =
+ buildRuntimeChatEvent(runtime_event);
+ if (event && mRuntimeEventCallback)
+ {
+ mRuntimeEventCallback(*event);
+ }
+ })),
+ mRuntimeFlushTimer(
+ std::unique_ptr<LLEventTimer>(
+ LLEventTimer::run_every(
+ RUNTIME_FLUSH_INTERVAL,
+ [this]()
+ {
+ flushExpiredRuntimeFragments();
+ })))
+{
+}
+
+LLPublishedObjectMgr::~LLPublishedObjectMgr() = default;
+
+void LLPublishedObjectMgr::ingestRuntimeChat(
+ const LLChat& chat_msg,
+ RuntimeEventAggregator::Channel channel)
+{
+ static const boost::regex runtime_error_header(
+ R"(^(.+?)\s+\[script:([^\]]+)\]\s+Script run-time error)");
+
+ std::vector<std::string> lines =
+ LLStringUtil::getTokens(chat_msg.mText, "\n");
+ boost::smatch match;
+ bool is_error_header =
+ !lines.empty() &&
+ boost::regex_match(lines.front(), match, runtime_error_header);
+
+ if (!is_error_header && !mRuntimeEventAggregator->hasPending())
+ {
+ RuntimeEventAggregator::RuntimeEvent runtime_event;
+ runtime_event.mVM = RuntimeEventAggregator::VM::LSL2;
+ runtime_event.mChannel = channel;
+
+ RuntimeEventAggregator::Fragment fragment;
+ fragment.mFromID = chat_msg.mFromID;
+ fragment.mFromName = chat_msg.mFromName;
+ fragment.mText = chat_msg.mText;
+ runtime_event.mFragments.push_back(std::move(fragment));
+
+ std::optional<RuntimeChatEvent> event =
+ buildRuntimeChatEvent(runtime_event);
+ if (event && mRuntimeEventCallback)
+ {
+ mRuntimeEventCallback(*event);
+ }
+ return;
+ }
+
+ RuntimeEventAggregator::VM vm = RuntimeEventAggregator::VM::LSL2;
+ LLViewerObject* prim = gObjectList.findObject(chat_msg.mFromID);
+ if (prim)
+ {
+ std::vector<std::string> lines =
+ LLStringUtil::getTokens(chat_msg.mText, "\n");
+ static const boost::regex runtime_error_header(
+ R"(^(.+?)\s+\[script:([^\]]+)\]\s+Script run-time error)");
+ boost::smatch match;
+
+ if (!lines.empty() &&
+ boost::regex_match(lines.front(), match, runtime_error_header))
+ {
+ const std::string script_name = match[2].str();
+ LLInventoryObject::object_list_t inventory;
+ prim->getInventoryContents(inventory);
+ for (const auto& inventory_object : inventory)
+ {
+ LLInventoryItem* item =
+ dynamic_cast<LLInventoryItem*>(inventory_object.get());
+ if (item && item->getName() == script_name &&
+ item->getRuntime() == "luau")
+ {
+ vm = RuntimeEventAggregator::VM::LUAU;
+ break;
+ }
+ }
+ }
+ }
+
+ mRuntimeEventAggregator->ingest(
+ chat_msg.mFromID,
+ chat_msg.mFromName,
+ chat_msg.mText,
+ vm,
+ channel);
+}
+
+void LLPublishedObjectMgr::flushExpiredRuntimeFragments()
+{
+ mRuntimeEventAggregator->flushExpired();
+}
+
+std::optional<LLPublishedObjectMgr::RuntimeChatEvent>
+LLPublishedObjectMgr::buildRuntimeChatEvent(
+ const RuntimeEventAggregator::RuntimeEvent& runtime_event) const
+{
+ if (runtime_event.mFragments.empty())
+ {
+ return std::nullopt;
+ }
+
+ const RuntimeEventAggregator::Fragment& source =
+ runtime_event.mFragments.front();
+ LLViewerObject* prim = gObjectList.findObject(source.mFromID);
+ if (!prim)
+ {
+ return std::nullopt;
+ }
+
+ LLViewerObject* root = prim->getRootEdit();
+ if (!root)
+ {
+ return std::nullopt;
+ }
+
+ RuntimeChatEvent event;
+ event.mChannel = runtime_event.mChannel;
+ event.mVM = runtime_event.mVM;
+ event.mRootID = root->getID();
+ event.mPrimID = prim->getID();
+ event.mObjectName = source.mFromName;
+ for (const auto& fragment : runtime_event.mFragments)
+ {
+ if (!event.mMessage.empty())
+ {
+ event.mMessage += "\n";
+ }
+ event.mMessage += fragment.mText;
+ }
+
+ std::vector<std::string> lines =
+ LLStringUtil::getTokens(event.mMessage, "\n");
+ static const std::string runtime_error_marker = "Script run-time error";
+ static const boost::regex runtime_error_header(
+ R"(^(.+?)\s+\[script:([^\]]+)\]\s+Script run-time error)");
+
+ auto ends_with = [](const std::string& value, const std::string& suffix)
+ {
+ return value.size() >= suffix.size() &&
+ std::equal(suffix.rbegin(), suffix.rend(), value.rbegin());
+ };
+
+ if (!lines.empty() && ends_with(lines.front(), runtime_error_marker))
+ {
+ event.mIsError = true;
+ boost::smatch match;
+ if (boost::regex_match(lines.front(), match, runtime_error_header))
+ {
+ event.mObjectName = match[1].str();
+ event.mScriptName = match[2].str();
+ lines.erase(lines.begin());
+ }
+ else
+ {
+ lines.clear();
+ }
+ }
+
+ LLInventoryObject::object_list_t inventory;
+ prim->getInventoryContents(inventory);
+ for (const auto& inventory_object : inventory)
+ {
+ LLInventoryItem* item =
+ dynamic_cast<LLInventoryItem*>(inventory_object.get());
+ if (item && !event.mScriptName.empty() &&
+ item->getName() == event.mScriptName)
+ {
+ event.mItemID = item->getUUID();
+ break;
+ }
+ }
+
+ if (event.mIsError && !lines.empty())
+ {
+ RuntimeEventAggregator::VM vm = runtime_event.mVM;
+ if (event.mItemID.notNull())
+ {
+ LLInventoryItem* item =
+ dynamic_cast<LLInventoryItem*>(prim->getInventoryObject(event.mItemID));
+ if (item && item->getRuntime() == "luau")
+ {
+ vm = RuntimeEventAggregator::VM::LUAU;
+ }
+ }
+
+ RuntimeEventAggregator::ParsedError parsed =
+ mRuntimeEventAggregator->parseError(vm, runtime_event.mFragments);
+ event.mError = parsed.mError;
+ event.mLine = parsed.mLine;
+ event.mColumn = parsed.mColumn;
+ event.mStack = lines;
+ }
+
+ return event;
+}
+
+LLPublishedObjectMgr::RuntimeEventAggregator::RuntimeEventAggregator(
+ FlushCallback flush_callback):
+ mFlushCallback(std::move(flush_callback))
+{
+}
+
+void LLPublishedObjectMgr::RuntimeEventAggregator::ingest(
+ const LLUUID& from_id,
+ const std::string& from_name,
+ const std::string& text,
+ VM vm,
+ Channel channel)
+{
+ if (mPending &&
+ isNewBurst(from_id, from_name, vm, channel))
+ {
+ flushPending();
+ }
+
+ if (!mPending)
+ {
+ mPending = std::make_unique<PendingBurst>();
+ mPending->mFromID = from_id;
+ mPending->mFromName = from_name;
+ mPending->mVM = vm;
+ mPending->mChannel = channel;
+ }
+
+ Fragment fragment;
+ fragment.mFromID = from_id;
+ fragment.mFromName = from_name;
+ fragment.mText = text;
+ mPending->mFragments.push_back(std::move(fragment));
+ mPending->mTimer.setTimerExpirySec(FRAGMENT_TIMEOUT);
+}
+
+void LLPublishedObjectMgr::RuntimeEventAggregator::flushExpired()
+{
+ if (mPending && mPending->mTimer.hasExpired())
+ {
+ flushPending();
+ }
+}
+
+void LLPublishedObjectMgr::RuntimeEventAggregator::flush()
+{
+ flushPending();
+}
+
+bool LLPublishedObjectMgr::RuntimeEventAggregator::hasPending() const
+{
+ return mPending != nullptr;
+}
+
+LLPublishedObjectMgr::RuntimeEventAggregator::ParsedError
+LLPublishedObjectMgr::RuntimeEventAggregator::parseError(
+ VM vm,
+ const fragments_t& fragments) const
+{
+ ParsedError parsed;
+ const boost::regex* location_pattern = nullptr;
+ bool lsl_coordinates = false;
+
+ switch (vm)
+ {
+ case VM::LUAU:
+ location_pattern = &LUAU_LOCATION_PATTERN;
+ break;
+ case VM::LSL2:
+ location_pattern = &LSL_LOCATION_PATTERN;
+ lsl_coordinates = true;
+ break;
+ }
+
+ for (const auto& fragment : fragments)
+ {
+ std::vector<std::string> lines =
+ LLStringUtil::getTokens(fragment.mText, "\n");
+ for (const auto& line : lines)
+ {
+ boost::smatch match;
+ if (!boost::regex_match(line, match, *location_pattern))
+ {
+ continue;
+ }
+
+ if (lsl_coordinates)
+ {
+ parsed.mLine = static_cast<S32>(
+ std::strtol(match[1].str().c_str(), nullptr, 10)) + 1;
+ parsed.mColumn = static_cast<S32>(
+ std::strtol(match[2].str().c_str(), nullptr, 10)) + 1;
+ parsed.mError = match[4].str();
+ }
+ else
+ {
+ parsed.mSource = match[1].str();
+ parsed.mLine = static_cast<S32>(
+ std::strtol(match[2].str().c_str(), nullptr, 10));
+ parsed.mColumn = 0;
+ parsed.mError = match[3].str();
+ }
+ return parsed;
+ }
+ }
+
+ // LSL runtime errors commonly arrive as plain text without source
+ // location metadata, for example: "Math Error".
+ if (vm == VM::LSL2)
+ {
+ for (const auto& fragment : fragments)
+ {
+ const std::vector<std::string> lines =
+ LLStringUtil::getTokens(fragment.mText, "\n");
+ for (const auto& line : lines)
+ {
+ if (line.empty() ||
+ line.find("Script run-time error") != std::string::npos)
+ {
+ continue;
+ }
+
+ parsed.mError = line;
+ return parsed;
+ }
+ }
+ }
+
+ return parsed;
+}
+
+void LLPublishedObjectMgr::RuntimeEventAggregator::flushPending()
+{
+ if (!mPending)
+ {
+ return;
+ }
+
+ if (!mFlushCallback)
+ {
+ mPending.reset();
+ return;
+ }
+
+ RuntimeEvent event;
+ event.mVM = mPending->mVM;
+ event.mChannel = mPending->mChannel;
+ event.mFragments = mPending->mFragments;
+ event.mError = parseError(mPending->mVM, mPending->mFragments);
+ mFlushCallback(event);
+
+ mPending.reset();
+}
+
+bool LLPublishedObjectMgr::RuntimeEventAggregator::isNewBurst(
+ const LLUUID& from_id,
+ const std::string& from_name,
+ VM vm,
+ Channel channel) const
+{
+ return mPending->mFromID != from_id ||
+ mPending->mFromName != from_name ||
+ mPending->mVM != vm ||
+ mPending->mChannel != channel;
+}
+
+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();
+}