summaryrefslogtreecommitdiff
path: root/indra/newview/llviewermcp.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'indra/newview/llviewermcp.cpp')
-rw-r--r--indra/newview/llviewermcp.cpp3442
1 files changed, 3442 insertions, 0 deletions
diff --git a/indra/newview/llviewermcp.cpp b/indra/newview/llviewermcp.cpp
new file mode 100644
index 0000000000..b9937a8cec
--- /dev/null
+++ b/indra/newview/llviewermcp.cpp
@@ -0,0 +1,3442 @@
+/**
+ * @file llviewermcp.cpp
+ * @brief Loopback MCP server for the running viewer session.
+ *
+ * Listens on 127.0.0.1 when EnableViewerMCP is set. Tool calls run on the
+ * main thread. This controls the logged-in viewer, not a second avatar.
+ */
+
+#include "llviewerprecompiledheaders.h"
+
+#include "llviewermcp.h"
+#include "llthread.h"
+#include "llfloatermodelpreview.h"
+#include "llmaterialeditor.h"
+#include "llvovolume.h"
+#include "llviewerwindow.h"
+#include "llsnapshotmodel.h"
+#include "llgltfmateriallist.h"
+#include "lldate.h"
+
+#include "llassetstorage.h"
+#include "llfilesystem.h"
+#include "llagent.h"
+#include "llagentcamera.h"
+#include "llagentdata.h"
+#include "llagentui.h"
+#include "llappearancemgr.h"
+#include "llcommandhandler.h"
+#include "lldir.h"
+#include "llhudeffectlookat.h"
+#include "llimview.h"
+#include "llinventorybridge.h"
+#include "llinventorymodel.h"
+#include "llnamevalue.h"
+#include "llpermissions.h"
+#include "llfloaterreg.h"
+#include "llpreviewscript.h"
+#include "llprimitive.h"
+#include "roles_constants.h"
+#include "llselectmgr.h"
+#include "llstartup.h"
+#include "lltooldraganddrop.h"
+#include "lltoolgrab.h"
+#include "llviewerassetupload.h"
+#include "llviewermenufile.h"
+#include "llagentbenefits.h"
+#include "llfloaterperms.h"
+#include "llstatusbar.h"
+#include "llbvhloader.h"
+#include "llbvhconsts.h"
+#include "lldatapacker.h"
+#include "llkeyframemotion.h"
+#include "llhandmotion.h"
+#include "llviewertexture.h"
+#include "llimage.h"
+#include "llviewercontrol.h"
+#include "llviewermenu.h"
+#include "llviewernetwork.h"
+#include "llviewerobjectlist.h"
+#include "llviewerregion.h"
+#include "llvoavatar.h"
+#include "llvoavatarself.h"
+#include "llvolumemessage.h"
+#include "material_codes.h"
+#include "object_flags.h"
+
+#include "boost/json.hpp"
+
+#include <algorithm>
+#include <chrono>
+#include <map>
+#include <sstream>
+#if !LL_WINDOWS
+#include <fcntl.h>
+#include <sys/stat.h>
+#endif
+
+#include <atomic>
+#include <condition_variable>
+#include <deque>
+#include <mutex>
+#include <thread>
+
+#if LL_WINDOWS
+#include <winsock2.h>
+#include <ws2tcpip.h>
+using mcp_socket_t = SOCKET;
+static const mcp_socket_t MCP_INVALID_SOCKET = INVALID_SOCKET;
+static void mcp_close_socket(mcp_socket_t s) { if (s != INVALID_SOCKET) closesocket(s); }
+#else
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/select.h>
+#include <sys/socket.h>
+#include <unistd.h>
+using mcp_socket_t = int;
+static const mcp_socket_t MCP_INVALID_SOCKET = -1;
+static void mcp_close_socket(mcp_socket_t s) { if (s >= 0) ::close(s); }
+#endif
+
+extern void send_chat_from_viewer(const std::string& utf8_out_text, EChatType type, S32 channel);
+
+namespace
+{
+ constexpr S32 kMaxChat = 100;
+ constexpr S32 kMaxDebug = 200;
+ constexpr S32 kMaxNearby = 100;
+ constexpr F64 kChatThrottleSeconds = 1.0;
+
+ struct ChatLine
+ {
+ std::string kind;
+ std::string from;
+ LLUUID from_id;
+ std::string text;
+ std::string time; // ISO-8601 UTC. Set for debug lines.
+ };
+
+ struct Job
+ {
+ std::function<void(boost::json::object& out, bool& is_error)> fn;
+ boost::json::object result;
+ bool is_error = false;
+ bool done = false;
+ std::mutex mu;
+ std::condition_variable cv;
+ };
+
+ std::mutex gQueueMu;
+ std::deque<std::shared_ptr<Job>> gQueue;
+
+ std::mutex gChatMu;
+ std::deque<ChatLine> gChat;
+ std::deque<ChatLine> gDebug;
+
+ std::atomic<bool> gRun{false};
+ std::atomic<bool> gShutdown{false};
+ std::thread gThread;
+ mcp_socket_t gListen = MCP_INVALID_SOCKET;
+ int gPort = 9411;
+ F64 gLastChatTime = 0.0;
+ bool gStarted = false;
+
+ bool in_world()
+ {
+ return LLStartUp::getStartupState() == STATE_STARTED && isAgentAvatarValid();
+ }
+
+ void require_world(bool& is_error, const std::string& what)
+ {
+ if (!in_world())
+ {
+ is_error = true;
+ throw std::runtime_error(what + " requires the avatar to be in world");
+ }
+ }
+
+ std::string object_label(LLViewerObject* obj)
+ {
+ if (!obj)
+ {
+ return std::string();
+ }
+ if (obj->isAvatar())
+ {
+ return static_cast<LLVOAvatar*>(obj)->getFullname();
+ }
+ LLNameValue* nv = obj->getNVPair("Name");
+ if (nv && nv->getString())
+ {
+ return nv->getString();
+ }
+ return std::string();
+ }
+
+ boost::json::array link_ids(LLViewerObject* obj)
+ {
+ boost::json::array ids;
+ if (!obj || obj->isAvatar())
+ {
+ return ids;
+ }
+ LLViewerObject* root = obj->getRootEdit();
+ if (!root || root->isAvatar())
+ {
+ return ids;
+ }
+ std::vector<LLViewerObject*> links;
+ root->addThisAndNonJointChildren(links);
+ for (LLViewerObject* link : links)
+ {
+ if (link)
+ {
+ ids.push_back(boost::json::string(link->getID().asString()));
+ }
+ }
+ return ids;
+ }
+
+ LLViewerObject* object_by_id(const std::string& id_text, bool& is_error)
+ {
+ LLUUID id(id_text);
+ if (id.isNull())
+ {
+ is_error = true;
+ throw std::runtime_error("object id is not a UUID");
+ }
+ LLViewerObject* obj = gObjectList.findObject(id);
+ if (!obj)
+ {
+ is_error = true;
+ throw std::runtime_error("object is not in the viewer cache");
+ }
+ return obj;
+ }
+
+ const boost::json::object* args_of(const boost::json::object& params)
+ {
+ auto it = params.find("arguments");
+ if (it == params.end() || !it->value().is_object())
+ {
+ return nullptr;
+ }
+ return &it->value().as_object();
+ }
+
+ std::string jstr(const boost::json::object* args, const char* key, const std::string& fallback = std::string())
+ {
+ if (!args)
+ {
+ return fallback;
+ }
+ auto it = args->find(key);
+ if (it == args->end() || !it->value().is_string())
+ {
+ return fallback;
+ }
+ return std::string(it->value().as_string());
+ }
+
+ double jreal(const boost::json::object* args, const char* key, double fallback)
+ {
+ if (!args)
+ {
+ return fallback;
+ }
+ auto it = args->find(key);
+ if (it == args->end())
+ {
+ return fallback;
+ }
+ if (it->value().is_double())
+ {
+ return it->value().as_double();
+ }
+ if (it->value().is_int64())
+ {
+ return static_cast<double>(it->value().as_int64());
+ }
+ if (it->value().is_uint64())
+ {
+ return static_cast<double>(it->value().as_uint64());
+ }
+ return fallback;
+ }
+
+ bool jhas(const boost::json::object* args, const char* key)
+ {
+ return args && args->find(key) != args->end();
+ }
+
+ bool jbool(const boost::json::object* args, const char* key, bool fallback)
+ {
+ if (!args)
+ {
+ return fallback;
+ }
+ auto it = args->find(key);
+ if (it == args->end() || !it->value().is_bool())
+ {
+ return fallback;
+ }
+ return it->value().as_bool();
+ }
+
+ boost::json::object vec3_json(const LLVector3& v)
+ {
+ boost::json::object o;
+ o["x"] = v.mV[VX];
+ o["y"] = v.mV[VY];
+ o["z"] = v.mV[VZ];
+ return o;
+ }
+
+ std::string camera_mode_name()
+ {
+ switch (gAgentCamera.getCameraMode())
+ {
+ case CAMERA_MODE_THIRD_PERSON: return "third_person";
+ case CAMERA_MODE_MOUSELOOK: return "mouselook";
+ case CAMERA_MODE_CUSTOMIZE_AVATAR: return "customize_avatar";
+ case CAMERA_MODE_FOLLOW: return "follow";
+ case CAMERA_MODE_OTS: return "over_shoulder";
+ default: return "unknown";
+ }
+ }
+
+ void tool_status(const boost::json::object*, boost::json::object& out, bool&)
+ {
+ out["in_world"] = in_world();
+ out["startup_state"] = LLStartUp::getStartupStateString();
+ out["agent_id"] = gAgent.getID().asString();
+ std::string name;
+ LLAgentUI::buildFullname(name);
+ if (name.empty())
+ {
+ name = gAgentUsername;
+ }
+ out["agent_name"] = name;
+ LLViewerRegion* region = gAgent.getRegion();
+ if (region)
+ {
+ out["region"] = region->getName();
+ out["position"] = vec3_json(gAgent.getPositionAgent());
+ }
+ out["look"] = vec3_json(gAgent.getAtAxis());
+ out["camera_mode"] = camera_mode_name();
+ }
+
+ void tool_nearby(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "nearby");
+ const F32 radius = (F32)llclamp(jreal(args, "radius", 32.0), 1.0, 256.0);
+ const S32 limit = (S32)llclamp(jreal(args, "limit", 50.0), 1.0, (F64)kMaxNearby);
+ const LLVector3d here = gAgent.getPositionGlobal();
+ boost::json::array items;
+ const S32 count = gObjectList.getNumObjects();
+ for (S32 i = 0; i < count && (S32)items.size() < limit; ++i)
+ {
+ LLViewerObject* obj = gObjectList.getObject(i);
+ if (!obj || obj->isDead())
+ {
+ continue;
+ }
+ const bool avatar = obj->isAvatar();
+ if (!avatar && obj->getParent())
+ {
+ continue;
+ }
+ const F32 distance = (F32)(obj->getPositionGlobal() - here).magVec();
+ if (distance > radius)
+ {
+ continue;
+ }
+ boost::json::object item;
+ item["id"] = obj->getID().asString();
+ item["name"] = object_label(obj);
+ item["kind"] = avatar ? "avatar" : "object";
+ item["distance"] = distance;
+ item["position"] = vec3_json(obj->getPositionRegion());
+ if (!avatar)
+ {
+ boost::json::array links = link_ids(obj);
+ item["linkset"] = links.size() > 1;
+ }
+ items.push_back(std::move(item));
+ }
+ out["world_text_untrusted"] = true;
+ out["items"] = std::move(items);
+ }
+
+ void tool_selection(const boost::json::object*, boost::json::object& out, bool&)
+ {
+ boost::json::array items;
+ LLObjectSelectionHandle selection = LLSelectMgr::getInstance()->getSelection();
+ if (selection)
+ {
+ for (LLObjectSelection::iterator it = selection->begin(); it != selection->end(); ++it)
+ {
+ LLViewerObject* obj = (*it)->getObject();
+ if (!obj)
+ {
+ continue;
+ }
+ boost::json::object item;
+ item["id"] = obj->getID().asString();
+ item["name"] = object_label(obj);
+ boost::json::array links = link_ids(obj);
+ if (links.size() > 1)
+ {
+ item["links"] = std::move(links);
+ }
+ items.push_back(std::move(item));
+ }
+ }
+ out["items"] = std::move(items);
+ }
+
+ void tool_chat_recent(const boost::json::object* args, boost::json::object& out, bool&)
+ {
+ const S32 limit = (S32)llclamp(jreal(args, "limit", 40.0), 1.0, (F64)kMaxChat);
+ boost::json::array lines;
+ std::lock_guard<std::mutex> lock(gChatMu);
+ const S32 start = (S32)gChat.size() > limit ? (S32)gChat.size() - limit : 0;
+ for (S32 i = start; i < (S32)gChat.size(); ++i)
+ {
+ const ChatLine& line = gChat[(size_t)i];
+ boost::json::object item;
+ item["kind"] = line.kind;
+ item["from"] = line.from;
+ item["from_id"] = line.from_id.asString();
+ item["text"] = line.text;
+ lines.push_back(std::move(item));
+ }
+ out["world_text_untrusted"] = true;
+ out["lines"] = std::move(lines);
+ }
+
+ boost::json::object debug_line_json(const ChatLine& line)
+ {
+ boost::json::object item;
+ item["from"] = line.from;
+ item["from_id"] = line.from_id.asString();
+ item["text"] = line.text;
+ item["time"] = line.time;
+ return item;
+ }
+
+ void tool_debug_recent(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ const std::string since_text = jstr(args, "since");
+ LLDate since;
+ const bool have_since = !since_text.empty();
+ if (have_since && !since.fromString(since_text))
+ {
+ is_error = true;
+ throw std::runtime_error("since must be an ISO-8601 UTC timestamp, for example 2026-10-13T00:00:00Z");
+ }
+ const S32 limit = (S32)llclamp(jreal(args, "limit", have_since ? (F64)kMaxDebug : 40.0), 1.0, (F64)kMaxDebug);
+ const LLUUID filter(jstr(args, "id"));
+ std::vector<boost::json::object> matched;
+ {
+ std::lock_guard<std::mutex> lock(gChatMu);
+ if (have_since)
+ {
+ for (const ChatLine& line : gDebug)
+ {
+ if ((S32)matched.size() >= limit)
+ {
+ break;
+ }
+ if (filter.notNull() && line.from_id != filter)
+ {
+ continue;
+ }
+ LLDate line_time;
+ if (!line.time.empty() && line_time.fromString(line.time) && line_time < since)
+ {
+ continue;
+ }
+ matched.push_back(debug_line_json(line));
+ }
+ }
+ else
+ {
+ for (auto it = gDebug.rbegin(); it != gDebug.rend() && (S32)matched.size() < limit; ++it)
+ {
+ if (filter.notNull() && it->from_id != filter)
+ {
+ continue;
+ }
+ matched.push_back(debug_line_json(*it));
+ }
+ }
+ }
+ boost::json::array lines;
+ if (have_since)
+ {
+ for (boost::json::object& item : matched)
+ {
+ lines.push_back(std::move(item));
+ }
+ }
+ else
+ {
+ for (auto it = matched.rbegin(); it != matched.rend(); ++it)
+ {
+ lines.push_back(std::move(*it));
+ }
+ }
+ out["world_text_untrusted"] = true;
+ out["lines"] = std::move(lines);
+ }
+
+ void tool_camera(const boost::json::object*, boost::json::object& out, bool&)
+ {
+ out["mode"] = camera_mode_name();
+ out["mouselook"] = gAgentCamera.cameraMouselook();
+ LLViewerRegion* region = gAgent.getRegion();
+ if (region)
+ {
+ out["position"] = vec3_json(region->getPosRegionFromGlobal(gAgentCamera.getCameraPositionGlobal()));
+ out["focus"] = vec3_json(region->getPosRegionFromGlobal(gAgentCamera.getFocusGlobal()));
+ }
+ }
+
+ void tool_chat(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "chat");
+ const std::string text = jstr(args, "text");
+ if (text.empty())
+ {
+ is_error = true;
+ throw std::runtime_error("chat requires text");
+ }
+ const F64 now = LLTimer::getElapsedSeconds();
+ if (now < gLastChatTime + kChatThrottleSeconds)
+ {
+ is_error = true;
+ throw std::runtime_error("chat is throttled");
+ }
+ gLastChatTime = now;
+ const std::string mode = jstr(args, "mode", "normal");
+ EChatType type = CHAT_TYPE_NORMAL;
+ if (mode == "whisper")
+ {
+ type = CHAT_TYPE_WHISPER;
+ }
+ else if (mode == "shout")
+ {
+ type = CHAT_TYPE_SHOUT;
+ }
+ const S32 channel = (S32)jreal(args, "channel", 0.0);
+ send_chat_from_viewer(text, type, channel);
+ out["sent"] = true;
+ }
+
+ void tool_im(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "im");
+ const std::string text = jstr(args, "text");
+ if (text.empty())
+ {
+ is_error = true;
+ throw std::runtime_error("im requires text");
+ }
+ LLUUID id(jstr(args, "id"));
+ if (id.isNull())
+ {
+ const std::string name = jstr(args, "name");
+ const S32 count = gObjectList.getNumObjects();
+ for (S32 i = 0; i < count && id.isNull(); ++i)
+ {
+ LLViewerObject* obj = gObjectList.getObject(i);
+ if (obj && obj->isAvatar() && object_label(obj) == name)
+ {
+ id = obj->getID();
+ }
+ }
+ }
+ if (id.isNull())
+ {
+ is_error = true;
+ throw std::runtime_error("im needs an avatar id, or a name that is currently in view");
+ }
+ const LLUUID session = LLIMMgr::computeSessionID(IM_NOTHING_SPECIAL, id);
+ LLIMModel::sendMessage(text, session, id, IM_NOTHING_SPECIAL);
+ out["sent"] = true;
+ out["id"] = id.asString();
+ }
+
+ void tool_teleport(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "teleport");
+ const std::string region = jstr(args, "region");
+ if (region.empty())
+ {
+ is_error = true;
+ throw std::runtime_error("teleport requires a region name");
+ }
+ LLSD params(LLSD::emptyArray());
+ params.append(region);
+ params.append(jreal(args, "x", 128.0));
+ params.append(jreal(args, "y", 128.0));
+ params.append(jreal(args, "z", 25.0));
+ const bool ok = LLCommandDispatcher::dispatch(
+ "teleport", params, LLSD(), LLGridManager::getInstance()->getGrid(),
+ NULL, LLCommandHandler::NAV_TYPE_CLICKED, true);
+ out["sent"] = ok;
+ if (!ok)
+ {
+ is_error = true;
+ throw std::runtime_error("teleport was not accepted");
+ }
+ }
+
+ void tool_move(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "move");
+ if (jbool(args, "stop", false))
+ {
+ gAgent.stopAutoPilot(true);
+ out["stopped"] = true;
+ return;
+ }
+ LLViewerRegion* region = gAgent.getRegion();
+ if (!region)
+ {
+ is_error = true;
+ throw std::runtime_error("no current region");
+ }
+ const LLVector3 local((F32)jreal(args, "x", 0.0), (F32)jreal(args, "y", 0.0), (F32)jreal(args, "z", 0.0));
+ gAgent.startAutoPilotGlobal(
+ region->getPosGlobalFromRegion(local),
+ "mcp",
+ NULL,
+ NULL,
+ NULL,
+ (F32)jreal(args, "stop_distance", 1.5),
+ 0.03f,
+ jbool(args, "allow_flying", true));
+ out["started"] = true;
+ }
+
+ void tool_sit(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "sit");
+ LLViewerObject* obj = object_by_id(jstr(args, "id"), is_error);
+ if (!obj->getRegion() || obj->getPCode() != LL_PCODE_VOLUME)
+ {
+ is_error = true;
+ throw std::runtime_error("sit target is not a prim");
+ }
+ gMessageSystem->newMessageFast(_PREHASH_AgentRequestSit);
+ gMessageSystem->nextBlockFast(_PREHASH_AgentData);
+ gMessageSystem->addUUIDFast(_PREHASH_AgentID, gAgent.getID());
+ gMessageSystem->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
+ gMessageSystem->nextBlockFast(_PREHASH_TargetObject);
+ gMessageSystem->addUUIDFast(_PREHASH_TargetID, obj->getID());
+ gMessageSystem->addVector3Fast(_PREHASH_Offset, LLVector3::zero);
+ obj->getRegion()->sendReliableMessage();
+ out["sent"] = true;
+ }
+
+ void tool_stand(const boost::json::object*, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "stand");
+ gAgent.setControlFlags(AGENT_CONTROL_STAND_UP);
+ out["sent"] = true;
+ }
+
+ void tool_touch(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "touch");
+ LLViewerObject* obj = object_by_id(jstr(args, "id"), is_error);
+ LLPickInfo pick;
+ pick.mPickType = LLPickInfo::PICK_OBJECT;
+ pick.mObjectID = obj->getID();
+ pick.mObjectFace = (S32)jreal(args, "face", 0.0);
+ send_ObjectGrab_message(obj, pick, LLVector3::zero);
+ send_ObjectDeGrab_message(obj, pick);
+ out["sent"] = true;
+ }
+
+ void tool_look_at(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "look_at");
+ LLViewerObject* obj = NULL;
+ const std::string id_text = jstr(args, "id");
+ if (!id_text.empty())
+ {
+ obj = object_by_id(id_text, is_error);
+ }
+ const LLVector3 pos((F32)jreal(args, "x", 0.0), (F32)jreal(args, "y", 0.0), (F32)jreal(args, "z", 0.0));
+ gAgentCamera.setLookAt(LOOKAT_TARGET_SELECT, obj, pos);
+ out["sent"] = true;
+ }
+
+ void tool_camera_set(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "camera_set");
+ if (jbool(args, "reset", false))
+ {
+ gAgentCamera.changeCameraToDefault();
+ out["reset"] = true;
+ return;
+ }
+ LLViewerRegion* region = gAgent.getRegion();
+ if (!region)
+ {
+ is_error = true;
+ throw std::runtime_error("no current region");
+ }
+ const LLVector3d cam = region->getPosGlobalFromRegion(LLVector3(
+ (F32)jreal(args, "x", 0.0), (F32)jreal(args, "y", 0.0), (F32)jreal(args, "z", 0.0)));
+ const LLVector3d focus = region->getPosGlobalFromRegion(LLVector3(
+ (F32)jreal(args, "focus_x", 0.0), (F32)jreal(args, "focus_y", 0.0), (F32)jreal(args, "focus_z", 0.0)));
+ gAgentCamera.setCameraPosAndFocusGlobal(cam, focus, LLUUID::null);
+ out["sent"] = true;
+ }
+
+ void tool_select(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "select");
+ LLViewerObject* obj = object_by_id(jstr(args, "id"), is_error);
+ LLSelectMgr::getInstance()->deselectAll();
+ LLSelectMgr::getInstance()->selectObjectAndFamily(obj);
+ out["selected"] = obj->getID().asString();
+ }
+
+ struct AsyncJob
+ {
+ bool done = false;
+ bool ok = false;
+ std::string detail;
+ std::string item_id;
+ std::string source;
+ };
+
+ std::mutex gAsyncMu;
+ std::map<std::string, AsyncJob> gAsync;
+
+ void finish_async(const std::string& job, bool ok, const std::string& detail, const std::string& item_id, const std::string& source = std::string())
+ {
+ std::lock_guard<std::mutex> lock(gAsyncMu);
+ AsyncJob& slot = gAsync[job];
+ slot.done = true;
+ slot.ok = ok;
+ slot.detail = detail;
+ slot.item_id = item_id;
+ slot.source = source;
+ if (gAsync.size() > 32)
+ {
+ auto oldest = gAsync.begin();
+ if (oldest->first == job)
+ {
+ ++oldest;
+ }
+ if (oldest != gAsync.end())
+ {
+ gAsync.erase(oldest);
+ }
+ }
+ }
+
+ LLViewerInventoryItem* inventory_item(const std::string& name_or_id, bool& is_error)
+ {
+ LLUUID id(name_or_id);
+ if (id.notNull())
+ {
+ LLViewerInventoryItem* item = gInventory.getItem(id);
+ if (!item)
+ {
+ is_error = true;
+ throw std::runtime_error("inventory item was not found");
+ }
+ return item;
+ }
+ if (name_or_id.empty())
+ {
+ is_error = true;
+ throw std::runtime_error("inventory item name or id is required");
+ }
+ LLInventoryModel::cat_array_t cats;
+ LLInventoryModel::item_array_t items;
+ gInventory.collectDescendents(gInventory.getRootFolderID(), cats, items, LLInventoryModel::EXCLUDE_TRASH);
+ LLViewerInventoryItem* found = nullptr;
+ S32 matches = 0;
+ for (LLViewerInventoryItem* item : items)
+ {
+ if (item && item->getName() == name_or_id)
+ {
+ found = item;
+ ++matches;
+ }
+ }
+ if (matches != 1)
+ {
+ is_error = true;
+ throw std::runtime_error(matches == 0 ? "inventory item was not found" : "inventory name matches more than one item; pass an id");
+ }
+ return found;
+ }
+
+ LLPCode shape_pcode(const std::string& shape, bool& is_error)
+ {
+ if (shape == "box" || shape == "cube") return LL_PCODE_CUBE;
+ if (shape == "sphere") return LL_PCODE_SPHERE;
+ if (shape == "cylinder") return LL_PCODE_CYLINDER;
+ if (shape == "prism") return LL_PCODE_PRISM;
+ if (shape == "cone") return LL_PCODE_CONE;
+ if (shape == "torus") return LL_PCODE_TORUS;
+ if (shape == "pyramid") return LL_PCODE_PYRAMID;
+ is_error = true;
+ throw std::runtime_error("shape must be box, sphere, cylinder, prism, cone, torus, or pyramid");
+ }
+
+ void pack_new_prim(LLPCode pcode, LLVolumeParams& volume_params, LLQuaternion& rotation, LLPCode& volume_pcode)
+ {
+ rotation.loadIdentity();
+ volume_pcode = LL_PCODE_VOLUME;
+ switch (pcode)
+ {
+ case LL_PCODE_SPHERE:
+ rotation.setQuat(90.f * DEG_TO_RAD, LLVector3::y_axis);
+ volume_params.setType(LL_PCODE_PROFILE_CIRCLE_HALF, LL_PCODE_PATH_CIRCLE);
+ volume_params.setRatio(1, 1);
+ break;
+ case LL_PCODE_TORUS:
+ rotation.setQuat(90.f * DEG_TO_RAD, LLVector3::y_axis);
+ volume_params.setType(LL_PCODE_PROFILE_CIRCLE, LL_PCODE_PATH_CIRCLE);
+ volume_params.setRatio(1.f, 0.25f);
+ break;
+ case LL_PCODE_PRISM:
+ volume_params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE);
+ volume_params.setRatio(0, 1);
+ volume_params.setShear(-0.5f, 0);
+ break;
+ case LL_PCODE_PYRAMID:
+ volume_params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE);
+ volume_params.setRatio(0, 0);
+ break;
+ case LL_PCODE_CYLINDER:
+ volume_params.setType(LL_PCODE_PROFILE_CIRCLE, LL_PCODE_PATH_LINE);
+ volume_params.setRatio(1, 1);
+ break;
+ case LL_PCODE_CONE:
+ volume_params.setType(LL_PCODE_PROFILE_CIRCLE, LL_PCODE_PATH_LINE);
+ volume_params.setRatio(0, 0);
+ break;
+ case LL_PCODE_CUBE:
+ default:
+ volume_params.setType(LL_PCODE_PROFILE_SQUARE, LL_PCODE_PATH_LINE);
+ volume_params.setRatio(1, 1);
+ break;
+ }
+ volume_params.setBeginAndEndS(0.f, 1.f);
+ volume_params.setBeginAndEndT(0.f, 1.f);
+ }
+
+ void tool_rez_prim(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "rez_prim");
+ LLViewerRegion* region = gAgent.getRegion();
+ if (!region)
+ {
+ is_error = true;
+ throw std::runtime_error("no current region");
+ }
+ const LLPCode pcode = shape_pcode(jstr(args, "shape", "box"), is_error);
+ const F32 size = (F32)llclamp(jreal(args, "size", 0.5), 0.01, 64.0);
+ LLVector3 scale(size, size, size);
+ if (jhas(args, "scale_x"))
+ {
+ scale.set((F32)jreal(args, "scale_x", size), (F32)jreal(args, "scale_y", size), (F32)jreal(args, "scale_z", size));
+ }
+ const LLVector3 pos((F32)jreal(args, "x", 128.0), (F32)jreal(args, "y", 128.0), (F32)jreal(args, "z", 25.0));
+ LLVolumeParams volume_params;
+ LLQuaternion rotation;
+ LLPCode volume_pcode = LL_PCODE_VOLUME;
+ pack_new_prim(pcode, volume_params, rotation, volume_pcode);
+
+ U32 flags = FLAGS_CREATE_SELECTED;
+ if (jbool(args, "physics", false))
+ {
+ flags |= FLAGS_USE_PHYSICS;
+ }
+ gMessageSystem->newMessageFast(_PREHASH_ObjectAdd);
+ gMessageSystem->nextBlockFast(_PREHASH_AgentData);
+ gMessageSystem->addUUIDFast(_PREHASH_AgentID, gAgent.getID());
+ gMessageSystem->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
+ gMessageSystem->addUUIDFast(_PREHASH_GroupID, gAgent.getGroupID());
+ gMessageSystem->nextBlockFast(_PREHASH_ObjectData);
+ gMessageSystem->addU8Fast(_PREHASH_Material, LL_MCODE_WOOD);
+ gMessageSystem->addU32Fast(_PREHASH_AddFlags, flags);
+ LLVolumeMessage::packVolumeParams(&volume_params, gMessageSystem);
+ gMessageSystem->addU8Fast(_PREHASH_PCode, volume_pcode);
+ gMessageSystem->addVector3Fast(_PREHASH_Scale, scale);
+ gMessageSystem->addQuatFast(_PREHASH_Rotation, rotation);
+ gMessageSystem->addVector3Fast(_PREHASH_RayStart, pos);
+ gMessageSystem->addVector3Fast(_PREHASH_RayEnd, pos);
+ gMessageSystem->addU8Fast(_PREHASH_BypassRaycast, (U8)1);
+ gMessageSystem->addU8Fast(_PREHASH_RayEndIsIntersection, (U8)0);
+ gMessageSystem->addU8Fast(_PREHASH_State, (U8)0);
+ gMessageSystem->addUUIDFast(_PREHASH_RayTargetID, LLUUID::null);
+ gMessageSystem->sendReliable(region->getHost());
+ LLSelectMgr::getInstance()->deselectAll();
+ if (gViewerWindow && gViewerWindow->getWindow())
+ {
+ gViewerWindow->getWindow()->incBusyCount();
+ }
+ out["sent"] = true;
+ out["note"] = "The new prim is selected when the region sends it back. Call selection to read its UUID.";
+ }
+
+ void tool_rez_item(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "rez_item");
+ LLViewerRegion* region = gAgent.getRegion();
+ if (!region)
+ {
+ is_error = true;
+ throw std::runtime_error("no current region");
+ }
+ LLViewerInventoryItem* item = inventory_item(jstr(args, "id").empty() ? jstr(args, "name") : jstr(args, "id"), is_error);
+ if (item->getType() != LLAssetType::AT_OBJECT)
+ {
+ is_error = true;
+ throw std::runtime_error("inventory item is not an object");
+ }
+ const LLVector3 pos((F32)jreal(args, "x", 128.0), (F32)jreal(args, "y", 128.0), (F32)jreal(args, "z", 25.0));
+ LLMessageSystem* msg = gMessageSystem;
+ msg->newMessageFast(_PREHASH_RezObject);
+ msg->nextBlockFast(_PREHASH_AgentData);
+ msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID());
+ msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
+ msg->addUUIDFast(_PREHASH_GroupID, gAgent.getGroupID());
+ msg->nextBlock("RezData");
+ msg->addUUIDFast(_PREHASH_FromTaskID, LLUUID::null);
+ msg->addU8Fast(_PREHASH_BypassRaycast, (U8)1);
+ msg->addVector3Fast(_PREHASH_RayStart, pos);
+ msg->addVector3Fast(_PREHASH_RayEnd, pos);
+ msg->addUUIDFast(_PREHASH_RayTargetID, LLUUID::null);
+ msg->addBOOLFast(_PREHASH_RayEndIsIntersection, false);
+ msg->addBOOLFast(_PREHASH_RezSelected, true);
+ msg->addBOOLFast(_PREHASH_RemoveItem, false);
+ pack_permissions_slam(msg, item->getFlags(), item->getPermissions());
+ msg->nextBlockFast(_PREHASH_InventoryData);
+ item->packMessage(msg);
+ msg->sendReliable(region->getHost());
+ out["sent"] = true;
+ out["item_id"] = item->getUUID().asString();
+ }
+
+ void derez_object(LLViewerObject* object, bool copy);
+
+ void tool_take(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "take");
+ LLViewerObject* obj = object_by_id(jstr(args, "id"), is_error);
+ const bool copy = jbool(args, "copy", false);
+ LLViewerObject* root = obj->getRootEdit() ? obj->getRootEdit() : obj;
+ if (!root->permYouOwner())
+ {
+ is_error = true;
+ throw std::runtime_error("you do not own this object");
+ }
+ if (copy && !root->permCopy())
+ {
+ is_error = true;
+ throw std::runtime_error("you cannot copy this object");
+ }
+ if (!root->getRegion() || root->getLocalID() == 0)
+ {
+ is_error = true;
+ throw std::runtime_error("object is not ready to take");
+ }
+ if (gInventory.findCategoryUUIDForType(LLFolderType::FT_OBJECT).isNull())
+ {
+ is_error = true;
+ throw std::runtime_error("objects inventory folder was not found");
+ }
+ derez_object(root, copy);
+ out["sent"] = true;
+ out["id"] = root->getID().asString();
+ }
+
+ LLViewerJointAttachment* attachment_point(const std::string& name)
+ {
+ if (!isAgentAvatarValid() || name.empty())
+ {
+ return nullptr;
+ }
+ for (auto& entry : gAgentAvatarp->mAttachmentPoints)
+ {
+ if (entry.second && LLStringUtil::compareInsensitive(entry.second->getName(), name) == 0)
+ {
+ return entry.second;
+ }
+ }
+ return nullptr;
+ }
+
+ void tool_attach_points(const boost::json::object*, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "attach_points");
+ boost::json::array points;
+ if (isAgentAvatarValid())
+ {
+ for (auto& entry : gAgentAvatarp->mAttachmentPoints)
+ {
+ if (!entry.second)
+ {
+ continue;
+ }
+ boost::json::object item;
+ item["name"] = entry.second->getName();
+ item["attached"] = entry.second->getNumObjects();
+ points.push_back(std::move(item));
+ }
+ }
+ out["points"] = std::move(points);
+ }
+
+ void tool_attach(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "attach");
+ LLViewerInventoryItem* item = inventory_item(jstr(args, "id").empty() ? jstr(args, "name") : jstr(args, "id"), is_error);
+ const std::string point_name = jstr(args, "point");
+ LLViewerJointAttachment* point = attachment_point(point_name);
+ if (!point_name.empty() && !point)
+ {
+ is_error = true;
+ throw std::runtime_error("unknown attachment point");
+ }
+ rez_attachment(item, point, false);
+ out["sent"] = true;
+ out["item_id"] = item->getUUID().asString();
+ }
+
+ void tool_detach(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "detach");
+ LLViewerObject* obj = object_by_id(jstr(args, "id"), is_error);
+ const LLUUID item_id = obj->getAttachmentItemID();
+ if (item_id.isNull())
+ {
+ is_error = true;
+ throw std::runtime_error("object is not an attachment");
+ }
+ LLAppearanceMgr::instance().removeItemFromAvatar(item_id);
+ out["sent"] = true;
+ out["item_id"] = item_id.asString();
+ }
+
+ U8 material_code(const std::string& name, bool& is_error)
+ {
+ if (name == "stone") return LL_MCODE_STONE;
+ if (name == "metal") return LL_MCODE_METAL;
+ if (name == "glass") return LL_MCODE_GLASS;
+ if (name == "wood") return LL_MCODE_WOOD;
+ if (name == "flesh") return LL_MCODE_FLESH;
+ if (name == "plastic") return LL_MCODE_PLASTIC;
+ if (name == "rubber") return LL_MCODE_RUBBER;
+ if (name == "light") return LL_MCODE_LIGHT;
+ is_error = true;
+ throw std::runtime_error("material must be stone, metal, glass, wood, flesh, plastic, rubber, or light");
+ }
+
+ F32 unit_or_byte(const boost::json::object* args, const char* key, F32 current)
+ {
+ if (!jhas(args, key))
+ {
+ return current;
+ }
+ F32 value = (F32)jreal(args, key, current);
+ if (value > 1.f)
+ {
+ value /= 255.f;
+ }
+ return llclamp(value, 0.f, 1.f);
+ }
+
+ void send_transform_update(LLViewerObject* object, U8 type)
+ {
+ if (!object || !object->getRegion() || object->getLocalID() == 0 || type == UPD_NONE)
+ {
+ return;
+ }
+ if (object->isRootEdit())
+ {
+ type |= UPD_LINKED_SETS;
+ }
+ U8 data[64];
+ S32 offset = 0;
+ if (type & UPD_POSITION)
+ {
+ htolememcpy(&data[offset], &(object->getPosition().mV), MVT_LLVector3, 12);
+ offset += 12;
+ }
+ if (type & UPD_ROTATION)
+ {
+ LLVector3 packed = object->getRotation().packToVector3();
+ htolememcpy(&data[offset], &(packed.mV), MVT_LLQuaternion, 12);
+ offset += 12;
+ }
+ if (type & UPD_SCALE)
+ {
+ htolememcpy(&data[offset], &(object->getScale().mV), MVT_LLVector3, 12);
+ offset += 12;
+ }
+ gMessageSystem->newMessage("MultipleObjectUpdate");
+ gMessageSystem->nextBlockFast(_PREHASH_AgentData);
+ gMessageSystem->addUUIDFast(_PREHASH_AgentID, gAgent.getID());
+ gMessageSystem->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
+ gMessageSystem->nextBlockFast(_PREHASH_ObjectData);
+ gMessageSystem->addU32Fast(_PREHASH_ObjectLocalID, object->getLocalID());
+ gMessageSystem->addU8Fast(_PREHASH_Type, type);
+ gMessageSystem->addBinaryDataFast(_PREHASH_Data, data, offset);
+ gMessageSystem->sendReliable(object->getRegion()->getHost());
+ }
+
+ void send_name(LLViewerObject* object, const std::string& name)
+ {
+ if (name.empty() || !object->getRegion())
+ {
+ return;
+ }
+ gMessageSystem->newMessage("ObjectName");
+ gMessageSystem->nextBlockFast(_PREHASH_AgentData);
+ gMessageSystem->addUUIDFast(_PREHASH_AgentID, gAgent.getID());
+ gMessageSystem->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
+ gMessageSystem->nextBlockFast(_PREHASH_ObjectData);
+ gMessageSystem->addU32Fast(_PREHASH_LocalID, object->getLocalID());
+ gMessageSystem->addStringFast(_PREHASH_Name, name);
+ gMessageSystem->sendReliable(object->getRegion()->getHost());
+ }
+
+ void send_description(LLViewerObject* object, const std::string& description)
+ {
+ if (!object->getRegion())
+ {
+ return;
+ }
+ gMessageSystem->newMessage("ObjectDescription");
+ gMessageSystem->nextBlockFast(_PREHASH_AgentData);
+ gMessageSystem->addUUIDFast(_PREHASH_AgentID, gAgent.getID());
+ gMessageSystem->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
+ gMessageSystem->nextBlockFast(_PREHASH_ObjectData);
+ gMessageSystem->addU32Fast(_PREHASH_LocalID, object->getLocalID());
+ gMessageSystem->addStringFast(_PREHASH_Description, description);
+ gMessageSystem->sendReliable(object->getRegion()->getHost());
+ }
+
+ void derez_object(LLViewerObject* object, bool copy)
+ {
+ LLViewerObject* root = object->getRootEdit() ? object->getRootEdit() : object;
+ LLViewerRegion* region = root->getRegion();
+ const LLUUID folder_id = gInventory.findCategoryUUIDForType(LLFolderType::FT_OBJECT);
+ LLUUID transaction_id;
+ transaction_id.generate();
+ gMessageSystem->newMessageFast(_PREHASH_DeRezObject);
+ gMessageSystem->nextBlockFast(_PREHASH_AgentData);
+ gMessageSystem->addUUIDFast(_PREHASH_AgentID, gAgent.getID());
+ gMessageSystem->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID());
+ gMessageSystem->nextBlockFast(_PREHASH_AgentBlock);
+ gMessageSystem->addUUIDFast(_PREHASH_GroupID, gAgent.getGroupID());
+ gMessageSystem->addU8Fast(_PREHASH_Destination, copy ? (U8)DRD_ACQUIRE_TO_AGENT_INVENTORY : (U8)DRD_TAKE_INTO_AGENT_INVENTORY);
+ gMessageSystem->addUUIDFast(_PREHASH_DestinationID, folder_id);
+ gMessageSystem->addUUIDFast(_PREHASH_TransactionID, transaction_id);
+ gMessageSystem->addU8Fast(_PREHASH_PacketCount, (U8)1);
+ gMessageSystem->addU8Fast(_PREHASH_PacketNumber, (U8)0);
+ gMessageSystem->nextBlockFast(_PREHASH_ObjectData);
+ gMessageSystem->addU32Fast(_PREHASH_ObjectLocalID, root->getLocalID());
+ gMessageSystem->sendReliable(region->getHost());
+ if (gViewerWindow && gViewerWindow->getWindow())
+ {
+ gViewerWindow->getWindow()->incBusyCount();
+ }
+ }
+
+ void apply_shape(LLViewerObject* obj, const boost::json::object* args)
+ {
+ if (!obj->getVolume())
+ {
+ return;
+ }
+ const bool shape = jhas(args, "hollow") || jhas(args, "twist") || jhas(args, "twist_begin")
+ || jhas(args, "taper_x") || jhas(args, "taper_y") || jhas(args, "shear_x") || jhas(args, "shear_y")
+ || jhas(args, "cut_begin") || jhas(args, "cut_end") || jhas(args, "path_begin") || jhas(args, "path_end")
+ || jhas(args, "hole") || jhas(args, "skew") || jhas(args, "revolutions");
+ if (!shape)
+ {
+ return;
+ }
+ LLVolumeParams params = obj->getVolume()->getParams();
+ if (jhas(args, "hollow"))
+ {
+ F32 hollow = (F32)jreal(args, "hollow", 0.0);
+ if (hollow > 1.f)
+ {
+ hollow /= 100.f;
+ }
+ params.setHollow(llclamp(hollow, 0.f, 0.95f));
+ }
+ if (jhas(args, "twist") || jhas(args, "twist_begin"))
+ {
+ const U8 path = params.getPathParams().getCurveType();
+ const F32 limit = (path == LL_PCODE_PATH_LINE || path == LL_PCODE_PATH_FLEXIBLE)
+ ? OBJECT_TWIST_LINEAR_MAX : OBJECT_TWIST_MAX;
+ F32 begin = params.getPathParams().getTwistBegin() * limit;
+ F32 end = params.getPathParams().getTwistEnd() * limit;
+ if (jhas(args, "twist_begin"))
+ {
+ begin = (F32)jreal(args, "twist_begin", begin);
+ }
+ if (jhas(args, "twist"))
+ {
+ end = (F32)jreal(args, "twist", end);
+ }
+ params.setTwistBegin(begin / limit);
+ params.setTwist(end / limit);
+ }
+ if (jhas(args, "taper_x") || jhas(args, "taper_y"))
+ {
+ params.setTaper(
+ llclamp((F32)jreal(args, "taper_x", params.getPathParams().getTaperX()), -1.f, 1.f),
+ llclamp((F32)jreal(args, "taper_y", params.getPathParams().getTaperY()), -1.f, 1.f));
+ }
+ if (jhas(args, "shear_x") || jhas(args, "shear_y"))
+ {
+ params.setShear(
+ (F32)jreal(args, "shear_x", params.getPathParams().getShearX()),
+ (F32)jreal(args, "shear_y", params.getPathParams().getShearY()));
+ }
+ if (jhas(args, "cut_begin") || jhas(args, "cut_end"))
+ {
+ F32 begin = jhas(args, "cut_begin") ? (F32)jreal(args, "cut_begin", 0.0) : params.getProfileParams().getBegin();
+ F32 end = jhas(args, "cut_end") ? (F32)jreal(args, "cut_end", 1.0) : params.getProfileParams().getEnd();
+ params.setBeginAndEndS(llclamp(begin, 0.f, 1.f), llclamp(end, 0.f, 1.f));
+ }
+ if (jhas(args, "path_begin") || jhas(args, "path_end"))
+ {
+ F32 begin = jhas(args, "path_begin") ? (F32)jreal(args, "path_begin", 0.0) : params.getPathParams().getBegin();
+ F32 end = jhas(args, "path_end") ? (F32)jreal(args, "path_end", 1.0) : params.getPathParams().getEnd();
+ params.setBeginAndEndT(llclamp(begin, 0.f, 1.f), llclamp(end, 0.f, 1.f));
+ }
+ if (jhas(args, "skew"))
+ {
+ params.setSkew((F32)jreal(args, "skew", 0.0));
+ }
+ if (jhas(args, "revolutions"))
+ {
+ params.setRevolutions((F32)jreal(args, "revolutions", 1.0));
+ }
+ if (jhas(args, "hole"))
+ {
+ const std::string hole = jstr(args, "hole", "same");
+ U8 hole_code = LL_PCODE_HOLE_SAME;
+ if (hole == "circle") hole_code = LL_PCODE_HOLE_CIRCLE;
+ else if (hole == "square") hole_code = LL_PCODE_HOLE_SQUARE;
+ else if (hole == "triangle") hole_code = LL_PCODE_HOLE_TRIANGLE;
+ const U8 profile = params.getProfileParams().getCurveType() & ~LL_PCODE_HOLE_MASK;
+ params.setType(profile | hole_code, params.getPathParams().getCurveType());
+ }
+ obj->updateVolume(params);
+ }
+
+ void tool_edit(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "edit");
+ LLViewerObject* obj = object_by_id(jstr(args, "id"), is_error);
+ if (!obj->permModify())
+ {
+ is_error = true;
+ throw std::runtime_error("you cannot modify this object");
+ }
+ LLSelectMgr::getInstance()->deselectAll();
+ LLSelectMgr::getInstance()->selectObjectOnly(obj);
+ U8 updates = 0;
+ if (jhas(args, "x") && jhas(args, "y") && jhas(args, "z"))
+ {
+ const LLVector3 pos((F32)jreal(args, "x", 0), (F32)jreal(args, "y", 0), (F32)jreal(args, "z", 0));
+ LLViewerObject* root = obj->getRootEdit();
+ if (root && root != obj && !root->isAvatar())
+ {
+ LLVector3 local = pos - root->getPositionRegion();
+ local = local * ~root->getRotationRegion();
+ obj->setPositionParent(local);
+ }
+ else
+ {
+ obj->setPositionEdit(pos);
+ }
+ updates |= UPD_POSITION;
+ }
+ if (jhas(args, "scale_x") || jhas(args, "size"))
+ {
+ const F32 size = (F32)jreal(args, "size", obj->getScale().mV[VX]);
+ obj->setScale(LLVector3(
+ (F32)jreal(args, "scale_x", size),
+ (F32)jreal(args, "scale_y", size),
+ (F32)jreal(args, "scale_z", size)), false);
+ updates |= UPD_SCALE;
+ }
+ if (jhas(args, "rot_x") || jhas(args, "rot_y") || jhas(args, "rot_z"))
+ {
+ LLQuaternion rotation;
+ rotation.setQuat(
+ (F32)jreal(args, "rot_x", 0) * DEG_TO_RAD,
+ (F32)jreal(args, "rot_y", 0) * DEG_TO_RAD,
+ (F32)jreal(args, "rot_z", 0) * DEG_TO_RAD);
+ obj->setRotation(rotation);
+ updates |= UPD_ROTATION | UPD_POSITION;
+ }
+ if (updates)
+ {
+ // Selection updates skip individually selected prims, so the
+ // simulator never saw the new position or scale.
+ send_transform_update(obj, updates);
+ }
+ if (jhas(args, "name"))
+ {
+ send_name(obj, jstr(args, "name"));
+ }
+ if (jhas(args, "description"))
+ {
+ send_description(obj, jstr(args, "description"));
+ }
+ if (jhas(args, "material"))
+ {
+ const U8 material = material_code(jstr(args, "material"), is_error);
+ obj->setMaterial((obj->getMaterial() & ~LL_MCODE_MASK) | material);
+ obj->sendMaterialUpdate();
+ }
+ LLViewerObject* root = obj->getRootEdit() ? obj->getRootEdit() : obj;
+ if (jhas(args, "physics"))
+ {
+ root->setFlags(FLAGS_USE_PHYSICS, jbool(args, "physics", false));
+ }
+ if (jhas(args, "phantom"))
+ {
+ root->setFlags(FLAGS_PHANTOM, jbool(args, "phantom", false));
+ }
+ if (jhas(args, "temporary"))
+ {
+ root->setFlags(FLAGS_TEMPORARY_ON_REZ, jbool(args, "temporary", false));
+ }
+ apply_shape(obj, args);
+
+ const S32 face = jhas(args, "face") ? (S32)jreal(args, "face", -1.0) : -1;
+ const bool one_face = face >= 0 && face < obj->getNumTEs() && obj->getTE(face);
+ LLColor4 color = one_face ? obj->getTE(face)->getColor() : (obj->getTE(0) ? obj->getTE(0)->getColor() : LLColor4::white);
+ const bool rgb = jhas(args, "r") || jhas(args, "g") || jhas(args, "b");
+ if (rgb)
+ {
+ color.mV[VRED] = unit_or_byte(args, "r", color.mV[VRED]);
+ color.mV[VGREEN] = unit_or_byte(args, "g", color.mV[VGREEN]);
+ color.mV[VBLUE] = unit_or_byte(args, "b", color.mV[VBLUE]);
+ }
+ if (jhas(args, "alpha"))
+ {
+ color.mV[VALPHA] = llclamp((F32)jreal(args, "alpha", color.mV[VALPHA]), 0.f, 1.f);
+ }
+ if (jhas(args, "transparency"))
+ {
+ color.mV[VALPHA] = 1.f - llclamp((F32)jreal(args, "transparency", 0.0), 0.f, 1.f);
+ }
+ const bool surface = rgb || jhas(args, "alpha") || jhas(args, "transparency") || jhas(args, "glow")
+ || jhas(args, "fullbright") || jhas(args, "shiny") || jhas(args, "texture");
+ if (surface)
+ {
+ const LLUUID texture(jstr(args, "texture"));
+ if (jhas(args, "texture") && texture.isNull())
+ {
+ is_error = true;
+ throw std::runtime_error("texture must be a UUID");
+ }
+ const F32 glow = llclamp((F32)jreal(args, "glow", 0.0), 0.f, 1.f);
+ const U8 bright = jbool(args, "fullbright", false) ? 1 : 0;
+ const std::string shiny = jstr(args, "shiny", "none");
+ U8 shiny_code = 0;
+ if (shiny == "low") shiny_code = 1;
+ else if (shiny == "medium") shiny_code = 2;
+ else if (shiny == "high") shiny_code = 3;
+ const S32 first = one_face ? face : 0;
+ const S32 last = one_face ? face : obj->getNumTEs() - 1;
+ for (S32 te = first; te <= last; ++te)
+ {
+ if (!obj->getTE(te))
+ {
+ continue;
+ }
+ if (rgb || jhas(args, "alpha") || jhas(args, "transparency"))
+ {
+ obj->setTEColor(te, color);
+ }
+ if (jhas(args, "glow"))
+ {
+ obj->setTEGlow(te, glow);
+ }
+ if (jhas(args, "fullbright"))
+ {
+ obj->getTE(te)->setFullbright(bright);
+ }
+ if (jhas(args, "shiny"))
+ {
+ obj->getTE(te)->setShiny(shiny_code);
+ }
+ if (jhas(args, "texture"))
+ {
+ obj->setTETexture(te, texture);
+ }
+ }
+ obj->sendTEUpdate();
+ }
+ out["sent"] = true;
+ out["id"] = obj->getID().asString();
+ }
+
+ struct PendingLink
+ {
+ std::string job;
+ std::vector<LLUUID> ids;
+ F64 started = 0.0;
+ };
+
+ std::vector<PendingLink> gPendingLinks;
+
+ std::vector<LLUUID> link_ids_from_args(const boost::json::object* args, bool& is_error)
+ {
+ std::vector<LLUUID> ids;
+ if (!args)
+ {
+ is_error = true;
+ throw std::runtime_error("link requires ids");
+ }
+ auto it = args->find("ids");
+ if (it == args->end() || !it->value().is_array() || it->value().as_array().size() < 2)
+ {
+ is_error = true;
+ throw std::runtime_error("link requires at least two ids; the last id becomes the root");
+ }
+ for (const boost::json::value& value : it->value().as_array())
+ {
+ if (!value.is_string())
+ {
+ continue;
+ }
+ LLViewerObject* obj = object_by_id(std::string(value.as_string()), is_error);
+ ids.push_back(obj->getID());
+ }
+ if (ids.size() < 2)
+ {
+ is_error = true;
+ throw std::runtime_error("link requires at least two ids; the last id becomes the root");
+ }
+ return ids;
+ }
+
+ bool select_link_targets(const std::vector<LLUUID>& ids, std::string& error)
+ {
+ LLSelectMgr::getInstance()->deselectAll();
+ for (const LLUUID& id : ids)
+ {
+ LLViewerObject* obj = gObjectList.findObject(id);
+ if (!obj)
+ {
+ error = "an object left the viewer cache before it could be linked";
+ return false;
+ }
+ LLSelectMgr::getInstance()->selectObjectAndFamily(obj, true);
+ if (!obj->isSelected())
+ {
+ error = "could not select " + id.asString();
+ return false;
+ }
+ }
+ if (LLSelectMgr::getInstance()->getSelection()->getRootObjectCount() < 2)
+ {
+ error = "need at least two separate objects";
+ return false;
+ }
+ return true;
+ }
+
+ // Empty when the link can be sent. Non-empty is a real failure.
+ // Properties still downloading is not a failure; the caller waits.
+ std::string link_block_reason()
+ {
+ LLSelectMgr* sel = LLSelectMgr::getInstance();
+ if (!sel->selectGetRootsModify())
+ {
+ return "you cannot modify every object";
+ }
+ if (!sel->selectGetRootsNonPermanentEnforced())
+ {
+ return "a selected object is permanent";
+ }
+ LLUUID owner_id;
+ std::string owner_name;
+ if (!sel->selectGetOwner(owner_id, owner_name))
+ {
+ return "the objects have different owners";
+ }
+ if (!sel->selectGetSameRegion())
+ {
+ return "the objects are in different regions";
+ }
+ return std::string();
+ }
+
+ bool link_targets_selected(const std::vector<LLUUID>& ids)
+ {
+ for (const LLUUID& id : ids)
+ {
+ LLViewerObject* obj = gObjectList.findObject(id);
+ if (!obj || !obj->isSelected())
+ {
+ return false;
+ }
+ }
+ return LLSelectMgr::getInstance()->getSelection()->getRootObjectCount() >= 2;
+ }
+
+ void finish_pending_links()
+ {
+ const F64 now = LLTimer::getElapsedSeconds();
+ for (auto it = gPendingLinks.begin(); it != gPendingLinks.end(); )
+ {
+ std::string error;
+ // Selecting again clears the property-valid flag, so leave a
+ // selection alone while its object properties are downloading.
+ if (!link_targets_selected(it->ids) && !select_link_targets(it->ids, error))
+ {
+ finish_async(it->job, false, error, std::string());
+ it = gPendingLinks.erase(it);
+ continue;
+ }
+ if (!LLSelectMgr::getInstance()->selectGetAllRootsValid())
+ {
+ if (now - it->started > 8.0)
+ {
+ finish_async(it->job, false, "object properties did not load", std::string());
+ it = gPendingLinks.erase(it);
+ }
+ else
+ {
+ ++it;
+ }
+ continue;
+ }
+ error = link_block_reason();
+ if (!error.empty())
+ {
+ finish_async(it->job, false, error, std::string());
+ it = gPendingLinks.erase(it);
+ continue;
+ }
+ // linkObjects() returns true even when it refuses to link.
+ LLSelectMgr::getInstance()->sendLink();
+ finish_async(it->job, true, "linked", std::string());
+ it = gPendingLinks.erase(it);
+ }
+ }
+
+ void tool_link(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "link");
+ const std::vector<LLUUID> ids = link_ids_from_args(args, is_error);
+ std::string error;
+ if (!select_link_targets(ids, error))
+ {
+ is_error = true;
+ throw std::runtime_error(error);
+ }
+ if (LLSelectMgr::getInstance()->selectGetAllRootsValid())
+ {
+ error = link_block_reason();
+ if (!error.empty())
+ {
+ is_error = true;
+ throw std::runtime_error(error);
+ }
+ LLSelectMgr::getInstance()->sendLink();
+ out["sent"] = true;
+ out["pending"] = false;
+ return;
+ }
+ // ObjectSelect was just sent. Properties arrive on a later frame.
+ // Linking now hits UnableToLinkWhileDownloading and still returns success.
+ const std::string job = LLUUID::generateNewID().asString();
+ {
+ std::lock_guard<std::mutex> lock(gAsyncMu);
+ gAsync[job] = AsyncJob();
+ }
+ gPendingLinks.push_back(PendingLink{job, ids, LLTimer::getElapsedSeconds()});
+ out["job"] = job;
+ out["pending"] = true;
+ out["sent"] = false;
+ }
+
+ void tool_unlink(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "unlink");
+ LLViewerObject* obj = object_by_id(jstr(args, "id"), is_error);
+ LLSelectMgr::getInstance()->deselectAll();
+ LLSelectMgr::getInstance()->selectObjectAndFamily(obj->getRootEdit() ? obj->getRootEdit() : obj);
+ const bool ok = LLSelectMgr::getInstance()->unlinkObjects();
+ out["sent"] = ok;
+ }
+
+ void tool_contents(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "contents");
+ LLViewerObject* obj = object_by_id(jstr(args, "id"), is_error);
+ LLInventoryObject::object_list_t objects;
+ obj->getInventoryContents(objects);
+ if (objects.empty())
+ {
+ if (!obj->isInventoryPending())
+ {
+ obj->requestInventory();
+ }
+ out["pending"] = true;
+ out["items"] = boost::json::array();
+ return;
+ }
+ boost::json::array items;
+ for (const LLPointer<LLInventoryObject>& entry : objects)
+ {
+ LLInventoryItem* item = dynamic_cast<LLInventoryItem*>(entry.get());
+ if (!item)
+ {
+ continue;
+ }
+ boost::json::object row;
+ row["id"] = item->getUUID().asString();
+ row["name"] = item->getName();
+ row["type"] = std::string(LLAssetType::lookup(item->getType()));
+ row["asset_id"] = item->getAssetUUID().asString();
+ items.push_back(std::move(row));
+ }
+ out["pending"] = false;
+ out["items"] = std::move(items);
+ }
+
+ void tool_script_create(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "script_create");
+ LLViewerObject* obj = object_by_id(jstr(args, "id"), is_error);
+ const std::string name = jstr(args, "name", "New Script");
+ const bool lua = jstr(args, "language", "lsl") == "lua";
+ LLPermissions perm;
+ perm.init(gAgent.getID(), gAgent.getID(), LLUUID::null, LLUUID::null);
+ perm.initMasks(PERM_ALL, PERM_ALL, PERM_COPY, PERM_NONE, PERM_COPY | PERM_MODIFY | PERM_TRANSFER);
+ LLSD params;
+ params["enabled"] = true;
+ params["vm"] = lua ? "luau" : "mono";
+ const std::string job = LLUUID::generateNewID().asString();
+ {
+ std::lock_guard<std::mutex> lock(gAsyncMu);
+ gAsync[job] = AsyncJob();
+ }
+ obj->createInventoryItem(
+ LLAssetType::AT_LSL_TEXT,
+ LLInventoryType::IT_LSL,
+ lua ? (U8)SST_LUA : (U8)SST_LSL,
+ name,
+ "script",
+ perm,
+ params,
+ [job](bool success, const LLSD& response)
+ {
+ std::string item_id;
+ if (response.has("item_id"))
+ {
+ item_id = response["item_id"].asString();
+ }
+ finish_async(job, success, response["message"].asString(), item_id);
+ });
+ out["job"] = job;
+ out["pending"] = true;
+ }
+
+ void tool_script_save(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "script_save");
+ LLViewerObject* obj = object_by_id(jstr(args, "id"), is_error);
+ const LLUUID item_id(jstr(args, "item_id"));
+ if (item_id.isNull())
+ {
+ is_error = true;
+ throw std::runtime_error("script_save requires item_id");
+ }
+ const std::string source = jstr(args, "source");
+ if (source.empty())
+ {
+ is_error = true;
+ throw std::runtime_error("script_save requires source");
+ }
+ if (!obj->getRegion())
+ {
+ is_error = true;
+ throw std::runtime_error("object has no region");
+ }
+ const std::string url = obj->getRegion()->getCapability("UpdateScriptTask");
+ if (url.empty())
+ {
+ is_error = true;
+ throw std::runtime_error("UpdateScriptTask is not available");
+ }
+ const std::string vm = jstr(args, "vm", "mono");
+ const bool running = jbool(args, "running", true);
+ const std::string job = LLUUID::generateNewID().asString();
+ {
+ std::lock_guard<std::mutex> lock(gAsyncMu);
+ gAsync[job] = AsyncJob();
+ }
+ const LLUUID prim_id = obj->getID();
+ auto on_success = [job](LLUUID itemId, LLUUID, LLUUID, LLSD response)
+ {
+ std::string detail = response["compiled"].asBoolean() ? "compiled" : "compile failed";
+ if (response.has("errors"))
+ {
+ for (const LLSD& error : llsd::inArray(response["errors"]))
+ {
+ detail += "\n";
+ detail += error.asString();
+ }
+ }
+ finish_async(job, response["compiled"].asBoolean(), detail, itemId.asString());
+ };
+ auto on_failure = [job](LLUUID, LLUUID, LLSD, std::string reason)
+ {
+ finish_async(job, false, reason, std::string());
+ return false;
+ };
+ LLResourceUploadInfo::ptr_t upload(std::make_shared<LLScriptAssetUpload>(
+ prim_id, item_id, vm, running, LLUUID::null, source, on_success, on_failure));
+ LLViewerAssetUpload::EnqueueInventoryUpload(url, upload);
+ out["job"] = job;
+ out["pending"] = true;
+ }
+
+ void tool_script_job(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ const std::string job = jstr(args, "job");
+ if (job.empty())
+ {
+ is_error = true;
+ throw std::runtime_error("script_job requires job");
+ }
+ std::lock_guard<std::mutex> lock(gAsyncMu);
+ auto it = gAsync.find(job);
+ if (it == gAsync.end())
+ {
+ is_error = true;
+ throw std::runtime_error("unknown script job");
+ }
+ out["done"] = it->second.done;
+ out["ok"] = it->second.ok;
+ out["detail"] = it->second.detail;
+ out["item_id"] = it->second.item_id;
+ out["source"] = it->second.source;
+ out["world_text_untrusted"] = true;
+ }
+
+ bool read_cached_script(const LLUUID& asset_id, std::string& text)
+ {
+ if (asset_id.isNull() || !LLFileSystem::getExists(asset_id, LLAssetType::AT_LSL_TEXT))
+ {
+ return false;
+ }
+ LLFileSystem file(asset_id, LLAssetType::AT_LSL_TEXT);
+ const S32 length = file.getSize();
+ if (length <= 0)
+ {
+ return false;
+ }
+ std::vector<char> buffer((size_t)length + 1);
+ file.read(reinterpret_cast<U8*>(buffer.data()), length);
+ if (file.getLastBytesRead() != length)
+ {
+ return false;
+ }
+ text.assign(buffer.data(), (size_t)length);
+ return true;
+ }
+
+ bool open_object_script(LLViewerObject* prim, LLInventoryItem* item, std::string& error)
+ {
+ LLSD floater_key;
+ floater_key["taskid"] = prim->getID();
+ floater_key["itemid"] = item->getUUID();
+ LLLiveLSLEditor* preview = LLFloaterReg::showTypedInstance<LLLiveLSLEditor>("preview_scriptedit", floater_key, TAKE_FOCUS_YES);
+ if (!preview)
+ {
+ error = "could not open " + item->getName();
+ return false;
+ }
+ preview->setObjectID(prim->getID());
+ preview->openInExternalEditorWhenReady();
+ return true;
+ }
+
+ class LLQuietFileUpload : public LLNewFileResourceUploadInfo
+ {
+ public:
+ using LLNewFileResourceUploadInfo::LLNewFileResourceUploadInfo;
+ void setJob(const std::string& job) { mJob = job; }
+ bool showUploadDialog() const override { return false; }
+ bool showInventoryPanel() const override { return false; }
+ LLUUID finishUpload(LLSD& result) override
+ {
+ const LLUUID item_id = LLNewFileResourceUploadInfo::finishUpload(result);
+ finish_async(mJob, item_id.notNull(), item_id.notNull() ? "uploaded" : "upload finished without an inventory item", item_id.asString());
+ return item_id;
+ }
+ bool failedUpload(LLSD&, std::string& reason) override
+ {
+ finish_async(mJob, false, reason, std::string());
+ return true;
+ }
+ private:
+ std::string mJob;
+ };
+
+ class LLQuietResourceUpload : public LLResourceUploadInfo
+ {
+ public:
+ using LLResourceUploadInfo::LLResourceUploadInfo;
+ void setJob(const std::string& job) { mJob = job; }
+ bool showUploadDialog() const override { return false; }
+ bool showInventoryPanel() const override { return false; }
+ LLUUID finishUpload(LLSD& result) override
+ {
+ const LLUUID item_id = LLResourceUploadInfo::finishUpload(result);
+ finish_async(mJob, item_id.notNull(), item_id.notNull() ? "uploaded" : "upload finished without an inventory item", item_id.asString());
+ return item_id;
+ }
+ bool failedUpload(LLSD&, std::string& reason) override
+ {
+ finish_async(mJob, false, reason, std::string());
+ return true;
+ }
+ private:
+ std::string mJob;
+ };
+
+ struct AnimUploadOptions
+ {
+ bool has_loop = false;
+ bool loop = false;
+ bool has_loop_in = false;
+ F32 loop_in = 0.f;
+ bool has_loop_out = false;
+ F32 loop_out = 0.f;
+ bool has_priority = false;
+ S32 priority = 0;
+ bool has_ease_in = false;
+ F32 ease_in = 0.f;
+ bool has_ease_out = false;
+ F32 ease_out = 0.f;
+ bool has_hand = false;
+ std::string hand;
+ bool has_emote = false;
+ LLUUID emote;
+ };
+
+ bool animation_options_set(const AnimUploadOptions& opt)
+ {
+ return opt.has_loop || opt.has_loop_in || opt.has_loop_out || opt.has_priority
+ || opt.has_ease_in || opt.has_ease_out || opt.has_hand || opt.has_emote;
+ }
+
+ AnimUploadOptions read_anim_options(const boost::json::object* args)
+ {
+ AnimUploadOptions opt;
+ opt.has_loop = jhas(args, "loop");
+ opt.loop = jbool(args, "loop", false);
+ opt.has_loop_in = jhas(args, "loop_in");
+ opt.loop_in = (F32)jreal(args, "loop_in", 0.0);
+ opt.has_loop_out = jhas(args, "loop_out");
+ opt.loop_out = (F32)jreal(args, "loop_out", 100.0);
+ opt.has_priority = jhas(args, "priority");
+ opt.priority = (S32)jreal(args, "priority", 2.0);
+ opt.has_ease_in = jhas(args, "ease_in");
+ opt.ease_in = (F32)jreal(args, "ease_in", 0.0);
+ opt.has_ease_out = jhas(args, "ease_out");
+ opt.ease_out = (F32)jreal(args, "ease_out", 0.0);
+ opt.has_hand = jhas(args, "hand_pose");
+ opt.hand = jstr(args, "hand_pose");
+ opt.has_emote = jhas(args, "emote");
+ opt.emote = LLUUID(jstr(args, "emote"));
+ return opt;
+ }
+
+ void apply_anim_options(LLKeyframeMotion* motion, const AnimUploadOptions& opt)
+ {
+ if (opt.has_loop)
+ {
+ motion->setLoop(opt.loop);
+ }
+ if (opt.has_priority)
+ {
+ motion->setPriority(llclamp(opt.priority, 0, 6));
+ }
+ if (opt.has_ease_in)
+ {
+ motion->setEaseIn(llmax(opt.ease_in, 0.f));
+ }
+ if (opt.has_ease_out)
+ {
+ motion->setEaseOut(llmax(opt.ease_out, 0.f));
+ }
+ if (opt.has_hand)
+ {
+ motion->setHandPose(LLHandMotion::getHandPose(opt.hand));
+ }
+ if (opt.has_emote && opt.emote.notNull())
+ {
+ motion->setEmote(opt.emote);
+ }
+ const F32 duration = motion->getDuration();
+ if (opt.has_loop_in)
+ {
+ motion->setLoopIn(llclamp(opt.loop_in, 0.f, 100.f) * 0.01f * duration);
+ }
+ if (opt.has_loop_out)
+ {
+ motion->setLoopOut(llclamp(opt.loop_out, 0.f, 100.f) * 0.01f * duration);
+ }
+ }
+
+ bool write_motion_asset(LLKeyframeMotion* motion, const LLAssetID& asset_id, std::string& error)
+ {
+ const S32 file_size = motion->getFileSize();
+ U8* buffer = new(std::nothrow) U8[file_size];
+ if (!buffer)
+ {
+ error = "out of memory preparing animation";
+ return false;
+ }
+ LLDataPackerBinaryBuffer dp(buffer, file_size);
+ bool wrote = false;
+ if (motion->serialize(dp))
+ {
+ LLFileSystem file(asset_id, LLAssetType::AT_ANIMATION, LLFileSystem::APPEND);
+ wrote = file.write(buffer, dp.getCurrentSize());
+ if (!wrote)
+ {
+ error = "failed to write the animation asset";
+ }
+ }
+ else
+ {
+ error = "failed to serialize the animation";
+ }
+ delete[] buffer;
+ LLKeyframeDataCache::removeKeyframeData(asset_id);
+ return wrote;
+ }
+
+ LLKeyframeMotion* motion_from_bvh(const std::string& path, const LLAssetID& asset_id, std::string& error)
+ {
+ if (!gAgentAvatarp)
+ {
+ error = "avatar is not loaded";
+ return nullptr;
+ }
+ std::error_code ec;
+ LLFile infile(path, LLFile::in | LLFile::binary, ec);
+ if (ec || !infile)
+ {
+ error = "cannot open BVH file";
+ return nullptr;
+ }
+ const S64 size = infile.size(ec);
+ if (ec || size <= 0 || size > 8 * 1024 * 1024)
+ {
+ error = "BVH file is empty or too large";
+ return nullptr;
+ }
+ std::vector<char> text((size_t)size + 1);
+ if (infile.read(text.data(), size, ec) != size)
+ {
+ error = "failed to read BVH file";
+ return nullptr;
+ }
+ text[(size_t)size] = '\0';
+
+ ELoadStatus status = E_ST_OK;
+ S32 line = 0;
+ auto aliases = gAgentAvatarp->getJointAliases();
+ LLBVHLoader loader(text.data(), status, line, aliases);
+ if (!loader.isInitialized() || loader.getDuration() > MAX_ANIM_DURATION)
+ {
+ error = llformat("BVH load failed at line %d (status %d)", line, (int)status);
+ return nullptr;
+ }
+
+ const S32 buffer_size = (S32)loader.getOutputSize();
+ U8* raw = new(std::nothrow) U8[buffer_size];
+ if (!raw)
+ {
+ error = "out of memory converting BVH";
+ return nullptr;
+ }
+ LLDataPackerBinaryBuffer packed(raw, buffer_size);
+ loader.serialize(packed);
+ packed.reset();
+
+ LLKeyframeMotion* motion = new LLKeyframeMotion(asset_id);
+ motion->setCharacter(gAgentAvatarp);
+ const bool ok = motion->deserialize(packed, asset_id, false);
+ delete[] raw;
+ if (!ok)
+ {
+ delete motion;
+ error = "failed to read converted BVH motion";
+ return nullptr;
+ }
+ return motion;
+ }
+
+ LLKeyframeMotion* motion_from_anim(const std::string& path, const LLAssetID& asset_id, std::string& error)
+ {
+ std::error_code ec;
+ LLFile infile(path, LLFile::in | LLFile::binary, ec);
+ if (ec || !infile)
+ {
+ error = "cannot open animation file";
+ return nullptr;
+ }
+ const S64 size = infile.size(ec);
+ if (ec || size <= 0 || size > INT_MAX)
+ {
+ error = "animation file is empty or too large";
+ return nullptr;
+ }
+ U8* raw = new(std::nothrow) U8[size];
+ if (!raw)
+ {
+ error = "out of memory reading animation";
+ return nullptr;
+ }
+ const S64 got = infile.read(raw, size, ec);
+ if (ec || got != size)
+ {
+ delete[] raw;
+ error = "failed to read animation file";
+ return nullptr;
+ }
+ LLDataPackerBinaryBuffer packed(raw, (S32)size);
+ LLKeyframeMotion* motion = new LLKeyframeMotion(asset_id);
+ if (gAgentAvatarp)
+ {
+ motion->setCharacter(gAgentAvatarp);
+ }
+ const bool ok = motion->deserialize(packed, asset_id, false);
+ delete[] raw;
+ if (!ok)
+ {
+ delete motion;
+ error = "failed to read animation file";
+ return nullptr;
+ }
+ return motion;
+ }
+
+ U32 upload_perm(const boost::json::object* args, const char* key, U32 fallback)
+ {
+ if (!jhas(args, key))
+ {
+ return fallback;
+ }
+ return (U32)llclamp(jreal(args, key, (F64)fallback), 0.0, (F64)PERM_ALL);
+ }
+
+ void tool_upload(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "upload");
+ if (gDisconnected || !gAgent.getRegion())
+ {
+ is_error = true;
+ throw std::runtime_error("not connected to a region");
+ }
+
+ const std::string path = jstr(args, "path");
+ if (path.empty())
+ {
+ is_error = true;
+ throw std::runtime_error("path is required");
+ }
+ std::string ext = gDirUtilp->getExtension(path);
+ LLStringUtil::toLower(ext);
+ LLAssetType::EType asset_type = LLAssetType::AT_NONE;
+ U32 codec = IMG_CODEC_INVALID;
+ if (!LLResourceUploadInfo::findAssetTypeAndCodecOfExtension(ext, asset_type, codec, false))
+ {
+ is_error = true;
+ throw std::runtime_error("unsupported file type; use an image, wav, anim, or bvh");
+ }
+
+ std::string name = jstr(args, "name");
+ if (name.empty())
+ {
+ name = gDirUtilp->getBaseFileName(path, true);
+ }
+ LLStringUtil::replaceNonstandardASCII(name, '?');
+ LLStringUtil::replaceChar(name, '|', '?');
+ LLStringUtil::stripNonprintable(name);
+ LLStringUtil::trim(name);
+ if (name.empty())
+ {
+ is_error = true;
+ throw std::runtime_error("name is empty");
+ }
+ const std::string description = jstr(args, "description");
+ const LLUUID folder_id(jstr(args, "folder_id"));
+ const U32 next_owner = upload_perm(args, "next_owner_perm", LLFloaterPerms::getNextOwnerPerms("Uploads"));
+ const U32 group_perm = upload_perm(args, "group_perm", LLFloaterPerms::getGroupPerms("Uploads"));
+ const U32 everyone_perm = upload_perm(args, "everyone_perm", LLFloaterPerms::getEveryonePerms("Uploads"));
+
+ const AnimUploadOptions anim = read_anim_options(args);
+ const bool anim_file = ext == "bvh" || ext == "anim";
+ if (animation_options_set(anim) && !anim_file)
+ {
+ is_error = true;
+ throw std::runtime_error("loop, ease, priority, hand_pose, and emote apply only to anim and bvh files");
+ }
+
+ S32 cost = 0;
+ U32 max_image = 0;
+ if (asset_type == LLAssetType::AT_TEXTURE)
+ {
+ if (jhas(args, "max_size"))
+ {
+ max_image = (U32)llclamp(jreal(args, "max_size", 0.0), 4.0, (F64)LLViewerFetchedTexture::MAX_IMAGE_SIZE_DEFAULT);
+ }
+ LLPointer<LLImageFormatted> image = LLImageFormatted::createFromType(codec);
+ if (image.isNull() || !image->load(path))
+ {
+ is_error = true;
+ throw std::runtime_error("failed to read the image");
+ }
+ S32 width = image->getWidth();
+ S32 height = image->getHeight();
+ if (max_image > 0)
+ {
+ width = llmin(width, (S32)max_image);
+ height = llmin(height, (S32)max_image);
+ }
+ cost = LLAgentBenefitsMgr::current().getTextureUploadCost(width, height);
+ }
+ else if (asset_type == LLAssetType::AT_SOUND)
+ {
+ cost = LLAgentBenefitsMgr::current().getSoundUploadCost();
+ }
+ else if (asset_type == LLAssetType::AT_ANIMATION)
+ {
+ cost = LLAgentBenefitsMgr::current().getAnimationUploadCost();
+ }
+ if (gStatusBar && gStatusBar->getBalance() < cost)
+ {
+ is_error = true;
+ throw std::runtime_error(llformat("upload costs L$%d and the balance is lower", cost));
+ }
+
+ LLInventoryType::EType inv_type = LLInventoryType::IT_NONE;
+ if (asset_type == LLAssetType::AT_TEXTURE)
+ {
+ inv_type = LLInventoryType::IT_TEXTURE;
+ }
+ else if (asset_type == LLAssetType::AT_SOUND)
+ {
+ inv_type = LLInventoryType::IT_SOUND;
+ }
+ else if (asset_type == LLAssetType::AT_ANIMATION)
+ {
+ inv_type = LLInventoryType::IT_ANIMATION;
+ }
+
+ LLTransactionID transaction;
+ transaction.generate();
+ const LLAssetID asset_id = transaction.makeAssetID(gAgent.getSecureSessionID());
+
+ if (anim_file && (ext == "bvh" || animation_options_set(anim)))
+ {
+ std::string prepare_error;
+ LLKeyframeMotion* motion = (ext == "bvh")
+ ? motion_from_bvh(path, asset_id, prepare_error)
+ : motion_from_anim(path, asset_id, prepare_error);
+ if (!motion)
+ {
+ is_error = true;
+ throw std::runtime_error(prepare_error);
+ }
+ apply_anim_options(motion, anim);
+ const bool wrote = write_motion_asset(motion, asset_id, prepare_error);
+ delete motion;
+ if (!wrote)
+ {
+ is_error = true;
+ throw std::runtime_error(prepare_error);
+ }
+
+ const std::string job = LLUUID::generateNewID().asString();
+ {
+ std::lock_guard<std::mutex> lock(gAsyncMu);
+ gAsync[job] = AsyncJob();
+ }
+ auto upload = std::make_shared<LLQuietResourceUpload>(
+ transaction, LLAssetType::AT_ANIMATION,
+ name, description, 0,
+ LLFolderType::FT_NONE, LLInventoryType::IT_ANIMATION,
+ next_owner, group_perm, everyone_perm,
+ cost, folder_id, false);
+ upload->setJob(job);
+ LLResourceUploadInfo::ptr_t base = upload;
+ upload_new_resource(base);
+ out["job"] = job;
+ }
+ else
+ {
+ const std::string job = LLUUID::generateNewID().asString();
+ {
+ std::lock_guard<std::mutex> lock(gAsyncMu);
+ gAsync[job] = AsyncJob();
+ }
+ auto upload = std::make_shared<LLQuietFileUpload>(
+ path, name, description, 0,
+ LLFolderType::FT_NONE, inv_type,
+ next_owner, group_perm, everyone_perm,
+ cost, folder_id, false);
+ upload->setJob(job);
+ if (max_image > 0)
+ {
+ upload->setMaxImageSize(max_image);
+ }
+ LLResourceUploadInfo::ptr_t base = upload;
+ upload_new_resource(base);
+ out["job"] = job;
+ }
+
+ out["queued"] = true;
+ out["pending"] = true;
+ out["name"] = name;
+ out["asset_type"] = std::string(LLAssetType::lookup(asset_type));
+ out["transaction_id"] = transaction.asString();
+ out["cost"] = cost;
+ if (folder_id.notNull())
+ {
+ out["folder_id"] = folder_id.asString();
+ }
+ }
+
+ void tool_script_edit(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "script_edit");
+ LLViewerObject* prim = object_by_id(jstr(args, "id"), is_error);
+ if (!prim->permModify() && !gAgent.isGodlike())
+ {
+ is_error = true;
+ throw std::runtime_error("you cannot modify this object");
+ }
+
+ LLInventoryObject::object_list_t contents;
+ prim->getInventoryContents(contents);
+ std::vector<LLInventoryItem*> scripts;
+ for (const LLPointer<LLInventoryObject>& entry : contents)
+ {
+ LLInventoryItem* item = dynamic_cast<LLInventoryItem*>(entry.get());
+ if (item && item->getType() == LLAssetType::AT_LSL_TEXT)
+ {
+ scripts.push_back(item);
+ }
+ }
+ if (scripts.empty())
+ {
+ if (!prim->isInventoryPending())
+ {
+ prim->requestInventory();
+ }
+ is_error = true;
+ throw std::runtime_error("object contents are not loaded yet, or it has no scripts; call contents and retry");
+ }
+
+ const bool open_all = jbool(args, "all", false);
+ std::vector<std::string> wanted_names;
+ if (jhas(args, "name"))
+ {
+ wanted_names.push_back(jstr(args, "name"));
+ }
+ if (args)
+ {
+ auto names = args->find("names");
+ if (names != args->end() && names->value().is_array())
+ {
+ for (const boost::json::value& value : names->value().as_array())
+ {
+ if (value.is_string())
+ {
+ wanted_names.push_back(std::string(value.as_string()));
+ }
+ }
+ }
+ }
+ const LLUUID only_id(jstr(args, "item_id"));
+
+ std::vector<LLInventoryItem*> chosen;
+ if (open_all)
+ {
+ chosen = scripts;
+ }
+ else if (only_id.notNull())
+ {
+ for (LLInventoryItem* script : scripts)
+ {
+ if (script->getUUID() == only_id)
+ {
+ chosen.push_back(script);
+ }
+ }
+ if (chosen.empty())
+ {
+ is_error = true;
+ throw std::runtime_error("that item is not a script in this object");
+ }
+ }
+ else if (!wanted_names.empty())
+ {
+ for (const std::string& wanted : wanted_names)
+ {
+ bool found = false;
+ for (LLInventoryItem* script : scripts)
+ {
+ if (LLStringUtil::compareInsensitive(script->getName(), wanted) == 0)
+ {
+ chosen.push_back(script);
+ found = true;
+ }
+ }
+ if (!found)
+ {
+ is_error = true;
+ throw std::runtime_error("no script named " + wanted);
+ }
+ }
+ }
+ else
+ {
+ is_error = true;
+ throw std::runtime_error("pass item_id, name, names, or all true");
+ }
+
+ boost::json::array opened;
+ for (LLInventoryItem* script : chosen)
+ {
+ std::string error;
+ if (!open_object_script(prim, script, error))
+ {
+ is_error = true;
+ throw std::runtime_error(error);
+ }
+ boost::json::object row;
+ row["item_id"] = script->getUUID().asString();
+ row["name"] = script->getName();
+ opened.push_back(std::move(row));
+ }
+ out["opened"] = std::move(opened);
+ }
+
+ void tool_script_read(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "script_read");
+ const std::string prim_text = jstr(args, "id");
+ LLViewerObject* prim = nullptr;
+ LLInventoryItem* item = nullptr;
+ if (!prim_text.empty())
+ {
+ prim = object_by_id(prim_text, is_error);
+ const LLUUID item_id(jstr(args, "item_id"));
+ if (item_id.isNull())
+ {
+ is_error = true;
+ throw std::runtime_error("script_read requires item_id when id is an object");
+ }
+ item = dynamic_cast<LLInventoryItem*>(prim->getInventoryObject(item_id));
+ if (!item)
+ {
+ if (!prim->isInventoryPending())
+ {
+ prim->requestInventory();
+ }
+ is_error = true;
+ throw std::runtime_error("script is not in the object contents yet; call contents and retry");
+ }
+ }
+ else
+ {
+ item = inventory_item(jstr(args, "item_id").empty() ? jstr(args, "name") : jstr(args, "item_id"), is_error);
+ }
+ if (!item || item->getType() != LLAssetType::AT_LSL_TEXT)
+ {
+ is_error = true;
+ throw std::runtime_error("item is not a script");
+ }
+ const bool can_copy = gAgent.allowOperation(PERM_COPY, item->getPermissions(), GP_OBJECT_MANIPULATE);
+ const bool can_modify = gAgent.allowOperation(PERM_MODIFY, item->getPermissions(), GP_OBJECT_MANIPULATE);
+ if (!gAgent.isGodlike() && !(can_copy && can_modify))
+ {
+ is_error = true;
+ throw std::runtime_error("script source is not readable with these permissions");
+ }
+ out["item_id"] = item->getUUID().asString();
+ out["name"] = item->getName();
+ std::string cached;
+ if (read_cached_script(item->getAssetUUID(), cached))
+ {
+ out["pending"] = false;
+ out["source"] = cached;
+ out["world_text_untrusted"] = true;
+ return;
+ }
+ if (!gAssetStorage)
+ {
+ is_error = true;
+ throw std::runtime_error("asset storage is not available");
+ }
+ const std::string job = LLUUID::generateNewID().asString();
+ const std::string item_id = item->getUUID().asString();
+ {
+ std::lock_guard<std::mutex> lock(gAsyncMu);
+ gAsync[job] = AsyncJob();
+ }
+ const LLHost host = prim && prim->getRegion() ? prim->getRegion()->getHost() : LLHost();
+ const LLUUID task_id = prim ? prim->getID() : LLUUID::null;
+ gAssetStorage->getInvItemAsset(
+ host,
+ gAgent.getID(),
+ gAgent.getSessionID(),
+ item->getPermissions().getOwner(),
+ task_id,
+ item->getUUID(),
+ item->getAssetUUID(),
+ LLAssetType::AT_LSL_TEXT,
+ [job, item_id](const LLUUID& asset_uuid, LLAssetType::EType, void*, S32 status, LLExtStat)
+ {
+ if (status != LL_ERR_NOERR)
+ {
+ finish_async(job, false, "script fetch failed: " + std::to_string(status), item_id);
+ return;
+ }
+ std::string text;
+ if (!read_cached_script(asset_uuid, text))
+ {
+ finish_async(job, false, "script file was not in the cache after download", item_id);
+ return;
+ }
+ finish_async(job, true, std::string(), item_id, text);
+ },
+ nullptr,
+ true);
+ out["job"] = job;
+ out["pending"] = true;
+ }
+
+ boost::json::object inventory_json(LLViewerInventoryItem* item)
+ {
+ boost::json::object row;
+ const LLPermissions& perm = item->getPermissions();
+ row["id"] = item->getUUID().asString();
+ row["name"] = item->getName();
+ row["description"] = item->getDescription();
+ row["type"] = std::string(LLAssetType::lookup(item->getType()));
+ row["asset_id"] = item->getAssetUUID().asString();
+ row["folder_id"] = item->getParentUUID().asString();
+ row["owner_mask"] = (int)perm.getMaskOwner();
+ row["next_owner_mask"] = (int)perm.getMaskNextOwner();
+ row["group_mask"] = (int)perm.getMaskGroup();
+ row["everyone_mask"] = (int)perm.getMaskEveryone();
+ return row;
+ }
+
+ LLAssetType::EType asset_type_arg(const std::string& type, bool& is_error)
+ {
+ if (type.empty())
+ {
+ return LLAssetType::AT_NONE;
+ }
+ std::string key = type;
+ LLStringUtil::toLower(key);
+ if (key == "animation" || key == "anim")
+ {
+ key = "animatn";
+ }
+ const LLAssetType::EType parsed = LLAssetType::lookup(key);
+ if (parsed == LLAssetType::AT_NONE)
+ {
+ is_error = true;
+ throw std::runtime_error("unknown inventory type " + type);
+ }
+ return parsed;
+ }
+
+ void tool_inventory_find(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "inventory_find");
+ std::string query = jstr(args, "query");
+ LLStringUtil::trim(query);
+ if (query.empty())
+ {
+ is_error = true;
+ throw std::runtime_error("query is required");
+ }
+ LLStringUtil::toLower(query);
+ const LLAssetType::EType type_filter = asset_type_arg(jstr(args, "type"), is_error);
+ const S32 limit = (S32)llclamp(jreal(args, "limit", 25.0), 1.0, 100.0);
+ LLInventoryModel::cat_array_t cats;
+ LLInventoryModel::item_array_t items;
+ gInventory.collectDescendents(gInventory.getRootFolderID(), cats, items, LLInventoryModel::EXCLUDE_TRASH);
+ boost::json::array rows;
+ for (LLViewerInventoryItem* item : items)
+ {
+ if (!item || (S32)rows.size() >= limit)
+ {
+ continue;
+ }
+ if (type_filter != LLAssetType::AT_NONE && item->getType() != type_filter)
+ {
+ continue;
+ }
+ std::string name = item->getName();
+ LLStringUtil::toLower(name);
+ if (name.find(query) == std::string::npos)
+ {
+ continue;
+ }
+ rows.push_back(inventory_json(item));
+ }
+ out["items"] = std::move(rows);
+ }
+
+ void tool_inventory_get(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "inventory_get");
+ LLViewerInventoryItem* item = inventory_item(jstr(args, "id"), is_error);
+ out["item"] = inventory_json(item);
+ }
+
+ LLInventoryItem* task_item(LLViewerObject* obj, const boost::json::object* args, bool& is_error)
+ {
+ LLInventoryObject::object_list_t contents;
+ obj->getInventoryContents(contents);
+ if (contents.empty() && !obj->isInventoryPending())
+ {
+ obj->requestInventory();
+ }
+ const LLUUID item_id(jstr(args, "item_id"));
+ const std::string name = jstr(args, "name");
+ LLInventoryItem* found = nullptr;
+ S32 matches = 0;
+ for (const LLPointer<LLInventoryObject>& entry : contents)
+ {
+ LLInventoryItem* item = dynamic_cast<LLInventoryItem*>(entry.get());
+ if (!item)
+ {
+ continue;
+ }
+ if (item_id.notNull())
+ {
+ if (item->getUUID() == item_id)
+ {
+ return item;
+ }
+ continue;
+ }
+ if (!name.empty() && item->getName() == name)
+ {
+ found = item;
+ ++matches;
+ }
+ }
+ if (item_id.notNull())
+ {
+ if (obj->isInventoryPending() || contents.empty())
+ {
+ is_error = true;
+ throw std::runtime_error("object contents are not loaded yet; call contents and retry");
+ }
+ is_error = true;
+ throw std::runtime_error("item is not in the object");
+ }
+ if (matches != 1)
+ {
+ if (contents.empty() || obj->isInventoryPending())
+ {
+ is_error = true;
+ throw std::runtime_error("object contents are not loaded yet; call contents and retry");
+ }
+ is_error = true;
+ throw std::runtime_error(matches == 0 ? "item is not in the object" : "name matches more than one item; pass item_id");
+ }
+ return found;
+ }
+
+ void tool_contents_copy(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "contents_copy");
+ LLViewerObject* obj = object_by_id(jstr(args, "id"), is_error);
+ if (!(obj->permModify() || obj->flagAllowInventoryAdd()))
+ {
+ is_error = true;
+ throw std::runtime_error("you cannot add inventory to this object");
+ }
+ LLViewerInventoryItem* item = inventory_item(jstr(args, "item_id"), is_error);
+ if (item->getIsLinkType())
+ {
+ is_error = true;
+ throw std::runtime_error("cannot copy an inventory link");
+ }
+ if (!item->getPermissions().allowCopyBy(gAgent.getID()))
+ {
+ is_error = true;
+ throw std::runtime_error("item is not copyable; it was not moved out of inventory");
+ }
+ LLPointer<LLViewerInventoryItem> copy = new LLViewerInventoryItem(item);
+ copy->setCreationDate(time_corrected());
+ obj->updateInventory(copy, TASK_INVENTORY_ITEM_KEY, true);
+ out["sent"] = true;
+ out["item_id"] = item->getUUID().asString();
+ out["name"] = item->getName();
+ }
+
+ void tool_contents_remove(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "contents_remove");
+ LLViewerObject* obj = object_by_id(jstr(args, "id"), is_error);
+ if (!obj->permModify() && !obj->flagAllowInventoryAdd())
+ {
+ is_error = true;
+ throw std::runtime_error("you cannot change this object's inventory");
+ }
+ LLInventoryItem* item = task_item(obj, args, is_error);
+ const LLUUID removed = item->getUUID();
+ const std::string removed_name = item->getName();
+ obj->removeInventory(removed);
+ out["sent"] = true;
+ out["item_id"] = removed.asString();
+ out["name"] = removed_name;
+ }
+
+ LLSD color_arg(const boost::json::object* args, const char* key)
+ {
+ LLSD color;
+ if (!args || !args->contains(key) || !args->at(key).is_array())
+ {
+ return color;
+ }
+ for (const auto& component : args->at(key).as_array())
+ {
+ if (component.is_number())
+ {
+ color.append(component.to_number<double>());
+ }
+ }
+ return color;
+ }
+
+ void tool_upload_mesh(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "upload_mesh");
+ const std::string path = jstr(args, "path");
+ if (path.empty())
+ {
+ is_error = true;
+ throw std::runtime_error("path is required");
+ }
+ LLSD options;
+ if (jhas(args, "name")) options["name"] = jstr(args, "name");
+ if (jhas(args, "description")) options["description"] = jstr(args, "description");
+ if (jhas(args, "scale")) options["scale"] = jreal(args, "scale", 1.0);
+ if (jhas(args, "pelvis_offset")) options["pelvis_offset"] = jreal(args, "pelvis_offset", 0.0);
+ if (jhas(args, "upload_textures")) options["upload_textures"] = jbool(args, "upload_textures", true);
+ if (jhas(args, "upload_skin")) options["upload_skin"] = jbool(args, "upload_skin", false);
+ if (jhas(args, "upload_joints")) options["upload_joints"] = jbool(args, "upload_joints", false);
+ if (jhas(args, "lock_scale")) options["lock_scale"] = jbool(args, "lock_scale", false);
+ if (jhas(args, "lod_medium")) options["lod_medium"] = jstr(args, "lod_medium");
+ if (jhas(args, "lod_low")) options["lod_low"] = jstr(args, "lod_low");
+ if (jhas(args, "lod_lowest")) options["lod_lowest"] = jstr(args, "lod_lowest");
+ if (jhas(args, "physics")) options["physics"] = jstr(args, "physics");
+ const LLUUID folder(jstr(args, "folder_id"));
+ const std::string job = LLUUID::generateNewID().asString();
+ {
+ std::lock_guard<std::mutex> lock(gAsyncMu);
+ gAsync[job] = AsyncJob();
+ }
+ const bool started = LLFloaterModelPreview::startHeadlessUpload(path, folder, options,
+ [job](bool ok, const std::string& detail, S32 price)
+ {
+ const std::string text = price > 0 ? detail + llformat(" fee=L$%d", price) : detail;
+ finish_async(job, ok, text, std::string());
+ });
+ if (!started)
+ {
+ finish_async(job, false, "mesh upload could not be started", std::string());
+ is_error = true;
+ throw std::runtime_error("mesh upload could not be started");
+ }
+ out["job"] = job;
+ out["queued"] = true;
+ out["pending"] = true;
+ }
+
+ void tool_upload_material(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "upload_material");
+ const std::string path = jstr(args, "path");
+ if (path.empty())
+ {
+ is_error = true;
+ throw std::runtime_error("path is required");
+ }
+ LLSD options;
+ if (jhas(args, "name")) options["name"] = jstr(args, "name");
+ if (jhas(args, "double_sided")) options["double_sided"] = jbool(args, "double_sided", false);
+ if (jhas(args, "transparency")) options["transparency"] = jreal(args, "transparency", 0.0);
+ if (jhas(args, "alpha_mode")) options["alpha_mode"] = jstr(args, "alpha_mode");
+ if (jhas(args, "alpha_cutoff")) options["alpha_cutoff"] = jreal(args, "alpha_cutoff", 0.0);
+ if (jhas(args, "metalness")) options["metalness"] = jreal(args, "metalness", 0.0);
+ if (jhas(args, "roughness")) options["roughness"] = jreal(args, "roughness", 1.0);
+ const LLSD base_color = color_arg(args, "base_color");
+ if (base_color.isArray()) options["base_color"] = base_color;
+ const LLSD emissive = color_arg(args, "emissive_color");
+ if (emissive.isArray()) options["emissive_color"] = emissive;
+ const S32 index = jhas(args, "index") ? (S32)jreal(args, "index", 0.0) : -1;
+ const LLUUID folder(jstr(args, "folder_id"));
+ const std::string job = LLUUID::generateNewID().asString();
+ {
+ std::lock_guard<std::mutex> lock(gAsyncMu);
+ gAsync[job] = AsyncJob();
+ }
+ std::string error;
+ const bool started = LLMaterialEditor::startHeadlessUpload(path, index, folder, options,
+ [job](bool ok, const LLUUID& item_id, const std::string& detail)
+ {
+ finish_async(job, ok, detail, item_id.asString());
+ },
+ error);
+ if (!started)
+ {
+ finish_async(job, false, error, std::string());
+ is_error = true;
+ throw std::runtime_error(error.empty() ? "material upload could not be started" : error);
+ }
+ out["job"] = job;
+ out["queued"] = true;
+ out["pending"] = true;
+ }
+
+ const char* physics_shape_name(U8 shape)
+ {
+ switch (shape)
+ {
+ case LLViewerObject::PHYSICS_SHAPE_PRIM: return "prim";
+ case LLViewerObject::PHYSICS_SHAPE_NONE: return "none";
+ case LLViewerObject::PHYSICS_SHAPE_CONVEX_HULL: return "convex_hull";
+ default: return "unknown";
+ }
+ }
+
+ void tool_material_set(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "material_set");
+ LLViewerObject* obj = object_by_id(jstr(args, "id"), is_error);
+ if (!obj->permModify())
+ {
+ is_error = true;
+ throw std::runtime_error("object is not modifiable");
+ }
+ LLUUID material_id(jstr(args, "material_id"));
+ if (material_id.isNull())
+ {
+ is_error = true;
+ throw std::runtime_error("material_id is required");
+ }
+ if (LLViewerInventoryItem* item = gInventory.getItem(material_id))
+ {
+ if (item->getType() == LLAssetType::AT_MATERIAL && item->getAssetUUID().notNull())
+ {
+ material_id = item->getAssetUUID();
+ }
+ }
+ const S32 face = jhas(args, "face") ? (S32)jreal(args, "face", -1.0) : -1;
+ if (face >= (S32)obj->getNumTEs())
+ {
+ is_error = true;
+ throw std::runtime_error("face is out of range");
+ }
+ obj->setRenderMaterialID(face, material_id, true);
+ LLGLTFMaterialList::flushUpdates();
+ out["sent"] = true;
+ out["face"] = face;
+ out["material_id"] = material_id.asString();
+ }
+
+ LLVOVolume* linkset_root_volume(LLViewerObject* obj, bool& is_error)
+ {
+ LLViewerObject* root = obj->getRootEdit();
+ if (!root || root->getPCode() != LL_PCODE_VOLUME)
+ {
+ is_error = true;
+ throw std::runtime_error("animated mesh applies only to a mesh or prim linkset");
+ }
+ return (LLVOVolume*)root;
+ }
+
+ void tool_animated_mesh(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "animated_mesh");
+ LLViewerObject* obj = object_by_id(jstr(args, "id"), is_error);
+ LLVOVolume* root = linkset_root_volume(obj, is_error);
+ const bool changing = jhas(args, "enabled");
+ if (changing)
+ {
+ if (!root->permModify())
+ {
+ is_error = true;
+ throw std::runtime_error("object is not modifiable");
+ }
+ const bool enabled = jbool(args, "enabled", false);
+ U32 flags = root->getExtendedMeshFlags();
+ U32 new_flags = enabled
+ ? (flags | LLExtendedMeshParams::ANIMATED_MESH_ENABLED_FLAG)
+ : (flags & ~LLExtendedMeshParams::ANIMATED_MESH_ENABLED_FLAG);
+ if (new_flags != flags)
+ {
+ root->setExtendedMeshFlags(new_flags);
+ root->refreshBakeTexture();
+ for (LLViewerObject* child : root->getChildren())
+ {
+ if (child)
+ {
+ child->refreshBakeTexture();
+ }
+ }
+ if (gAgentAvatarp)
+ {
+ gAgentAvatarp->updateMeshVisibility();
+ }
+ }
+ }
+ out["id"] = obj->getID().asString();
+ out["root_id"] = root->getID().asString();
+ out["animated_mesh"] = root->isAnimatedObject();
+ out["sent"] = changing;
+ }
+
+ void tool_object_inspect(const boost::json::object* args, boost::json::object& out, bool& is_error)
+ {
+ require_world(is_error, "object_inspect");
+ LLViewerObject* obj = object_by_id(jstr(args, "id"), is_error);
+ const LLVector3 scale = obj->getScale();
+ boost::json::object dimensions;
+ dimensions["x"] = scale.mV[VX];
+ dimensions["y"] = scale.mV[VY];
+ dimensions["z"] = scale.mV[VZ];
+ out["dimensions"] = std::move(dimensions);
+ const U8 shape = obj->getPhysicsShapeType();
+ out["physics_shape"] = physics_shape_name(shape);
+ out["physics_shape_code"] = (int)shape;
+ out["animated_mesh"] = obj->isAnimatedObject();
+ const F32 object_cost = obj->getObjectCost();
+ const F32 physics_cost = obj->getPhysicsCost();
+ const F32 linkset_cost = obj->getLinksetCost();
+ const F32 linkset_physics = obj->getLinksetPhysicsCost();
+ out["object_cost"] = object_cost;
+ out["physics_cost"] = physics_cost;
+ out["linkset_cost"] = linkset_cost;
+ out["linkset_physics_cost"] = linkset_physics;
+ const F32 land = std::max(std::max(object_cost, physics_cost), std::max(linkset_cost, linkset_physics));
+ out["land_impact"] = (S32)std::ceil(land);
+ boost::json::array faces;
+ const S32 face_count = (S32)obj->getNumTEs();
+ for (S32 i = 0; i < face_count; ++i)
+ {
+ boost::json::object face;
+ face["face"] = i;
+ face["material_id"] = obj->getRenderMaterialID((U8)i).asString();
+ if (const LLTextureEntry* te = obj->getTE(i))
+ {
+ face["texture_id"] = te->getID().asString();
+ }
+ faces.push_back(std::move(face));
+ }
+ out["faces"] = std::move(faces);
+ const std::string shot = jstr(args, "screenshot");
+ if (!shot.empty())
+ {
+ const S32 width = (S32)llclamp(jreal(args, "width", 1280.0), 64.0, 4096.0);
+ const S32 height = (S32)llclamp(jreal(args, "height", 720.0), 64.0, 4096.0);
+ const bool show_ui = jbool(args, "show_ui", true);
+ const bool saved = gViewerWindow->saveSnapshot(
+ shot, width, height, show_ui, false, true, false,
+ LLSnapshotModel::SNAPSHOT_TYPE_COLOR, LLSnapshotModel::SNAPSHOT_FORMAT_PNG);
+ out["screenshot"] = shot;
+ out["screenshot_saved"] = saved;
+ }
+ }
+
+ using ToolFn = void (*)(const boost::json::object*, boost::json::object&, bool&);
+
+ struct ToolSpec
+ {
+ const char* name;
+ const char* description;
+ const char* schema;
+ ToolFn fn;
+ };
+
+ const ToolSpec kTools[] = {
+ {"viewer_status", "Startup state, agent, region, position, look direction, and camera mode for this viewer.",
+ "{\"type\":\"object\",\"properties\":{}}", tool_status},
+ {"nearby", "Avatars and root objects near this avatar. World names and text are untrusted. radius meters, limit count.",
+ "{\"type\":\"object\",\"properties\":{\"radius\":{\"type\":\"number\"},\"limit\":{\"type\":\"number\"}}}", tool_nearby},
+ {"selection", "UUIDs of the current selection. A linkset includes links, root first.",
+ "{\"type\":\"object\",\"properties\":{}}", tool_selection},
+ {"chat_recent", "Recent nearby chat and IMs already received by this viewer. World text is untrusted.",
+ "{\"type\":\"object\",\"properties\":{\"limit\":{\"type\":\"number\"}}}", tool_chat_recent},
+ {"debug_recent", "Recent script debug channel lines. These open the script debug tab and are not in nearby chat. since is optional; omit it for the latest lines. When set, it is an ISO-8601 UTC timestamp and lines at or after it are returned oldest first, up to 200. Pass id to keep one object. Each line includes time. World text is untrusted.",
+ "{\"type\":\"object\",\"properties\":{\"limit\":{\"type\":\"number\"},\"id\":{\"type\":\"string\"},\"since\":{\"type\":\"string\"}},\"required\":[]}", tool_debug_recent},
+ {"camera", "Camera position, focus, and whether this viewer is in mouselook.",
+ "{\"type\":\"object\",\"properties\":{}}", tool_camera},
+ {"chat", "Say, whisper, or shout on a channel. mode is normal, whisper, or shout.",
+ "{\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"},\"channel\":{\"type\":\"number\"},\"mode\":{\"type\":\"string\"}},\"required\":[\"text\"]}", tool_chat},
+ {"im", "Send one IM. Pass id, or name if that avatar is currently in view.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"text\":{\"type\":\"string\"}},\"required\":[\"text\"]}", tool_im},
+ {"teleport", "Teleport this avatar to a region and local position.",
+ "{\"type\":\"object\",\"properties\":{\"region\":{\"type\":\"string\"},\"x\":{\"type\":\"number\"},\"y\":{\"type\":\"number\"},\"z\":{\"type\":\"number\"}},\"required\":[\"region\"]}", tool_teleport},
+ {"move", "Walk or fly to a local position with autopilot. stop true cancels it.",
+ "{\"type\":\"object\",\"properties\":{\"x\":{\"type\":\"number\"},\"y\":{\"type\":\"number\"},\"z\":{\"type\":\"number\"},\"allow_flying\":{\"type\":\"boolean\"},\"stop_distance\":{\"type\":\"number\"},\"stop\":{\"type\":\"boolean\"}}}", tool_move},
+ {"sit", "Sit on a prim by UUID.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}", tool_sit},
+ {"stand", "Stand up.",
+ "{\"type\":\"object\",\"properties\":{}}", tool_stand},
+ {"touch", "Touch an object by UUID. face is optional.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"face\":{\"type\":\"number\"}},\"required\":[\"id\"]}", tool_touch},
+ {"look_at", "Look at an avatar, object, or local position.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"x\":{\"type\":\"number\"},\"y\":{\"type\":\"number\"},\"z\":{\"type\":\"number\"}}}", tool_look_at},
+ {"camera_set", "Move the camera to a local position aimed at focus_x/y/z. reset true returns it to the user.",
+ "{\"type\":\"object\",\"properties\":{\"x\":{\"type\":\"number\"},\"y\":{\"type\":\"number\"},\"z\":{\"type\":\"number\"},\"focus_x\":{\"type\":\"number\"},\"focus_y\":{\"type\":\"number\"},\"focus_z\":{\"type\":\"number\"},\"reset\":{\"type\":\"boolean\"}}}", tool_camera_set},
+ {"select", "Select an in-world object by UUID.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}", tool_select},
+ {"rez_prim", "Create a prim at a local position. shape is box, sphere, cylinder, prism, cone, torus, or pyramid. The new prim is selected when the region sends it back.",
+ "{\"type\":\"object\",\"properties\":{\"shape\":{\"type\":\"string\"},\"x\":{\"type\":\"number\"},\"y\":{\"type\":\"number\"},\"z\":{\"type\":\"number\"},\"size\":{\"type\":\"number\"},\"scale_x\":{\"type\":\"number\"},\"scale_y\":{\"type\":\"number\"},\"scale_z\":{\"type\":\"number\"},\"physics\":{\"type\":\"boolean\"}}}", tool_rez_prim},
+ {"rez_item", "Rez an object from inventory at a local position. Pass id or name. The inventory item is kept.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"x\":{\"type\":\"number\"},\"y\":{\"type\":\"number\"},\"z\":{\"type\":\"number\"}}}", tool_rez_item},
+ {"take", "Take an owned object into inventory. copy true leaves the object in the world.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"copy\":{\"type\":\"boolean\"}},\"required\":[\"id\"]}", tool_take},
+ {"attach_points", "List avatar attachment point names and how many objects are on each.",
+ "{\"type\":\"object\",\"properties\":{}}", tool_attach_points},
+ {"attach", "Attach an inventory object. point is an attachment point name; omit it to wear on the item's default point.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"point\":{\"type\":\"string\"}}}", tool_attach},
+ {"detach", "Detach a worn object by its in-world UUID.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}", tool_detach},
+ {"edit", "Change one prim: position, size, rotation, name, description, material, physics, color, texture, glow, and shape. Rotation and twist are degrees. Color channels are 0-1 or 0-255. face limits color and texture to one face.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"x\":{\"type\":\"number\"},\"y\":{\"type\":\"number\"},\"z\":{\"type\":\"number\"},\"size\":{\"type\":\"number\"},\"scale_x\":{\"type\":\"number\"},\"scale_y\":{\"type\":\"number\"},\"scale_z\":{\"type\":\"number\"},\"rot_x\":{\"type\":\"number\"},\"rot_y\":{\"type\":\"number\"},\"rot_z\":{\"type\":\"number\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\"},\"material\":{\"type\":\"string\"},\"physics\":{\"type\":\"boolean\"},\"phantom\":{\"type\":\"boolean\"},\"temporary\":{\"type\":\"boolean\"},\"r\":{\"type\":\"number\"},\"g\":{\"type\":\"number\"},\"b\":{\"type\":\"number\"},\"alpha\":{\"type\":\"number\"},\"transparency\":{\"type\":\"number\"},\"glow\":{\"type\":\"number\"},\"fullbright\":{\"type\":\"boolean\"},\"shiny\":{\"type\":\"string\"},\"texture\":{\"type\":\"string\"},\"face\":{\"type\":\"number\"},\"hollow\":{\"type\":\"number\"},\"twist\":{\"type\":\"number\"},\"twist_begin\":{\"type\":\"number\"},\"taper_x\":{\"type\":\"number\"},\"taper_y\":{\"type\":\"number\"},\"shear_x\":{\"type\":\"number\"},\"shear_y\":{\"type\":\"number\"},\"cut_begin\":{\"type\":\"number\"},\"cut_end\":{\"type\":\"number\"},\"path_begin\":{\"type\":\"number\"},\"path_end\":{\"type\":\"number\"},\"hole\":{\"type\":\"string\"},\"skew\":{\"type\":\"number\"},\"revolutions\":{\"type\":\"number\"}},\"required\":[\"id\"]}", tool_edit},
+ {"link", "Link objects. The last id becomes the root. If object properties are still downloading, poll script_job. sent is true only after ObjectLink is sent.",
+ "{\"type\":\"object\",\"properties\":{\"ids\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}}},\"required\":[\"ids\"]}", tool_link},
+ {"unlink", "Unlink the linkset that contains this object.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}", tool_unlink},
+ {"contents", "List an object's inventory. The first call may return pending while the contents download. Each item includes asset_id.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}", tool_contents},
+ {"contents_copy", "Copy an agent inventory item into an object. The item must be copyable. Pass the object id and the inventory item_id.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"item_id\":{\"type\":\"string\"}},\"required\":[\"id\",\"item_id\"]}", tool_contents_copy},
+ {"contents_remove", "Remove an item from an object's inventory. Pass item_id or a unique name.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"item_id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"}},\"required\":[\"id\"]}", tool_contents_remove},
+ {"inventory_find", "Search this avatar's inventory by name. query is a case-insensitive substring. type is optional, for example animation, texture, or sound.",
+ "{\"type\":\"object\",\"properties\":{\"query\":{\"type\":\"string\"},\"type\":{\"type\":\"string\"},\"limit\":{\"type\":\"number\"}},\"required\":[\"query\"]}", tool_inventory_find},
+ {"inventory_get", "Read one inventory item by id, including its asset id, folder, description, and permission masks.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"}},\"required\":[\"id\"]}", tool_inventory_get},
+ {"script_create", "Create a new LSL or Lua script inside an object. Poll script_job for the item id.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"language\":{\"type\":\"string\"}},\"required\":[\"id\"]}", tool_script_create},
+ {"script_edit", "Open object scripts in the external editor, the same as the Edit button. Pass item_id, name, names, or all true.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"item_id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"names\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},\"all\":{\"type\":\"boolean\"}},\"required\":[\"id\"]}", tool_script_edit},
+ {"upload", "Upload a local image, wav, anim, or bvh into inventory. This spends L$. Returns job; poll script_job until done, then item_id is the new inventory item. Animation options are optional and apply only to anim and bvh: loop, loop_in and loop_out as percent of duration, ease_in and ease_out in seconds, priority 0-6, hand_pose, emote uuid. max_size limits a texture. name, description, folder_id, and permission masks are optional.",
+ "{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\"},\"folder_id\":{\"type\":\"string\"},\"next_owner_perm\":{\"type\":\"number\"},\"group_perm\":{\"type\":\"number\"},\"everyone_perm\":{\"type\":\"number\"},\"max_size\":{\"type\":\"number\"},\"loop\":{\"type\":\"boolean\"},\"loop_in\":{\"type\":\"number\"},\"loop_out\":{\"type\":\"number\"},\"ease_in\":{\"type\":\"number\"},\"ease_out\":{\"type\":\"number\"},\"priority\":{\"type\":\"number\"},\"hand_pose\":{\"type\":\"string\"},\"emote\":{\"type\":\"string\"}},\"required\":[\"path\"]}", tool_upload},
+ {"upload_mesh", "Upload a local dae, gltf, or glb mesh without the model dialog. This spends L$. Returns job; poll script_job. detail is the model name and fee. Optional: name, description, folder_id, scale, pelvis_offset, upload_textures, upload_skin, upload_joints, lock_scale, lod_medium, lod_low, lod_lowest, physics. Those last four are file paths.",
+ "{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"},\"description\":{\"type\":\"string\"},\"folder_id\":{\"type\":\"string\"},\"scale\":{\"type\":\"number\"},\"pelvis_offset\":{\"type\":\"number\"},\"upload_textures\":{\"type\":\"boolean\"},\"upload_skin\":{\"type\":\"boolean\"},\"upload_joints\":{\"type\":\"boolean\"},\"lock_scale\":{\"type\":\"boolean\"},\"lod_medium\":{\"type\":\"string\"},\"lod_low\":{\"type\":\"string\"},\"lod_lowest\":{\"type\":\"string\"},\"physics\":{\"type\":\"string\"}},\"required\":[\"path\"]}", tool_upload_mesh},
+ {"material_set", "Assign a PBR material asset to one face of an object, or every face when face is omitted. material_id is the material asset UUID.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"face\":{\"type\":\"number\"},\"material_id\":{\"type\":\"string\"}},\"required\":[\"id\",\"material_id\"]}", tool_material_set},
+ {"animated_mesh", "Read or set Animated Mesh on a linkset. The flag lives on the root. Pass enabled true or false to change it; omit enabled to read it. The reply includes animated_mesh.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"enabled\":{\"type\":\"boolean\"}},\"required\":[\"id\"]}", tool_animated_mesh},
+ {"object_inspect", "Read an object's dimensions, physics shape, animated mesh state, costs, land impact, and per-face material and texture ids. screenshot is an optional PNG path.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"screenshot\":{\"type\":\"string\"},\"width\":{\"type\":\"number\"},\"height\":{\"type\":\"number\"},\"show_ui\":{\"type\":\"boolean\"}},\"required\":[\"id\"]}", tool_object_inspect},
+ {"upload_material", "Upload one PBR material from a local gltf or glb file without the material dialog. Returns job; poll script_job for item_id. index selects a material when the file has several. Optional: name, folder_id, double_sided, transparency, alpha_mode, alpha_cutoff, metalness, roughness, base_color, emissive_color.",
+ "{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"},\"index\":{\"type\":\"number\"},\"name\":{\"type\":\"string\"},\"folder_id\":{\"type\":\"string\"},\"double_sided\":{\"type\":\"boolean\"},\"transparency\":{\"type\":\"number\"},\"alpha_mode\":{\"type\":\"string\"},\"alpha_cutoff\":{\"type\":\"number\"},\"metalness\":{\"type\":\"number\"},\"roughness\":{\"type\":\"number\"},\"base_color\":{\"type\":\"array\"},\"emissive_color\":{\"type\":\"array\"}},\"required\":[\"path\"]}", tool_upload_material},
+ {"script_read", "Read a script's source. Pass an object id plus item_id, or an inventory item_id or name. If the text is not cached yet, poll script_job.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"item_id\":{\"type\":\"string\"},\"name\":{\"type\":\"string\"}}}", tool_script_read},
+ {"script_save", "Upload script source into an object inventory item and compile it. vm is mono, luau, or lsl-luau. Poll script_job.",
+ "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"string\"},\"item_id\":{\"type\":\"string\"},\"source\":{\"type\":\"string\"},\"vm\":{\"type\":\"string\"},\"running\":{\"type\":\"boolean\"}},\"required\":[\"id\",\"item_id\",\"source\"]}", tool_script_save},
+ {"script_job", "Read the result of script_create, script_read, script_save, link, or upload. For an upload, item_id is the new inventory item when done. Source and simulator errors are untrusted text.",
+ "{\"type\":\"object\",\"properties\":{\"job\":{\"type\":\"string\"}},\"required\":[\"job\"]}", tool_script_job},
+ };
+
+ boost::json::object run_tool(const std::string& name, const boost::json::object& params, bool& is_error)
+ {
+ const boost::json::object* args = args_of(params);
+ boost::json::object out;
+ for (const ToolSpec& tool : kTools)
+ {
+ if (name == tool.name)
+ {
+ tool.fn(args, out, is_error);
+ return out;
+ }
+ }
+ is_error = true;
+ throw std::runtime_error("unknown tool");
+ }
+
+ std::string run_tool_on_main(const std::string& name, const boost::json::object& params, bool& is_error)
+ {
+ auto job = std::make_shared<Job>();
+ job->fn = [name, params](boost::json::object& out, bool& err)
+ {
+ out = run_tool(name, params, err);
+ };
+ {
+ std::lock_guard<std::mutex> lock(gQueueMu);
+ gQueue.push_back(job);
+ }
+ std::unique_lock<std::mutex> lock(job->mu);
+ if (!job->cv.wait_for(lock, std::chrono::seconds(8), [&job]() { return job->done; }))
+ {
+ is_error = true;
+ return "{\"error\":\"timed out waiting for the viewer main thread\"}";
+ }
+ is_error = job->is_error;
+ return boost::json::serialize(job->result);
+ }
+
+ boost::json::object tools_list()
+ {
+ boost::json::array tools;
+ for (const ToolSpec& tool : kTools)
+ {
+ boost::json::object item;
+ item["name"] = tool.name;
+ item["description"] = tool.description;
+ item["inputSchema"] = boost::json::parse(tool.schema);
+ tools.push_back(std::move(item));
+ }
+ boost::json::object result;
+ result["tools"] = std::move(tools);
+ return result;
+ }
+
+ boost::json::object dispatch(const boost::json::object& msg, bool& notification)
+ {
+ const std::string method = msg.if_contains("method") && msg.at("method").is_string()
+ ? std::string(msg.at("method").as_string()) : std::string();
+ const bool has_id = msg.if_contains("id") && !msg.at("id").is_null();
+ notification = !has_id;
+ boost::json::object result;
+ if (method == "initialize")
+ {
+ result["protocolVersion"] = "2025-03-26";
+ result["capabilities"] = boost::json::object{{"tools", boost::json::object{}}};
+ result["serverInfo"] = boost::json::object{{"name", "megapahit"}, {"version", "1"}};
+ }
+ else if (method == "tools/list")
+ {
+ result = tools_list();
+ }
+ else if (method == "tools/call")
+ {
+ const boost::json::object empty;
+ const boost::json::object& params = msg.if_contains("params") && msg.at("params").is_object()
+ ? msg.at("params").as_object() : empty;
+ const std::string name = params.if_contains("name") && params.at("name").is_string()
+ ? std::string(params.at("name").as_string()) : std::string();
+ bool is_error = false;
+ std::string text;
+ try
+ {
+ text = run_tool_on_main(name, params, is_error);
+ }
+ catch (const std::exception& e)
+ {
+ is_error = true;
+ text = e.what();
+ }
+ boost::json::array content;
+ content.push_back(boost::json::object{{"type", "text"}, {"text", text}});
+ result["content"] = std::move(content);
+ result["isError"] = is_error;
+ }
+ else if (method == "ping" || method == "notifications/initialized" || method == "notifications/cancelled")
+ {
+ notification = method != "ping" && !has_id;
+ }
+ else if (!notification)
+ {
+ throw std::runtime_error("method not found");
+ }
+ return result;
+ }
+
+ bool read_some(mcp_socket_t fd, std::string& buf)
+ {
+ char tmp[4096];
+#if LL_WINDOWS
+ const int n = recv(fd, tmp, sizeof(tmp), 0);
+#else
+ const ssize_t n = ::recv(fd, tmp, sizeof(tmp), 0);
+#endif
+ if (n <= 0)
+ {
+ return false;
+ }
+ buf.append(tmp, tmp + n);
+ return true;
+ }
+
+ void send_all(mcp_socket_t fd, const std::string& data)
+ {
+ size_t sent = 0;
+ while (sent < data.size())
+ {
+#if LL_WINDOWS
+ const int n = ::send(fd, data.data() + sent, (int)(data.size() - sent), 0);
+#else
+ const ssize_t n = ::send(fd, data.data() + sent, data.size() - sent, MSG_NOSIGNAL);
+#endif
+ if (n <= 0)
+ {
+ return;
+ }
+ sent += (size_t)n;
+ }
+ }
+
+ void reply_http(mcp_socket_t fd, int code, const std::string& reason, const std::string& body, const char* content_type)
+ {
+ std::ostringstream out;
+ out << "HTTP/1.1 " << code << " " << reason << "\r\n"
+ << "Content-Type: " << content_type << "\r\n"
+ << "Content-Length: " << body.size() << "\r\n"
+ << "Connection: close\r\n"
+ << "\r\n"
+ << body;
+ send_all(fd, out.str());
+ }
+
+ void serve_client(mcp_socket_t fd)
+ {
+ std::string buf;
+ while (buf.find("\r\n\r\n") == std::string::npos)
+ {
+ if (!read_some(fd, buf) || buf.size() > 1024 * 1024)
+ {
+ return;
+ }
+ }
+ const size_t header_end = buf.find("\r\n\r\n");
+ const std::string headers = buf.substr(0, header_end);
+ const size_t line_end = headers.find("\r\n");
+ const std::string request = headers.substr(0, line_end);
+ if (request.find("POST /mcp") == std::string::npos)
+ {
+ reply_http(fd, 404, "Not Found", "", "text/plain");
+ return;
+ }
+ std::string lower = headers;
+ std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return (char)std::tolower(c); });
+ const size_t len_at = lower.find("content-length:");
+ size_t length = 0;
+ if (len_at != std::string::npos)
+ {
+ length = (size_t)std::strtoul(lower.c_str() + len_at + 15, nullptr, 10);
+ }
+ if (length > 1024 * 1024)
+ {
+ reply_http(fd, 413, "Payload Too Large", "", "text/plain");
+ return;
+ }
+ std::string body = buf.substr(header_end + 4);
+ while (body.size() < length)
+ {
+ if (!read_some(fd, body))
+ {
+ return;
+ }
+ }
+ body.resize(length);
+ try
+ {
+ boost::json::value parsed = boost::json::parse(body);
+ if (!parsed.is_object())
+ {
+ throw std::runtime_error("request is not an object");
+ }
+ const boost::json::object& msg = parsed.as_object();
+ bool notification = false;
+ boost::json::object result = dispatch(msg, notification);
+ if (notification)
+ {
+ reply_http(fd, 202, "Accepted", "", "text/plain");
+ return;
+ }
+ boost::json::object response;
+ response["jsonrpc"] = "2.0";
+ response["id"] = msg.if_contains("id") ? msg.at("id") : boost::json::value(nullptr);
+ response["result"] = std::move(result);
+ reply_http(fd, 200, "OK", boost::json::serialize(response), "application/json");
+ }
+ catch (const std::exception& e)
+ {
+ boost::json::object response;
+ response["jsonrpc"] = "2.0";
+ response["id"] = nullptr;
+ response["error"] = boost::json::object{{"code", -32603}, {"message", e.what()}};
+ reply_http(fd, 200, "OK", boost::json::serialize(response), "application/json");
+ }
+ }
+
+ void write_endpoint_file()
+ {
+ if (!gDirUtilp)
+ {
+ return;
+ }
+ const std::string path = gDirUtilp->getOSUserAppDir() + gDirUtilp->getDirDelimiter() + "mcp.json";
+ boost::json::object doc;
+ doc["url"] = std::string("http://127.0.0.1:") + llformat("%d", gPort) + "/mcp";
+ const std::string body = boost::json::serialize(doc);
+#if LL_WINDOWS
+ FILE* fp = fopen(path.c_str(), "wb");
+#else
+ const int fd = ::open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0600);
+ FILE* fp = fd >= 0 ? fdopen(fd, "w") : nullptr;
+#endif
+ if (!fp)
+ {
+ LL_WARNS("MCP") << "Could not write " << path << LL_ENDL;
+ return;
+ }
+ fwrite(body.data(), 1, body.size(), fp);
+ fclose(fp);
+#if !LL_WINDOWS
+ ::chmod(path.c_str(), 0600);
+#endif
+ LL_INFOS("MCP") << "Listening on 127.0.0.1:" << gPort << LL_ENDL;
+ }
+
+ void remove_endpoint_file()
+ {
+ if (!gDirUtilp)
+ {
+ return;
+ }
+ const std::string path = gDirUtilp->getOSUserAppDir() + gDirUtilp->getDirDelimiter() + "mcp.json";
+ LLFile::remove(path);
+ }
+
+ void listen_loop()
+ {
+#if LL_WINDOWS
+ WSADATA data;
+ WSAStartup(MAKEWORD(2, 2), &data);
+#endif
+ gListen = ::socket(AF_INET, SOCK_STREAM, 0);
+ if (gListen == MCP_INVALID_SOCKET)
+ {
+ gRun = false;
+ return;
+ }
+ int yes = 1;
+ ::setsockopt(gListen, SOL_SOCKET, SO_REUSEADDR, (const char*)&yes, sizeof(yes));
+ sockaddr_in addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+ addr.sin_port = htons((uint16_t)gPort);
+ if (::bind(gListen, (sockaddr*)&addr, sizeof(addr)) != 0 || ::listen(gListen, 4) != 0)
+ {
+ LL_WARNS("MCP") << "Could not listen on 127.0.0.1:" << gPort << LL_ENDL;
+ mcp_close_socket(gListen);
+ gListen = MCP_INVALID_SOCKET;
+ gRun = false;
+ return;
+ }
+ sockaddr_in bound;
+ socklen_t bound_len = sizeof(bound);
+ ::getsockname(gListen, (sockaddr*)&bound, &bound_len);
+ gPort = ntohs(bound.sin_port);
+ write_endpoint_file();
+
+ while (gRun)
+ {
+ fd_set fds;
+ FD_ZERO(&fds);
+ FD_SET(gListen, &fds);
+ timeval tv;
+ tv.tv_sec = 0;
+ tv.tv_usec = 200000;
+#if LL_WINDOWS
+ const int ready = ::select(0, &fds, nullptr, nullptr, &tv);
+#else
+ const int ready = ::select(gListen + 1, &fds, nullptr, nullptr, &tv);
+#endif
+ if (!gRun || ready <= 0)
+ {
+ continue;
+ }
+ sockaddr_in client_addr;
+ socklen_t client_len = sizeof(client_addr);
+ mcp_socket_t client = ::accept(gListen, (sockaddr*)&client_addr, &client_len);
+ if (client == MCP_INVALID_SOCKET)
+ {
+ continue;
+ }
+ if (client_addr.sin_addr.s_addr != htonl(INADDR_LOOPBACK))
+ {
+ mcp_close_socket(client);
+ continue;
+ }
+ serve_client(client);
+ mcp_close_socket(client);
+ }
+ mcp_close_socket(gListen);
+ gListen = MCP_INVALID_SOCKET;
+ remove_endpoint_file();
+ }
+
+ void start_server()
+ {
+ if (gStarted)
+ {
+ return;
+ }
+ S32 port = 9411;
+ if (gSavedSettings.controlExists("ViewerMCPPort"))
+ {
+ port = gSavedSettings.getS32("ViewerMCPPort");
+ }
+ if (port < 1 || port > 65535)
+ {
+ port = 9411;
+ }
+ gPort = (int)port;
+ gRun = true;
+ gStarted = true;
+ gThread = std::thread(listen_loop);
+ }
+
+ void request_stop()
+ {
+ gRun = false;
+ mcp_close_socket(gListen);
+ gListen = MCP_INVALID_SOCKET;
+ }
+
+ void finish_stop()
+ {
+ if (!gStarted)
+ {
+ return;
+ }
+ if (gThread.joinable())
+ {
+ ll_join_thread(gThread);
+ }
+ gStarted = false;
+ remove_endpoint_file();
+ }
+}
+
+void LLViewerMCP::pump()
+{
+ finish_pending_links();
+ const bool enabled = !gShutdown && gSavedSettings.getBOOL("EnableViewerMCP");
+ if (gShutdown || !enabled)
+ {
+ if (gStarted)
+ {
+ request_stop();
+ }
+ }
+ else
+ {
+ start_server();
+ }
+
+ std::deque<std::shared_ptr<Job>> jobs;
+ {
+ std::lock_guard<std::mutex> lock(gQueueMu);
+ jobs.swap(gQueue);
+ }
+ for (const std::shared_ptr<Job>& job : jobs)
+ {
+ bool is_error = false;
+ boost::json::object result;
+ try
+ {
+ job->fn(result, is_error);
+ }
+ catch (const std::exception& e)
+ {
+ is_error = true;
+ result["error"] = e.what();
+ }
+ {
+ std::lock_guard<std::mutex> lock(job->mu);
+ job->result = std::move(result);
+ job->is_error = is_error;
+ job->done = true;
+ }
+ job->cv.notify_one();
+ }
+ if ((gShutdown || !enabled) && gStarted)
+ {
+ finish_stop();
+ }
+}
+
+void LLViewerMCP::shutdown()
+{
+ gShutdown = true;
+ pump();
+}
+
+void LLViewerMCP::noteChat(const std::string& kind, const std::string& from, const LLUUID& from_id, const std::string& text)
+{
+ if (text.empty())
+ {
+ return;
+ }
+ std::lock_guard<std::mutex> lock(gChatMu);
+ gChat.push_back(ChatLine{kind, from, from_id, text});
+ while ((S32)gChat.size() > kMaxChat)
+ {
+ gChat.pop_front();
+ }
+}
+
+void LLViewerMCP::noteDebug(const std::string& from, const LLUUID& from_id, const std::string& text)
+{
+ if (text.empty())
+ {
+ return;
+ }
+ std::lock_guard<std::mutex> lock(gChatMu);
+ gDebug.push_back(ChatLine{"debug", from, from_id, text, LLDate::now().asString()});
+ while ((S32)gDebug.size() > kMaxDebug)
+ {
+ gDebug.pop_front();
+ }
+}