summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorErik Kundiman <erik@megapahit.org>2026-09-04 22:45:38 +0800
committerErik Kundiman <erik@megapahit.org>2026-09-04 22:45:38 +0800
commitf7beb28baafa6600c1e8c9b6a08deb3705ba2e78 (patch)
tree6bd3af510b83613f2e9c9627d3bf4c33e30db394
parentf57d1d24e1a9947405587a6fe36d38ba42e097bd (diff)
parent5ddd940bf4bd75659a86e8d0eb205a766be5d53e (diff)
Merge tag 'Second_Life_Release#5ddd940b-26.4' into 26.4
-rw-r--r--doc/external-editor-json-rpc.md186
-rw-r--r--indra/llcorehttp/lljsonrpcws.cpp124
-rw-r--r--indra/llcorehttp/lljsonrpcws.h13
-rw-r--r--indra/llui/lltextbase.cpp8
-rw-r--r--indra/llui/lltextbase.h5
-rw-r--r--indra/newview/CMakeLists.txt2
-rw-r--r--indra/newview/llcompilequeue.cpp19
-rw-r--r--indra/newview/llfloatermaturitydialog.cpp86
-rw-r--r--indra/newview/llfloatermaturitydialog.h47
-rw-r--r--indra/newview/llpublishedobjectmgr.cpp26
-rw-r--r--indra/newview/llscripteditorws.cpp227
-rw-r--r--indra/newview/llscripteditorws.h7
-rw-r--r--indra/newview/llviewerfloaterreg.cpp2
-rw-r--r--indra/newview/llviewermessage.cpp34
-rw-r--r--indra/newview/skins/default/xui/en/floater_maturity_dialog.xml100
-rw-r--r--indra/newview/skins/default/xui/en/notifications.xml11
16 files changed, 827 insertions, 70 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/llui/lltextbase.cpp b/indra/llui/lltextbase.cpp
index 802367720f..06645df203 100644
--- a/indra/llui/lltextbase.cpp
+++ b/indra/llui/lltextbase.cpp
@@ -158,6 +158,7 @@ LLTextBase::Params::Params()
track_end("track_end", false),
read_only("read_only", false),
skip_link_underline("skip_link_underline", false),
+ link_color("link_color"),
spellcheck("spellcheck", false),
v_pad("v_pad", 0),
h_pad("h_pad", 0),
@@ -196,6 +197,8 @@ LLTextBase::LLTextBase(const LLTextBase::Params &p)
mReadOnly(p.read_only),
mSkipTripleClick(false),
mSkipLinkUnderline(p.skip_link_underline),
+ mHasLinkColor(p.link_color.isProvided()),
+ mLinkColor(p.link_color.isProvided() ? p.link_color() : LLUIColor()),
mSpellCheck(p.spellcheck),
mSpellCheckStart(-1),
mSpellCheckEnd(-1),
@@ -2426,6 +2429,11 @@ void LLTextBase::appendTextImpl(const std::string& new_text, const LLStyle::Para
LLStyle::Params link_params(style_params);
link_params.overwriteFrom(match.getStyle());
+ if (mHasLinkColor)
+ {
+ link_params.color = mLinkColor;
+ link_params.readonly_color = mLinkColor;
+ }
// output the text before the Url
if (start > 0)
diff --git a/indra/llui/lltextbase.h b/indra/llui/lltextbase.h
index 62f984b8fc..d3edd17dca 100644
--- a/indra/llui/lltextbase.h
+++ b/indra/llui/lltextbase.h
@@ -351,7 +351,8 @@ public:
bg_writeable_color,
bg_focus_color,
text_selected_color,
- bg_selected_color;
+ bg_selected_color,
+ link_color;
Optional<bool> bg_visible,
border_visible,
@@ -777,6 +778,8 @@ protected:
bool mAlwaysShowIcons;
bool mSkipLinkUnderline;
+ bool mHasLinkColor;
+ LLUIColor mLinkColor;
// support widgets
LLHandle<LLContextMenu> mPopupMenuHandle;
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt
index ee77a4deaf..800826e9d1 100644
--- a/indra/newview/CMakeLists.txt
+++ b/indra/newview/CMakeLists.txt
@@ -266,6 +266,7 @@ set(viewer_SOURCE_FILES
llfloaterloadprefpreset.cpp
llfloatermarketplacelistings.cpp
llfloatermap.cpp
+ llfloatermaturitydialog.cpp
llfloatermediasettings.cpp
llfloatermemleak.cpp
llfloatermodelpreview.cpp
@@ -962,6 +963,7 @@ set(viewer_HEADER_FILES
llfloatermap.h
llfloatermarketplace.h
llfloatermarketplacelistings.h
+ llfloatermaturitydialog.h
llfloatermediasettings.h
llfloatermemleak.h
llfloatermodelpreview.h
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/llfloatermaturitydialog.cpp b/indra/newview/llfloatermaturitydialog.cpp
new file mode 100644
index 0000000000..afa9a70024
--- /dev/null
+++ b/indra/newview/llfloatermaturitydialog.cpp
@@ -0,0 +1,86 @@
+/**
+ * @file llfloatermaturitydialog.cpp
+ * @brief LLFloaterMaturityDialog class implementation
+ *
+ * $LicenseInfo:firstyear=2026&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2026, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#include "llviewerprecompiledheaders.h"
+
+#include "llfloatermaturitydialog.h"
+
+#include "llbutton.h"
+#include "llagent.h"
+#include "lltextbox.h"
+#include "llviewercontrol.h"
+#include "llviewerregion.h"
+
+LLFloaterMaturityDialog::LLFloaterMaturityDialog(const LLSD& key)
+ : LLModalDialog(key, true)
+{
+}
+
+bool LLFloaterMaturityDialog::postBuild()
+{
+ setCanDrag(false);
+ getChild<LLButton>("continue_btn")->setCommitCallback(
+ [this](LLUICtrl*, const LLSD&) { onContinue(); });
+ getChild<LLButton>("cancel_btn")->setCommitCallback(
+ [this](LLUICtrl*, const LLSD&) { onCancel(); });
+
+ return true;
+}
+
+void LLFloaterMaturityDialog::onOpen(const LLSD& key)
+{
+ LLModalDialog::onOpen(key);
+
+ mRegionAccess = static_cast<U8>(key.asInteger());
+
+ LLStringUtil::format_map_t args;
+ args["[MATURITY]"] = LLViewerRegion::accessToString(mRegionAccess);
+ getChild<LLTextBox>("location_rated_lbl")->setText(getString("location_rated_string", args));
+ getChild<LLTextBox>("current_maturity_lbl")->setText(getString("update_maturity_string", args));
+ getChild<LLButton>("continue_btn")->setLabel(getString("allow_maturity_string", args));
+
+ centerOnScreen();
+}
+
+void LLFloaterMaturityDialog::draw()
+{
+ // Skip floater shadow/background; icon child provides the visual background
+ LLView::draw();
+}
+
+void LLFloaterMaturityDialog::onContinue()
+{
+ gSavedSettings.setU32("PreferredMaturity", static_cast<U32>(mRegionAccess));
+ gAgent.restartFailedTeleportRequest();
+ closeFloater();
+}
+
+void LLFloaterMaturityDialog::onCancel()
+{
+ gAgent.clearTeleportRequest();
+ closeFloater();
+}
+
diff --git a/indra/newview/llfloatermaturitydialog.h b/indra/newview/llfloatermaturitydialog.h
new file mode 100644
index 0000000000..4ca1a53c2a
--- /dev/null
+++ b/indra/newview/llfloatermaturitydialog.h
@@ -0,0 +1,47 @@
+/**
+ * @file llfloatermaturitydialog.h
+ * @brief LLFloaterMaturityDialog class definition
+ *
+ * $LicenseInfo:firstyear=2026&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2026, Linden Research, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation;
+ * version 2.1 of the License only.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA
+ * $/LicenseInfo$
+ */
+
+#pragma once
+
+#include "llmodaldialog.h"
+
+class LLFloaterMaturityDialog : public LLModalDialog
+{
+ friend class LLFloaterReg;
+public:
+ bool postBuild() override;
+ void onOpen(const LLSD& key) override;
+ void draw() override;
+
+private:
+ LLFloaterMaturityDialog(const LLSD& key);
+ ~LLFloaterMaturityDialog() = default;
+
+ void onContinue();
+ void onCancel();
+
+ U8 mRegionAccess = SIM_ACCESS_ADULT;
+};
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
{
diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp
index 52da6b1a37..12320ac86e 100644
--- a/indra/newview/llviewerfloaterreg.cpp
+++ b/indra/newview/llviewerfloaterreg.cpp
@@ -103,6 +103,7 @@
#include "llfloatermap.h"
#include "llfloatermarketplace.h"
#include "llfloatermarketplacelistings.h"
+#include "llfloatermaturitydialog.h"
#include "llfloatermediasettings.h"
#include "llfloatermemleak.h"
#include "llfloatermodelpreview.h"
@@ -431,6 +432,7 @@ void LLViewerFloaterReg::registerFloaters()
LLFloaterReg::add("marketplace", "floater_marketplace.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterMarketplace>);
LLFloaterReg::add("marketplace_listings", "floater_marketplace_listings.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterMarketplaceListings>);
LLFloaterReg::add("marketplace_validation", "floater_marketplace_validation.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterMarketplaceValidation>);
+ LLFloaterReg::add("maturity_dialog", "floater_maturity_dialog.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterMaturityDialog>);
LLFloaterReg::add("message_critical", "floater_critical.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterTOS>);
LLFloaterReg::add("message_tos", "floater_tos.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterTOS>);
LLFloaterReg::add("moveview", "floater_moveview.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterMove>);
diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp
index dde4897ab7..c4bd9e73ec 100644
--- a/indra/newview/llviewermessage.cpp
+++ b/indra/newview/llviewermessage.cpp
@@ -4950,10 +4950,12 @@ bool handle_teleport_access_blocked(LLSD& llsdBlock, const std::string & notific
{
U8 regionAccess = static_cast<U8>(llsdBlock["_region_access"].asInteger());
std::string regionMaturity = LLViewerRegion::accessToString(regionAccess);
+ llsdBlock["REGIONMATURITY_CAP"] = regionMaturity;
LLStringUtil::toLower(regionMaturity);
llsdBlock["REGIONMATURITY"] = regionMaturity;
LLNotificationPtr tp_failure_notification;
+ bool skip_notif = false;
std::string notifySuffix;
if (notificationID == std::string("TeleportEntryAccessBlocked"))
@@ -5023,21 +5025,39 @@ bool handle_teleport_access_blocked(LLSD& llsdBlock, const std::string & notific
}
} // End of special handling for "TeleportEntryAccessBlocked"
else
- { // Normal case, no message munging
- gAgent.clearTeleportRequest();
- if (LLNotifications::getInstance()->templateExists(notificationID))
+ {
+ if (notificationID == "RegionTPAccessBlocked")
{
- tp_failure_notification = LLNotificationsUtil::add(notificationID, llsdBlock, llsdBlock);
+ bool can_change_maturity = (regionAccess == SIM_ACCESS_MATURE) ? gAgent.isMature() : gAgent.isAdult();
+ if (can_change_maturity)
+ {
+ LLFloaterReg::showInstance("maturity_dialog", LLSD((S32)regionAccess));
+ skip_notif = true;
+ }
+ else
+ {
+ gAgent.clearTeleportRequest();
+ tp_failure_notification = LLNotificationsUtil::add("RegionTPAccessBlocked_NotifyAdultsOnly", llsdBlock);
+ }
}
else
{
- llsdBlock["MESSAGE"] = defaultMessage;
- tp_failure_notification = LLNotificationsUtil::add("GenericAlertOK", llsdBlock);
+ // Normal case, no message munging
+ gAgent.clearTeleportRequest();
+ if (LLNotifications::getInstance()->templateExists(notificationID))
+ {
+ tp_failure_notification = LLNotificationsUtil::add(notificationID, llsdBlock, llsdBlock);
+ }
+ else
+ {
+ llsdBlock["MESSAGE"] = defaultMessage;
+ tp_failure_notification = LLNotificationsUtil::add("GenericAlertOK", llsdBlock);
+ }
}
returnValue = true;
}
- if ((tp_failure_notification == NULL) || tp_failure_notification->isIgnored())
+ if (((tp_failure_notification == NULL) || tp_failure_notification->isIgnored()) && !skip_notif)
{
// Given a simple notification if no tp_failure_notification is set or it is ignore
LLNotificationsUtil::add(notificationID + notifySuffix, llsdBlock);
diff --git a/indra/newview/skins/default/xui/en/floater_maturity_dialog.xml b/indra/newview/skins/default/xui/en/floater_maturity_dialog.xml
new file mode 100644
index 0000000000..35769534ac
--- /dev/null
+++ b/indra/newview/skins/default/xui/en/floater_maturity_dialog.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<floater
+ height="185"
+ width="540"
+ layout="topleft"
+ name="floater_maturity_dialog"
+ title="MATURITY"
+ show_title="false"
+ header_height="0"
+ bg_opaque_image="Window_NoTitle_Foreground"
+ bg_alpha_image="Window_NoTitle_Background"
+ can_resize="false"
+ can_drag_on_left="false"
+ can_minimize="false"
+ single_instance="true"
+ can_close="false"
+ background_visible="false">
+ <floater.string
+ name="location_rated_string"
+ value="The location you've chosen is rated [MATURITY]."/>
+ <floater.string
+ name="update_maturity_string"
+ value="Your current maturity preference doesn't include [MATURITY] content. Allowing it will update your preference."/>
+ <floater.string
+ name="allow_maturity_string"
+ value="Allow [MATURITY] content"/>
+ <icon
+ color="FloaterFocusBackgroundColor"
+ follows="left|right|bottom|top"
+ image_name="Rounded_Square"
+ layout="topleft"
+ left="0"
+ top="0"
+ height="185"
+ width="540"/>
+ <text
+ type="string"
+ length="1"
+ follows="top|left|right"
+ font="SansSerifMediumBold"
+ text_color="White"
+ layout="topleft"
+ left="20"
+ height="18"
+ top="20"
+ right="-10"
+ name="location_rated_lbl">
+ The location you've chosen is rated Adult.
+ </text>
+ <text
+ type="string"
+ length="1"
+ follows="top|left|right"
+ text_color="White"
+ layout="topleft"
+ left="20"
+ height="32"
+ top_pad="10"
+ right="-10"
+ word_wrap="true"
+ name="current_maturity_lbl">
+ Your current maturity preference doesn't include Adult content. Allowing it will update your preference.
+ </text>
+
+ <button
+ follows="top|left"
+ layout="topleft"
+ height="28"
+ label="Allow content"
+ left="20"
+ top_pad="18"
+ name="continue_btn"
+ width="245" />
+
+ <button
+ follows="top|left"
+ layout="topleft"
+ height="28"
+ label="Cancel"
+ left_pad="10"
+ name="cancel_btn"
+ width="245" />
+
+ <text
+ type="string"
+ length="1"
+ follows="top|left|right"
+ text_color="White"
+ link_color="White"
+ layout="topleft"
+ left="20"
+ height="16"
+ top_pad="25"
+ right="-10"
+ parse_urls="true"
+ name="learn_more_lbl">
+[https://lindenlab.freshdesk.com/support/solutions/articles/31000135220-i-can-t-access-adult-land-and-content- Learn more] about maturity ratings.
+ </text>
+
+</floater>
diff --git a/indra/newview/skins/default/xui/en/notifications.xml b/indra/newview/skins/default/xui/en/notifications.xml
index 68a8712ee1..90cda572ba 100644
--- a/indra/newview/skins/default/xui/en/notifications.xml
+++ b/indra/newview/skins/default/xui/en/notifications.xml
@@ -5703,6 +5703,17 @@ You won't receive any more notifications that you're about to visit a region wit
<notification
icon="alertmodal.tga"
+ name="RegionTPAccessBlocked_NotifyAdultsOnly"
+ type="alertmodal">
+ <tag>fail</tag>
+ The region you are trying to visit is rated [REGIONMATURITY_CAP], which is not available on your account type.
+ <usetemplate
+ name="okbutton"
+ yestext="OK"/>
+ </notification>
+
+ <notification
+ icon="alertmodal.tga"
name="LandClaimAccessBlocked"
type="alertmodal">
The land you're trying to claim has a maturity rating exceeding your current preferences. You can change your preferences using Me &gt; Preferences &gt; General.