/** * @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 #include #include #include #if !LL_WINDOWS #include #include #endif #include #include #include #include #include #if LL_WINDOWS #include #include 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 #include #include #include #include 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 fn; boost::json::object result; bool is_error = false; bool done = false; std::mutex mu; std::condition_variable cv; }; std::mutex gQueueMu; std::deque> gQueue; std::mutex gChatMu; std::deque gChat; std::deque gDebug; std::atomic gRun{false}; std::atomic 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(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 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(it->value().as_int64()); } if (it->value().is_uint64()) { return static_cast(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 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 matched; { std::lock_guard 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 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 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 ids; F64 started = 0.0; }; std::vector gPendingLinks; std::vector link_ids_from_args(const boost::json::object* args, bool& is_error) { std::vector 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& 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& 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 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 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& entry : objects) { LLInventoryItem* item = dynamic_cast(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 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 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( 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 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 buffer((size_t)length + 1); file.read(reinterpret_cast(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("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 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 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 lock(gAsyncMu); gAsync[job] = AsyncJob(); } auto upload = std::make_shared( 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 lock(gAsyncMu); gAsync[job] = AsyncJob(); } auto upload = std::make_shared( 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 scripts; for (const LLPointer& entry : contents) { LLInventoryItem* item = dynamic_cast(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 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 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(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 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& entry : contents) { LLInventoryItem* item = dynamic_cast(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 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()); } } 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 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 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->fn = [name, params](boost::json::object& out, bool& err) { out = run_tool(name, params, err); }; { std::lock_guard lock(gQueueMu); gQueue.push_back(job); } std::unique_lock 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> jobs; { std::lock_guard lock(gQueueMu); jobs.swap(gQueue); } for (const std::shared_ptr& 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 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 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 lock(gChatMu); gDebug.push_back(ChatLine{"debug", from, from_id, text, LLDate::now().asString()}); while ((S32)gDebug.size() > kMaxDebug) { gDebug.pop_front(); } }