feat: open Explorer UI from SDK scene#9472
Conversation
SDK7 scenes open a native Explorer fullscreen panel via ~system/RestrictedActions and receive a typed OpenExplorerUiResult verdict (Opened / RejectedNotCurrentScene / RejectedAlreadyOpen / RejectedFeatureDisabled / RejectedNoUserGesture). - Protocol bindings regenerated (OpenExplorerUi RPC, ExplorerUi & OpenExplorerUiResult enums); also picks up experimental's additive AvatarEmoteCommand.Mask field. - Impl gates: current-scene, feature-flag, user-gesture window, already-open. - Gesture signal: LastUserInputTick on ISceneStateProvider, stamped by WritePointerEventResultsSystem, consumed by TryOpenExplorerUi. - IExplorerUiActions returns OpenSectionResult and tracks panel open-state via MVC events; disposal wired through the restricted-actions API. - Host RestrictedActions.js exposes the runtime enums; tests cover the gesture and already-open rejections. CONTEXT.md documents the open-verdict domain language. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Windows and Mac build successful in Unity Cloud! You can find a link to the downloadable artifact below. |
SDK7 scenes open a native Explorer fullscreen panel via ~system/RestrictedActions and receive a typed OpenExplorerUiResult verdict (Opened / RejectedNotCurrentScene / RejectedAlreadyOpen / RejectedFeatureDisabled / RejectedNoUserGesture). - Protocol bindings regenerated (OpenExplorerUi RPC, ExplorerUi & OpenExplorerUiResult enums); also picks up experimental's additive AvatarEmoteCommand.Mask field. - Impl gates: current-scene, feature-flag, user-gesture window, already-open. - Gesture signal: LastUserInputTick on ISceneStateProvider, stamped by WritePointerEventResultsSystem, consumed by TryOpenExplorerUi. - IExplorerUiActions returns OpenSectionResult and tracks panel open-state via MVC events; disposal wired through the restricted-actions API. - Host RestrictedActions.js exposes the runtime enums; tests cover the gesture and already-open rejections. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…' into feat/scene-triggered-explorer-ui
Regenerate RestrictedActions.gen.cs from protocol commit 0e7de00: ExplorerUi loses EU_UNSPECIFIED and renumbers contiguously with EU_SETTINGS as the zero value, and OpenExplorerUiResult renames REJECTED_ALREADY_OPEN to WAS_ALREADY_OPEN (value 2, no longer a rejection; rejected values shift to 3-5). Mirror both enums in the host JS module. Delete the OpenSectionResult enum and return the protobuf OpenExplorerUiResult from IExplorerUiActions.OpenSection, collapsing the TryOpenExplorerUi tail to a direct cast of the OpenSection verdict. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FeatureId.COMMUNITIES is never populated in FeaturesRegistry (its state is identity-dependent), so the synchronous registry gate in TryOpenExplorerUi rejected the section unconditionally, while the sidebar and ExplorePanel opened it fine via CommunitiesFeatureAccess. Move the Communities gate into ExplorerUiActions, backed by a new synchronous CommunitiesFeatureAccess.IsUserAllowedCached() snapshot of the async allowlist check (warmed at login; true in the editor, mirroring the async path). CAMERA_REEL and DISCOVER stay on the registry gate, which is populated correctly for them.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| }, | ||
| "dependencies": { | ||
| "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-29092627614.commit-d2f272f.tgz", | ||
| "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-30037355888.commit-0e7de00.tgz", |
There was a problem hiding this comment.
change it back when approved
decentraland-bot
left a comment
There was a problem hiding this comment.
STEP 2 — Root-cause check
Problem: SDK7 scenes have no way to open native Explorer panels (Settings, Map, Backpack, etc.) from user gestures.
Fix vs symptom: The diff adds a new openExplorerUi restricted action — this is the right approach. It extends the existing restricted-actions infrastructure with proper gating (current-scene, gesture window, feature flags, communities identity check). This addresses the cause directly.
Verdict: PASS ✅
STEP 3 — Design & integration
Owner search
New unit: ExplorerUiActions — manages whether the explore panel is open (via MVC events) and opens sections.
- Entity/resource managed: The explore panel's open/close state and the act of opening a section.
- Existing owners searched:
ExplorePanelController— the MVC controller that owns the panel lifecycle. Lives inDCL.Social.SidebarController— opens explore panel sections from the sidebar. Lives inDCL.Social.ExplorePanelPlugin— constructs theExplorePanelControllerand wires its dependencies.
- Can the logic live at those points? No. The restricted-actions API lives in
SceneRuntime, which cannot referenceDCL.Social(the dependency runs the other way). The asmref that compilesExplorerUiActionsintoDCL.Socialwhile keeping the file in theRestrictedActions/folder is the correct cross-assembly seam.
Teardown trace
| Opener | Mirror | Location |
|---|---|---|
mvcManager.OnViewShowed += OnViewShowed |
mvcManager.OnViewShowed -= OnViewShowed |
ExplorerUiActions ctor → Dispose() |
mvcManager.OnViewClosed += OnViewClosed |
mvcManager.OnViewClosed -= OnViewClosed |
ExplorerUiActions ctor → Dispose() |
Disposal chain: SceneRuntimeImpl.Dispose() → RestrictedActionsAPIWrapper.Dispose() (line 118) → RestrictedActionsAPIImplementation.Dispose() (line 242) → explorerUiActions.Dispose() (line 244) → unsubscribes MVC events. ✅
Verdict: PASS ✅
STEP 4 — Member audit
| New member | Consumers | Assessment |
|---|---|---|
CommunitiesFeatureAccess.IsUserAllowedCached() |
ExplorerUiActions.OpenSection (1) |
Legitimate accessor centralizing storedResult with editor override. Not a single-use derivation. |
ISceneStateProvider.LastUserInputTick |
WritePointerEventResultsSystem (writer), RestrictedActionsAPIImplementation.TryOpenExplorerUi (reader) |
Simple stamp property, 2 consumers. |
IExplorerUiActions.OpenSection() |
RestrictedActionsAPIImplementation.TryOpenExplorerUi (1) |
Interface boundary for cross-assembly seam. Mocked in tests. Justified. |
IRestrictedActionsAPI.TryOpenExplorerUi() |
RestrictedActionsAPIWrapper.OpenExplorerUi (1) |
Standard interface method following existing restricted-actions pattern. |
STEP 5 — Line-level review
3 findings (all P2). See inline comments.
Additional P2 — Unused constructor parameter (not on a changed line):
ExplorePanelController.cs:89 — The creditsPanelController field was removed but the constructor still accepts the parameter. It is no longer stored or used. Consider removing the parameter from the signature and its call site in ExplorePanelPlugin.cs (~line 620).
STEP 6 — Complexity assessment
COMPLEX — Adds a new restricted action with protocol bindings, cross-assembly asmref, MVC event subscriptions, gesture-window logic, and async patterns (UniTask + .Forget()).
STEP 7 — QA assessment
QA_REQUIRED: YES — Modifies runtime code that affects user-facing behavior: opening fullscreen panels from scene interactions, gesture-based gating, and feature-flag rejection.
STEP 8 — Non-blocking warnings
ExplorePanelController (creditsPanelController field, communitiesBrowserController private property, CanBeClosedByEscape override). The CanBeClosedByEscape removal is a behavioral change: previously, the panel could not be closed by Escape during the ViewShowing transition state; now the IController default applies (always closable for FULLSCREEN layer). This is likely fine (other fullscreen panels use the default), but confirm this is intentional — TC4 should validate no regression.
STEP 9 — Verdict
REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: New restricted action with protocol bindings, cross-assembly asmref, MVC event subscriptions, gesture-window logic, and async patterns.
QA_REQUIRED: YES
Security review
No security issues found. The gesture gate prevents scenes from opening panels without user interaction (1-tick window, underflow-safe). LastUserInputTick is only written by internal C# systems — scene JS cannot spoof it. The TryMapExplorerUi switch rejects unknown enum values. IsUserAllowedCached() defaults to false (fail-closed). uint overflow wraps to 0, which also fails closed.
Reviewed by Jarvis 🤖 · Requested by decentraland-bot via GitHub
| if (isExplorePanelOpen) | ||
| return OpenExplorerUiResult.WasAlreadyOpen; | ||
|
|
||
| OpenSectionAsync(section).Forget(); | ||
| return OpenExplorerUiResult.Opened; |
There was a problem hiding this comment.
[P2] Race in isExplorePanelOpen guard. Between the isExplorePanelOpen check (line 47) and OnViewShowed firing, a rapid second call passes the guard and queues a duplicate ShowAsync. Both return Opened to the scene. The MVC manager handles double-show gracefully (TC6 documents both verdicts as acceptable), so this isn't a functional bug, but setting the flag eagerly would make the WasAlreadyOpen verdict more accurate.
| if (isExplorePanelOpen) | |
| return OpenExplorerUiResult.WasAlreadyOpen; | |
| OpenSectionAsync(section).Forget(); | |
| return OpenExplorerUiResult.Opened; | |
| if (isExplorePanelOpen) | |
| return OpenExplorerUiResult.WasAlreadyOpen; | |
| isExplorePanelOpen = true; | |
| OpenSectionAsync(section).Forget(); | |
| return OpenExplorerUiResult.Opened; |
If ShowAsync fails before OnViewShowed fires, reset the flag in the catch blocks of OpenSectionAsync:
catch (OperationCanceledException) { isExplorePanelOpen = false; }
catch (Exception e)
{
isExplorePanelOpen = false;
ReportHub.LogException(e, ReportCategory.RESTRICTED_ACTIONS);
}|
|
||
| bool TryOpenNftDialog(string urn); | ||
|
|
||
| int TryOpenExplorerUi(int ui); |
There was a problem hiding this comment.
[P2] Naming: Try prefix implies bool return. By C# convention, TryX methods return bool (e.g. TryOpenExternalUrl on line 11). TryOpenExplorerUi returns int (a result code), breaking the convention. Since the caller always receives a verdict rather than success/failure, consider renaming to OpenExplorerUi (matching the JS bridge name and the protocol RPC name).
| int TryOpenExplorerUi(int ui); | |
| int OpenExplorerUi(int ui); |
(Would also require renaming in RestrictedActionsAPIImplementation.cs:212 and RestrictedActionsAPIWrapper.cs:99.)
|
Warnings not reduced: 14591 => 14592 — remove at least one warning to merge. Warnings/errors in files changed by this PR (81) |
|
Tests: 24288 passed, 1 failed Failed tests (1)
|
|
PR reviewed and approved by QA on both platforms following the PR test instructions. ✅ Build: Test results:
Unrelated errors noted (do not affect verdict):
Verdict: PASS ✅ All 7 test cases (TC1–TC7) pass on both Windows and Mac, across standard, fresh, flag-disabled, and gesture-edge-case runs, with no regressions or related errors found. Full click→verdict chain traced end-to-end from scene console + Player.log evidence. Scene console + Player.logs for windows and mac TC 1 24.07.2026_15.10.14_REC.9472.new.user.mp424.07.2026_15.03.48_REC.9472.old.user.mp4TC 3 24.07.2026_15.51.15_REC.TC.3-1.mp424.07.2026_15.31.06_REC.TC3.step.1.Mac.camera.reel.mp4TC 4 24.07.2026_15.53.10_REC.TC.4.Mac.OK.mp4TC 5-6-7 24.07.2026_15.56.19_REC.TC.5.mp424.07.2026_15.58.05_REC.TC.6.mp4 |
SDK7 scenes open a native Explorer fullscreen panel via
~system/RestrictedActionsand receive a typedOpenExplorerUiResultverdict (OPENED / WAS_ALREADY_OPEN / REJECTED_NOT_CURRENT_SCENE / REJECTED_FEATURE_DISABLED / REJECTED_NO_USER_GESTURE).OpenExplorerUiRPC,ExplorerUi&OpenExplorerUiResultenums); also picks up experimental's additiveAvatarEmoteCommand.Maskfield.LastUserInputTickonISceneStateProvider, stamped byWritePointerEventResultsSystem, consumed byTryOpenExplorerUi.IExplorerUiActionsreturns the open verdict and tracks panel open-state via MVC events; disposal wired through the restricted-actions API.CommunitiesFeatureAccess(identity-dependent flag + wallets allowlist — the same source the sidebar uses), not throughFeaturesRegistry, whereFeatureId.COMMUNITIESis never populated.RestrictedActions.jsexposes the runtime enums; unit tests cover the gesture, already-open, unknown-value, feature-disabled and communities-rejection-propagation paths.Pull Request Description
What does this PR change?
Adds the
openExplorerUirestricted action so SDK7 scenes can open native Explorer panels from a user gesture:ExplorerUivalueEU_SETTINGS(0)EU_MAP(1)EU_BACKPACK(2)EU_CAMERA_REEL(3)camera-reelfeature flagEU_COMMUNITIES(4)EU_PLACES(5)discoverfeature flagEU_EVENTS(6)discoverfeature flagEvery call returns a verdict to the scene:
OPENED(1),WAS_ALREADY_OPEN(2),REJECTED_NOT_CURRENT_SCENE(3),REJECTED_FEATURE_DISABLED(4),REJECTED_NO_USER_GESTURE(5).Test Instructions
The world
flutterecho.dcl.ethhosts a test scene with two rows of labeled clickable cubes in front of spawn:openExplorerUifor that panel.DELAYED 5s(calls after a countdown, outside the user-gesture window),DOUBLE CALL(two calls from one click),INVALID (99)(unknownuivalue). On scene load the scene also fires one automaticON-START(MAP)call with no gesture at all.The player spawns right in front of the cube rows. Every verdict is shown in an on-screen HUD (top-right under the scene-name badge, newest first with a scene-side timestamp: green
OPENED, yellowWAS_ALREADY_OPEN, red rejections) — log tailing is optional. The scene console additionally logs each step with the[openExplorerUi]prefix and a timestamp:click <NAME>,request <NAME> (ui=N)andverdict <NAME> -> <RESULT> (<code>), so the click→verdict chain is fully traceable from Explorer logs.Steps (standard run):
(Alternatively join any realm and use the chat command
/goto flutterecho.dcl.eth.)Expected result: see the test cases below — every cube opens its panel with
OPENED (1)in the logs.Steps (fresh account):
Expected result: same as the standard run — in particular the COMMUNITIES cube must behave exactly like the sidebar does for that account.
Prerequisites
metaforge explorer run 9472)Test Steps
TC1 — happy path, all 7 panels
MAP).OPENED (1).TC2 — Communities parity (bug fixed in this PR)
COMMUNITIEScube.OPENED (1). Before the fix this always returnedREJECTED_FEATURE_DISABLED (4)even for allowed users.TC3 — feature-flag rejection
metaforge explorer run 9472 -- --realm flutterecho.dcl.eth --debug --camera-reel falseCAMERA_REELcube: no panel opens, log showsREJECTED_FEATURE_DISABLED (4).--debug --discover false: thePLACESandEVENTScubes are rejected the same way;MAP/SETTINGS/BACKPACKstill open.TC4 — manual UI regression
dev(the gating source did not change, only the restricted action now consults it).TC5 — user-gesture gate
ON-START(MAP) -> REJECTED_NO_USER_GESTURE (5)— a call with no gesture ever recorded is rejected.DELAYED 5scube (row 2) and don't click anything else during the countdown shown above it: no panel opens, the HUD showsDELAYED(MAP) -> REJECTED_NO_USER_GESTURE (5).OPENED— that is not a failure, just retry without clicking.)TC6 — rapid clicks / double call
DOUBLE CALLcube (firesopenExplorerUitwice from one click): exactly one Map panel opens —MVCManagerignores a show command for a controller that is already showing.DOUBLE#1(MAP) -> OPENED (1)and, forDOUBLE#2(MAP), eitherOPENED (1)orWAS_ALREADY_OPEN (2)depending on whether the panel registered as shown between the two calls — both are acceptable; the failure mode would be a second panel or a stuck UI.TC7 — unknown ui value
INVALID (99)cube: no panel opens, the HUD showsINVALID(99) -> REJECTED_FEATURE_DISABLED (4).Additional Testing Notes
REJECTED_NOT_CURRENT_SCENEis the only verdict not drivable in the world (a world confines the player to its own scene). It is unit-tested; in a Genesis City deployment of the same scene it can be reproduced by clickingDELAYED 5sand walking to a neighbouring parcel during the countdown.ExplorerUinumbering and open the wrong (off-by-one) panel. The scene deployed toflutterecho.dcl.ethis up to date; if cubes open mismatched panels, suspect a stale scene deployment, not the client.--scene-console).sdk7-test-scenesat parcel80,-5(scenes/80,-5-open-explorer-ui) for local-scene-development runs.Quality Checklist
flutterecho.dcl.ethworld +sdk7-test-scenes/scenes/80,-5-open-explorer-ui🤖 Generated with Claude Code