diff options
| author | Rider Linden <rider@lindenlab.com> | 2026-08-28 14:21:52 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-08-28 14:21:52 -0700 |
| commit | 5ddd940bf4bd75659a86e8d0eb205a766be5d53e (patch) | |
| tree | 862ca9ad6c1ed12e92967826a60bcb39f13db523 | |
| parent | e3320f649d2e25aac6df491614b42c94645679c7 (diff) | |
| parent | 608d7193ab965dbf05466dd3a327d6ead356306d (diff) | |
Merge pull request #6214 from secondlife/rider/lua_beta_bugfixA
Rider/lua beta bugfix a
| -rw-r--r-- | doc/external-editor-json-rpc.md | 186 | ||||
| -rw-r--r-- | indra/llcorehttp/lljsonrpcws.cpp | 124 | ||||
| -rw-r--r-- | indra/llcorehttp/lljsonrpcws.h | 13 | ||||
| -rw-r--r-- | indra/newview/llcompilequeue.cpp | 19 | ||||
| -rw-r--r-- | indra/newview/llpublishedobjectmgr.cpp | 26 | ||||
| -rw-r--r-- | indra/newview/llscripteditorws.cpp | 227 | ||||
| -rw-r--r-- | indra/newview/llscripteditorws.h | 7 |
7 files changed, 540 insertions, 62 deletions
diff --git a/doc/external-editor-json-rpc.md b/doc/external-editor-json-rpc.md index 6fd5d20eea..570388ac7c 100644 --- a/doc/external-editor-json-rpc.md +++ b/doc/external-editor-json-rpc.md @@ -17,7 +17,10 @@ This document describes all the message interfaces defined for WebSocket communi - [SessionHandshakeResponse](#sessionhandshakeresponse) - [Session OK](#session-ok) - [SessionDisconnect](#sessiondisconnect) - - [SessionPing](#sessionping) + - [SystemPing](#systemping) + - [SystemVersion](#systemversion) + - [SystemStatus](#systemstatus) + - [SystemListMethods](#systemlistmethods) - [Language and Syntax Interfaces](#language-and-syntax-interfaces) - [SyntaxChange](#syntaxchange) - [Language Syntax ID Request](#language-syntax-id-request) @@ -165,8 +168,14 @@ WebSocket connects → session.handshake → session.ok | `session.handshake` (response) | Extension → Viewer | Response | `SessionHandshakeResponse` | | `session.ok` | Viewer → Extension | Notification | _(no interface)_ | | `session.disconnect` | Bidirectional | Notification | `SessionDisconnect` | -| `session.ping` | Bidirectional | Call | `SessionPing` | -| `session.ping` (response) | Bidirectional | Response | `SessionPingResponse` | +| `system.ping` | Bidirectional | Call | `SystemPing` | +| `system.ping` (response) | Bidirectional | Response | `SystemPingResponse` | +| `system.getVersion` | Bidirectional | Call | _(no parameters)_ | +| `system.getVersion` (response) | Bidirectional | Response | `SystemVersionResponse` | +| `system.status` | Bidirectional | Call | _(no parameters)_ | +| `system.status` (response) | Bidirectional | Response | `SystemStatusResponse` | +| `system.listMethods` | Bidirectional | Call | _(no parameters)_ | +| `system.listMethods` (response) | Bidirectional | Response | `SystemListMethodsResponse`| | `script.subscribe` | Extension → Viewer | Call | `ScriptSubscribe` | | `script.subscribe` (response) | Viewer → Extension | Response | `ScriptSubscribeResponse` | | `script.unsubscribe` | Viewer → Extension | Notification | `ScriptUnsubscribe` | @@ -373,18 +382,19 @@ interface SessionDisconnect { - `4`: Internal server error - `message`: Human-readable description of the disconnect reason -### SessionPing +### SystemPing -**JSON-RPC Method:** `session.ping` (call, bidirectional) +**JSON-RPC Method:** `system.ping` (call, bidirectional) -Heartbeat call used to verify the connection is alive and measure latency. Either side can initiate a ping; the recipient responds with the original timestamp plus its own server time. +Ping call used to verify that the connection is alive and measure latency. Either side can +initiate a ping. -In practice the extension initiates and the viewer only answers — the viewer never sends -`session.ping` itself. The extension pings every 30 seconds and tears the connection down after -two consecutive failures. +The generic JSON-RPC server responds with the simple result `"pong"`. The editor server extends +that response with the original timestamp and its current server time. The extension uses the +extended response for its periodic connection-health check. ```typescript -interface SessionPing { +interface SystemPing { timestamp: number; } ``` @@ -396,7 +406,8 @@ interface SessionPing { **Response:** ```typescript -interface SessionPingResponse { +interface SystemPingResponse { + pong: string; timestamp: number; server_time: number; } @@ -404,15 +415,19 @@ interface SessionPingResponse { **Response Fields:** -- `timestamp`: The original timestamp from the request. Echoed back only when the request supplied one. +- `pong`: Acknowledgement that the ping was received. +- `timestamp`: The original timestamp from the request, echoed by the editor server. - `server_time`: Unix timestamp in milliseconds when the response was generated +The extension sends a `system.ping` request every 30 seconds and tears the connection down +after two consecutive failures. + **Example Request:** ```json { "jsonrpc": "2.0", - "method": "session.ping", + "method": "system.ping", "id": 42, "params": { "timestamp": 1721145600000 @@ -420,6 +435,50 @@ interface SessionPingResponse { } ``` +### SystemVersion + +**JSON-RPC Method:** `system.getVersion` (call, bidirectional) + +Requests the identity and version of the peer. The response uses the same field names in both +directions: + +```typescript +interface SystemVersionResponse { + client_name: string; + client_version: string; +} +``` + +The viewer returns its viewer channel as `client_name` and its full viewer version as +`client_version`. The extension returns its package name as `client_name` and its package version +as `client_version`. + +**Example viewer response:** + +```json +{ + "jsonrpc": "2.0", + "id": 43, + "result": { + "client_name": "Second Life", + "client_version": "7.1.0.123456" + } +} +``` + +**Example extension response:** + +```json +{ + "jsonrpc": "2.0", + "id": 43, + "result": { + "client_name": "sl-vscode-plugin", + "client_version": "1.0.6" + } +} +``` + **Example Response:** ```json @@ -427,12 +486,73 @@ interface SessionPingResponse { "jsonrpc": "2.0", "id": 42, "result": { + "pong": "pong", "timestamp": 1721145600000, "server_time": 1721145600015 } } ``` +### SystemStatus + +**JSON-RPC Method:** `system.status` (call, bidirectional) + +Requests the current status of the peer. The default response is: + +```typescript +interface SystemStatusResponse { + status: "OK"; +} +``` + +Both the viewer and the extension currently return `status: "OK"`. The response may be extended +with additional status information in the future. + +**Example response:** + +```json +{ + "jsonrpc": "2.0", + "id": 44, + "result": { + "status": "OK" + } +} +``` + +### SystemListMethods + +**JSON-RPC Method:** `system.listMethods` (call, bidirectional) + +Requests the names of the methods available on the receiving peer. The response contains an +alphabetically ordered list of unique method names. Sync versus async dispatch is an internal +implementation detail and is not exposed by this interface. + +```typescript +type SystemListMethodsResponse = string[]; +``` + +The viewer returns all methods registered on the connection, including methods registered by the +base JSON-RPC server and the editor server. The extension returns its built-in system methods +together with dynamically registered handlers. + +**Example response:** + +```json +{ + "jsonrpc": "2.0", + "id": 45, + "result": [ + "command.execute", + "command.list", + "system.getVersion", + "system.listMethods", + "system.ping", + "system.status" + ] +} +``` + ## Language and Syntax Interfaces ### SyntaxChange @@ -779,7 +899,7 @@ Debug message notification sent by the viewer during script execution. ```typescript interface RuntimeDebug { - script_id: string; // Not currently sent — see note below + script_id?: string; // Present when the viewer can resolve a script subscription id object_id: string; prim_id: string; item_id: string; @@ -809,7 +929,7 @@ Runtime error notification sent by the viewer when a script encounters an error ```typescript interface RuntimeError { - script_id: string; // Not currently sent — see note below + script_id?: string; // Present when the viewer can resolve a script subscription id object_id: string; prim_id: string; item_id: string; @@ -852,8 +972,9 @@ interface ItemRef { - `name`: Script name as it appears in the prim's inventory. - `language`: The script's source language. Independent of the compile target; the VM is not carried in runtime messages. -**Note on `script_id`:** this field is part of the contract but is **not currently sent** by the -viewer for either `runtime.debug` or `runtime.error`. Implementation is tracked separately. +**Note on `script_id`:** this field is optional. It is included when the viewer can resolve the +originating script inventory item and construct a subscription id from `prim_id` + `item_id`. +When that resolution is not possible, the field is omitted. **Delivery:** `runtime.debug` and `runtime.error` are broadcast to all connections. An event is emitted when the originating object is published or its script is subscribed. @@ -957,6 +1078,7 @@ interface LinkedObject { link_number: number; // Link number (root=1, children≥2) link_name: string; link_description?: string; + permissions?: ObjectPermissions; // Actual permissions for this linked prim inventory: ObjectInventoryItem[]; } @@ -972,7 +1094,7 @@ interface PublishedObject { object_description?: string; region?: string; owner_id?: string; - permissions?: ObjectPermissions; + permissions?: ObjectPermissions; // Actual permissions for this root prim can_save_back?: boolean; // Whether Save Back to Contents is currently available for this object inventory: ObjectInventoryItem[]; // Root prim's scripts and notecards linked_objects?: LinkedObject[]; // Child prims @@ -1220,7 +1342,7 @@ interface ObjectContentSaveParams { item_id: string; content: string; vm?: "mono" | "lsl2" | "luau"; - running?: boolean; // Scripts only: run state applied after compilation. Defaults to false. + running?: boolean; // Scripts only: run state applied after compilation when supplied. } interface ObjectContentSaveResponse { @@ -1238,12 +1360,12 @@ interface ObjectContentSaveResponse { - `item_id`: UUID of the saved inventory item. - `content`: Raw script/notecard source text to store. - `vm` (optional): Scripts only compile target. Accepted values are `"mono"`, `"lsl2"`, `"luau"`. When `"luau"` is specified for an LSL script (as opposed to a native Luau script), the viewer automatically selects the correct LSL-on-Luau compile path. If omitted, inferred from item metadata or content analysis. -- `running` (optional): Scripts only. The run state the viewer applies to the script once the upload and compilation complete. Defaults to `false` when omitted. To preserve a script's current run state across a save, echo the `running` value from the corresponding `ObjectInventoryItem` in the most recent `object.publish` or `object.update`. +- `running` (optional): Scripts only. When provided, the viewer applies that run state after upload and compilation. When omitted, the viewer preserves the script's current run state and does not force it off. - `success`: Whether the upload/save operation succeeded. - `compiled` (optional): Scripts only. `true` when compilation succeeded, `false` when source saved but compile failed. - `diagnostics` (optional): Scripts only. Compiler diagnostics when `compiled` is `false`. -> **Warning:** Omitting `running` does not leave the script's run state unchanged — it stops the script. A client that saves a running script without sending `running: true` will silently stop it. +> **Note:** Omitting `running` leaves the script's existing run state unchanged. Only an explicit `true` or `false` changes the post-save state. **Permissions.** Requires `PERM_MODIFY` on the item and modify permission on the containing prim. See [Common preconditions](#common-preconditions) for the shared checks and errors. @@ -1614,10 +1736,17 @@ The invoked command's own handler may raise further errors — `-32602` for bad `-32003` when the action is not permitted, `-32603` on internal failure. Clients must handle any error code, not only the two above. -**Capability gate:** A side MUST NOT send `command.execute` unless the peer advertised -`commands: true` in the handshake. A receiver that receives the call without having negotiated the -feature should respond with a JSON-RPC error. **Not currently enforced on receive by the viewer** -— the gate is applied only when sending. Implementation is tracked separately. +**Capability gate (directional):** + +- A sender MUST NOT call `command.execute` on a peer that did not advertise `commands: true`. +- A receiver MAY accept `command.execute` whenever it advertised `commands: true`, regardless of + whether the sender advertised `commands`. +- In practice this means: + - Extension → Viewer calls are allowed when the viewer advertised `commands: true`. + - Viewer → Extension calls are allowed only when the extension advertised `commands: true`. + +This treats `commands` as a receiver capability per direction, not as a symmetric "both sides or +nothing" toggle. **Example — extension asks viewer to teleport:** @@ -1712,9 +1841,8 @@ interface CommandParamInfo { - `commands`: Array of commands the responder supports. Each entry describes one command. - `command`: The namespaced command identifier. - `description` (optional): Human-readable description of what the command does. - - `params` (optional): Map of parameter names to their type descriptors. **Not currently - populated** — the viewer returns only `command` and `description`, so parameter discovery - does not work. Implementation is tracked separately. + - `params` (optional): Map of parameter names to their type descriptors. The viewer populates + this field for its registered commands. **Known viewer commands:** @@ -1723,6 +1851,8 @@ interface CommandParamInfo { | `viewer.teleport` | `object_id: string` | Teleport agent to an in-world object. | | `viewer.camera.focus` | `object_id: string` | Zoom camera to an in-world object (same behavior as context menu Zoom In). | | `viewer.object.save_back_to_contents` | `object_id: string` | Save an in-world object back to source object contents. | +| `viewer.script.reset_all` | `object_id: string` | Open the viewer's reset queue and reset all scripts in an in-world object. | +| `viewer.script.recompile_all` | `object_id: string`, `target: "luau" \| "lsl2" \| "mono" \| "auto"` | Open the viewer's compile queue and recompile scripts in an in-world object using the selected target. `luau` automatically selects Luau for native Luau scripts and LSL-Luau for LSL scripts. `auto` uses each script's previously registered VM. | **Known extension commands:** diff --git a/indra/llcorehttp/lljsonrpcws.cpp b/indra/llcorehttp/lljsonrpcws.cpp index d6c5b0eb97..5b704e6322 100644 --- a/indra/llcorehttp/lljsonrpcws.cpp +++ b/indra/llcorehttp/lljsonrpcws.cpp @@ -207,16 +207,17 @@ void LLJSONRPCConnection::processRequest(const LLSD& request) // Async handler — launched as a coroutine, response sent by the lambda. if (is_notification) { - LL_WARNS("JSONRPC") << "Async method " << method - << " called as notification; ignoring" << LL_ENDL; - return; + LL_WARNS("JSONRPC") << "Method " << method + << " called as notification; rejecting request" << LL_ENDL; + throw InvalidRequest("Method " + method + " cannot be called as a notification"); } ptr_t conn = std::static_pointer_cast<LLJSONRPCConnection>(getSelfPtr()); if (!conn) { - LL_WARNS("JSONRPC") << "Connection expired before async method " << method - << " could be launched" << LL_ENDL; - return; + LL_WARNS("JSONRPC") << "Connection expired before method " << method + << " could be launched; failing request" << LL_ENDL; + throw InternalError("Connection expired before method " + method + + " could be launched"); } LLMainThreadTask::dispatch( [handler, method, id, params, conn]() @@ -515,6 +516,24 @@ void LLJSONRPCConnection::unregisterMethod(const std::string& method) LL_DEBUGS("JSONRPC") << "Unregistered method: " << method << LL_ENDL; } +std::set<std::string> LLJSONRPCConnection::getMethods() const +{ + LLMutexLock lock(&mMutex); + std::set<std::string> methods; + + for (const auto& [method, handler] : mMethodHandlers) + { + methods.insert(method); + } + + for (const auto& [method, handler] : mAsyncMethodHandlers) + { + methods.insert(method); + } + + return methods; +} + LLSD LLJSONRPCConnection::makeEnvelope(const LLSD& id, const std::string& method, const LLSD& params, @@ -669,23 +688,11 @@ LLJSONRPCServer::LLJSONRPCServer(const std::string& name, U16 port, bool local_o << " on port " << port << LL_ENDL; // Register standard JSON-RPC methods - registerGlobalMethod("system.listMethods", [this](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD { - LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL; - return getMethodList(); - }); - registerGlobalMethod("system.getStats", [this](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD { LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL; return getServerStats(); }); - registerGlobalMethod("system.ping", [](const std::string& method, const LLSD& id, const LLSD& params) -> LLSD { - LL_DEBUGS("JSONRPC") << "System method " << method << " called" << LL_ENDL; - LLSD result; - result["pong"] = LLDate::now().asString(); - result["params"] = params; - return result; - }); } LLWebsocketMgr::WSConnection::ptr_t LLJSONRPCServer::connectionFactory(LLWebsocketMgr::WSServer::ptr_t server, @@ -718,21 +725,80 @@ void LLJSONRPCServer::setupConnectionMethods(LLJSONRPCConnection::ptr_t connecti connection->registerMethod(method, handler); } - // Register session.ping handler for connection health monitoring - connection->registerMethod("session.ping", - [](const std::string&, const LLSD&, const LLSD& params) -> LLSD + std::weak_ptr<LLJSONRPCConnection> weak_connection = connection; + connection->registerMethod( + "system.listMethods", + [weak_connection]( + const std::string&, + const LLSD&, + const LLSD&) -> LLSD { - LLSD result; - // Echo back the original timestamp - if (params.has("timestamp")) + LLSD methods(LLSD::emptyArray()); + auto connection = weak_connection.lock(); + if (!connection) { - result["timestamp"] = params["timestamp"]; + return methods; } - // Add server's current time in milliseconds - result["server_time"] = static_cast<LLSD::Integer>( - LLDate::now().secondsSinceEpoch() * 1000.0); - return result; + + for (const std::string& method : connection->getMethods()) + { + methods.append(method); + } + + return methods; + }); + + connection->registerMethod( + "system.ping", + [this, weak_connection]( + const std::string& method, + const LLSD& id, + const LLSD& params) -> LLSD + { + LL_DEBUGS("JSONRPC") << "System method " << method + << " called" << LL_ENDL; + return handlePing(weak_connection.lock(), params); + }); + + connection->registerMethod( + "system.getVersion", + [this, weak_connection]( + const std::string& method, + const LLSD& id, + const LLSD& params) -> LLSD + { + LL_DEBUGS("JSONRPC") << "System method " << method + << " called" << LL_ENDL; + return handleGetVersion(weak_connection.lock(), params); }); + + connection->registerMethod( + "system.status", + [this, weak_connection]( + const std::string& method, + const LLSD& id, + const LLSD& params) -> LLSD + { + LL_DEBUGS("JSONRPC") << "System method " << method + << " called" << LL_ENDL; + return handleStatus(weak_connection.lock(), params); + }); +} + +LLSD LLJSONRPCServer::handlePing( + const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const +{ + return LLSD("pong"); +} + +LLSD LLJSONRPCServer::handleStatus( + const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const +{ + LLSD result; + result["status"] = "OK"; + return result; } void LLJSONRPCServer::registerGlobalMethod(const std::string& method, MethodHandler handler) diff --git a/indra/llcorehttp/lljsonrpcws.h b/indra/llcorehttp/lljsonrpcws.h index 7cb26b1fb2..aa4a96be4d 100644 --- a/indra/llcorehttp/lljsonrpcws.h +++ b/indra/llcorehttp/lljsonrpcws.h @@ -275,6 +275,12 @@ public: void unregisterMethod(const std::string& method); /** + * @brief Get the names of all registered methods + * @return Ordered set containing sync and async method names + */ + virtual std::set<std::string> getMethods() const; + + /** * @brief Make an asynchronous JSON-RPC call * @param method The method name to call * @param params The parameters to pass @@ -495,6 +501,13 @@ protected: LLWebsocketMgr::WSConnection::ptr_t connectionFactory(LLWebsocketMgr::WSServer::ptr_t server, LLWebsocketMgr::connection_h handle) override; + virtual LLSD handlePing(const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const; + virtual LLSD handleGetVersion(const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const = 0; + virtual LLSD handleStatus(const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const; + /** * @brief Apply global method handlers to a new connection * @param connection The connection to configure diff --git a/indra/newview/llcompilequeue.cpp b/indra/newview/llcompilequeue.cpp index ba19135fce..98b9255f66 100644 --- a/indra/newview/llcompilequeue.cpp +++ b/indra/newview/llcompilequeue.cpp @@ -356,7 +356,8 @@ bool LLFloaterCompileQueue::processScript(LLHandle<LLFloaterCompileQueue> hfloat LLCheckedHandle<LLFloaterCompileQueue> floater(hfloater); // Dereferencing floater may fail. If they do they throw LLExeceptionStaleHandle. // which is caught in objectScriptProcessingQueueCoro - std::string compile_target = floater->mCompileTarget; + const std::string requested_target = floater->mCompileTarget; + std::string compile_target = requested_target; // Initial test to see if we can (or should) attempt to compile the script. LLInventoryItem *item = dynamic_cast<LLInventoryItem *>(inventory); @@ -367,6 +368,22 @@ bool LLFloaterCompileQueue::processScript(LLHandle<LLFloaterCompileQueue> hfloat return true; } + if (requested_target == "auto-luau") + { + compile_target = + item->getInventorySubType() == SST_LUA ? "luau" : "lsl-luau"; + } + else if (requested_target == "auto") + { + compile_target = item->getRuntime(); + if (compile_target.empty()) + { + floater->addStringMessage( + "Skipping: " + item->getName() + " (no registered VM)"); + return true; + } + } + if (!item->getPermissions().allowModifyBy(gAgent.getID(), gAgent.getGroupID()) || !item->getPermissions().allowCopyBy(gAgent.getID(), gAgent.getGroupID())) { diff --git a/indra/newview/llpublishedobjectmgr.cpp b/indra/newview/llpublishedobjectmgr.cpp index d92bb382de..85a9542dce 100644 --- a/indra/newview/llpublishedobjectmgr.cpp +++ b/indra/newview/llpublishedobjectmgr.cpp @@ -88,6 +88,23 @@ namespace // Never emit an empty prim/object name to downstream tooling. return obj->getID().asString(); } + + void add_object_permissions(LLSD& object_data, LLViewerObject* object) + { + LLPermissions* permissions = + LLSelectMgr::getInstance()->findObjectPermissions(object); + if (!permissions) + { + return; + } + + LLSD permission_entry; + permission_entry["owner"] = + static_cast<S32>(permissions->getMaskOwner()); + permission_entry["next_owner"] = + static_cast<S32>(permissions->getMaskNextOwner()); + object_data["permissions"] = permission_entry; + } } class LLPublishedPrimListener : public LLVOInventoryListener @@ -729,6 +746,7 @@ LLSD LLPublishedObjectMgr::buildPublishedObjectLLSD(LLViewerObject* root) const { LL_PROFILE_ZONE_SCOPED_CATEGORY_SCRIPTDEV; LLSD pub; + add_object_permissions(pub, root); pub["object_id"] = root->getID(); pub["object_name"] = get_prim_name(root); pub["object_description"] = nv_string(root, "Desc"); @@ -737,6 +755,11 @@ LLSD LLPublishedObjectMgr::buildPublishedObjectLLSD(LLViewerObject* root) const { pub["region"] = root->getRegion()->getName(); } + const PublishedObjectInfo* published_info = getPublished(root->getID()); + if (published_info) + { + pub["can_save_back"] = published_info->mCanSaveBackToContents; + } pub["inventory"] = buildPrimInventoryLLSD(root); LLSD linked_objects = LLSD::emptyArray(); @@ -748,6 +771,7 @@ LLSD LLPublishedObjectMgr::buildPublishedObjectLLSD(LLViewerObject* root) const link["link_number"] = link_number++; link["link_name"] = get_prim_name(child); link["link_description"] = nv_string(child, "Desc"); + add_object_permissions(link, child); link["inventory"] = buildPrimInventoryLLSD(child); linked_objects.append(link); } @@ -773,6 +797,7 @@ LLSD LLPublishedObjectMgr::buildObjectListLLSD() const } LLSD pub; + add_object_permissions(pub, root); pub["object_id"] = info.mObjectID; pub["object_name"] = info.mObjectName; pub["object_description"] = info.mObjectDescription; @@ -803,6 +828,7 @@ LLSD LLPublishedObjectMgr::buildObjectListLLSD() const link["link_number"] = prim_info.mLinkNumber; link["link_name"] = prim_info.mPrimName; link["link_description"] = prim_info.mPrimDescription; + add_object_permissions(link, child); link["inventory"] = buildPrimInventoryLLSD(child); linked_objects.append(link); } diff --git a/indra/newview/llscripteditorws.cpp b/indra/newview/llscripteditorws.cpp index f886be819f..72b5b66463 100644 --- a/indra/newview/llscripteditorws.cpp +++ b/indra/newview/llscripteditorws.cpp @@ -34,6 +34,7 @@ #include "llagentcamera.h" #include "llappviewer.h" #include "llchat.h" +#include "llcompilequeue.h" #include "lldate.h" #include "llerror.h" #include "lleventcoro.h" @@ -169,6 +170,14 @@ namespace return std::string(s); } + LLSD object_id_command_params() + { + LLSD params(LLSD::emptyMap()); + params["object_id"]["type"] = "string"; + params["object_id"]["required"] = true; + return params; + } + } //======================================================================== @@ -184,7 +193,8 @@ LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port LL_INFOS("ScriptEditorWS") << "Created JSON-RPC script editor server: " << name << " on port " << port << LL_ENDL; - registerCommand({ "viewer.teleport", "Teleport agent to an in-world object" }, + registerCommand({ "viewer.teleport", "Teleport agent to an in-world object", + object_id_command_params() }, [](U32, const LLSD& p) -> LLSD { LLUUID object_id = p["object_id"].asUUID(); @@ -203,7 +213,9 @@ LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port return response; }); - registerCommand({ "viewer.camera.focus", "Zoom camera to an in-world object (same behavior as context menu Zoom In)" }, + registerCommand({ "viewer.camera.focus", + "Zoom camera to an in-world object (same behavior as context menu Zoom In)", + object_id_command_params() }, [](U32, const LLSD& p) -> LLSD { LLUUID object_id = p["object_id"].asUUID(); @@ -221,11 +233,36 @@ LLScriptEditorWSServer::LLScriptEditorWSServer(const std::string& name, U16 port return response; }); - registerCommand({ "viewer.object.save_back_to_contents", "Save an in-world object back to source object contents" }, + registerCommand({ "viewer.object.save_back_to_contents", + "Save an in-world object back to source object contents", + object_id_command_params() }, [this](U32 connection_id, const LLSD& p) -> LLSD { return this->handleSaveBackToObjectContents(connection_id, p); }); + + registerCommand({ "viewer.script.reset_all", + "Reset all scripts in an in-world object", + object_id_command_params() }, + [this](U32 connection_id, const LLSD& p) -> LLSD + { + return this->handleObjectScriptResetAll(connection_id, p); + }); + + registerCommand({ "viewer.script.recompile_all", + "Recompile all scripts in an in-world object", + LLSDMap("object_id", + LLSDMap("type", "string") + ("required", true)) + ("target", + LLSDMap("type", "string") + ("required", true) + ("description", + "Compilation target: luau, lsl2, mono, or auto")) }, + [this](U32 connection_id, const LLSD& p) -> LLSD + { + return this->handleObjectScriptRecompileAll(connection_id, p); + }); } LLScriptEditorWSServer::ptr_t LLScriptEditorWSServer::getServer() @@ -801,6 +838,146 @@ LLSD LLScriptEditorWSServer::handleObjectScriptReset(U32 connection_id, const LL return response; } +LLSD LLScriptEditorWSServer::handleObjectScriptResetAll(U32 connection_id, const LLSD& params) +{ + LLUUID prim_id = params["object_id"].asUUID(); + if (prim_id.isNull()) + { + throw LLJSONRPCConnection::InvalidParams("object_id is required"); + } + + LLViewerObject* prim = gObjectList.findObject(prim_id); + if (!prim) + { + throw LLJSONRPCConnection::InvalidParams("Object not found"); + } + + LLViewerObject* root = prim->getRootEdit(); + if (!root || !isObjectPublished(root->getID())) + { + throw LLJSONRPCConnection::ForbiddenError("Object is not published"); + } + + if (!prim->flagScripted()) + { + throw LLJSONRPCConnection::InvalidParams( + "Prim contains no scripts"); + } + + if (!prim->permModify()) + { + throw LLJSONRPCConnection::ForbiddenError( + "No modify permission on prim"); + } + + LLUUID queue_id; + queue_id.generate(); + + LLFloaterScriptQueue* queue = + LLFloaterReg::getTypedInstance<LLFloaterScriptQueue>( + "reset_queue", LLSD(queue_id)); + if (!queue) + { + throw LLJSONRPCConnection::InternalError( + "Unable to open reset queue"); + } + + queue->addObject(prim->getID(), prim->getID().asString()); + if (!queue->start()) + { + queue->closeFloater(); + throw LLJSONRPCConnection::InternalError( + "Unable to start reset queue"); + } + + queue->setTitle(LLTrans::getString("ResetQueueTitle")); + + LLSD response; + response["success"] = true; + response["object_id"] = prim->getID(); + response["queued"] = true; + return response; +} + +LLSD LLScriptEditorWSServer::handleObjectScriptRecompileAll( + U32 connection_id, const LLSD& params) +{ + LLUUID object_id = params["object_id"].asUUID(); + if (object_id.isNull()) + { + throw LLJSONRPCConnection::InvalidParams("object_id is required"); + } + + std::string target = params["target"].asString(); + if (target != "luau" && + target != "lsl2" && + target != "mono" && + target != "auto") + { + throw LLJSONRPCConnection::InvalidParams( + "target must be 'luau', 'lsl2', 'mono', or 'auto'"); + } + + LLViewerObject* object = gObjectList.findObject(object_id); + if (!object) + { + throw LLJSONRPCConnection::InvalidParams("Object not found"); + } + + LLViewerObject* root = object->getRootEdit(); + if (!root || root->getID() != object_id || !isObjectPublished(root->getID())) + { + throw LLJSONRPCConnection::ForbiddenError("Object is not published"); + } + + if (!root->flagScripted()) + { + throw LLJSONRPCConnection::InvalidParams( + "Object contains no scripts"); + } + + if (!root->permModify()) + { + throw LLJSONRPCConnection::ForbiddenError( + "No modify permission on object"); + } + + if (target == "luau") + { + target = "auto-luau"; + } + + LLUUID queue_id; + queue_id.generate(); + + LLFloaterCompileQueue* queue = + LLFloaterReg::getTypedInstance<LLFloaterCompileQueue>( + "compile_queue", LLSD(queue_id)); + if (!queue) + { + throw LLJSONRPCConnection::InternalError( + "Unable to open compile queue"); + } + + queue->setCompileTarget(target); + queue->addObject(root->getID(), root->getID().asString()); + if (!queue->start()) + { + queue->closeFloater(); + throw LLJSONRPCConnection::InternalError( + "Unable to start compile queue"); + } + + queue->setTitle(LLTrans::getString("CompileQueueTitle")); + + LLSD response; + response["success"] = true; + response["object_id"] = root->getID(); + response["target"] = (target == "auto-luau") ? "luau" : target; + response["queued"] = true; + return response; +} + LLSD LLScriptEditorWSServer::handleObjectModify(U32 connection_id, const LLSD& params) { // Step 1: Parameter Validation @@ -1019,6 +1196,10 @@ LLSD LLScriptEditorWSServer::handleCommandList() LLSD info; info["command"] = entry.first.command; info["description"] = entry.first.description; + if (!entry.first.params.isUndefined()) + { + info["params"] = entry.first.params; + } commands.append(info); } LLSD response; @@ -1070,6 +1251,33 @@ void LLScriptEditorWSServer::broadcastLanguageChange() } } +LLSD LLScriptEditorWSServer::handlePing( + const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const +{ + LLSD result; + result["pong"] = "pong"; + + if (params.has("timestamp")) + { + result["timestamp"] = params["timestamp"]; + } + + result["server_time"] = static_cast<LLSD::Integer>( + LLDate::now().secondsSinceEpoch() * 1000.0); + return result; +} + +LLSD LLScriptEditorWSServer::handleGetVersion( + const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const +{ + LLSD result; + result["client_name"] = LLVersionInfo::instance().getChannel(); + result["client_version"] = LLVersionInfo::instance().getVersion(); + return result; +} + LLSD LLScriptEditorWSServer::handleLanguageIdRequest() const { LLSD response; @@ -1456,6 +1664,7 @@ LLSD LLScriptEditorWSServer::handleObjectContentGet(const std::string& method, c response["prim_id"] = prim_id; response["item_id"] = item_id; response["content"] = text_content; + response["encoding"] = "utf-8"; return response; } @@ -1523,7 +1732,13 @@ LLSD LLScriptEditorWSServer::saveScript(LLViewerObject* prim, LLInventoryItem* i [&, prim_id, item_id](const std::string& pump_name) { auto [on_success, on_failure] = make_asset_upload_callbacks(pump_name); - bool is_running = params.has("running") ? params["running"].asBoolean() : false; + const LLViewerInventoryItem* viewer_item = + dynamic_cast<const LLViewerInventoryItem*>(item); + bool is_running = viewer_item ? viewer_item->getIsRunning() : false; + if (params.has("running")) + { + is_running = params["running"].asBoolean(); + } LLResourceUploadInfo::ptr_t uploadInfo(std::make_shared<LLScriptAssetUpload>( prim_id, item_id, compile_target, is_running, LLUUID::null, content, @@ -2078,6 +2293,10 @@ void LLScriptEditorWSServer::sendRuntimeEvent( } LLSD message; + if (!script_id.empty()) + { + message["script_id"] = script_id; + } message["object_id"] = event.mRootID; message["prim_id"] = event.mPrimID; message["item_id"] = event.mItemID; diff --git a/indra/newview/llscripteditorws.h b/indra/newview/llscripteditorws.h index 1dc218853b..6eb8385210 100644 --- a/indra/newview/llscripteditorws.h +++ b/indra/newview/llscripteditorws.h @@ -236,6 +236,10 @@ protected: LLWebsocketMgr::connection_h handle) override; void setupConnectionMethods(LLJSONRPCConnection::ptr_t connection) override; + LLSD handlePing(const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const override; + LLSD handleGetVersion(const LLJSONRPCConnection::ptr_t& connection, + const LLSD& params) const override; void broadcastLanguageChange(); @@ -257,6 +261,8 @@ protected: LLSD handleObjectList() const; LLSD handleObjectScriptSetRunning(U32 connection_id, const LLSD& params); LLSD handleObjectScriptReset(U32 connection_id, const LLSD& params); + LLSD handleObjectScriptResetAll(U32 connection_id, const LLSD& params); + LLSD handleObjectScriptRecompileAll(U32 connection_id, const LLSD& params); LLSD handleObjectModify(U32 connection_id, const LLSD& params); LLSD handleObjectItemModify(U32 connection_id, const LLSD& params); LLSD handleCommandExecute(U32 connection_id, const LLSD& params); @@ -333,6 +339,7 @@ private: { std::string command; std::string description; + LLSD params; }; enum WSCommandError { |
