Skip to content

feat: open Explorer UI from SDK scene - #9472

Merged
popuz merged 20 commits into
devfrom
feat/scene-triggered-explorer-ui
Jul 29, 2026
Merged

feat: open Explorer UI from SDK scene#9472
popuz merged 20 commits into
devfrom
feat/scene-triggered-explorer-ui

Conversation

@popuz

@popuz popuz commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

SDK7 scenes open a native Explorer fullscreen panel via ~system/RestrictedActions and receive a typed OpenExplorerUiResult verdict (OPENED / WAS_ALREADY_OPEN / REJECTED_NOT_CURRENT_SCENE / REJECTED_FEATURE_DISABLED / REJECTED_NO_USER_GESTURE).

  • 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 the open verdict and tracks panel open-state via MVC events; disposal wired through the restricted-actions API.
  • Communities is gated through CommunitiesFeatureAccess (identity-dependent flag + wallets allowlist — the same source the sidebar uses), not through FeaturesRegistry, where FeatureId.COMMUNITIES is never populated.
  • Host RestrictedActions.js exposes 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 openExplorerUi restricted action so SDK7 scenes can open native Explorer panels from a user gesture:

ExplorerUi value Panel Gate
EU_SETTINGS (0) Settings
EU_MAP (1) Map
EU_BACKPACK (2) Backpack
EU_CAMERA_REEL (3) Camera Reel camera-reel feature flag
EU_COMMUNITIES (4) Communities Communities feature flag + wallets allowlist (same as sidebar)
EU_PLACES (5) Places discover feature flag
EU_EVENTS (6) Events discover feature flag

Every 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.eth hosts a test scene with two rows of labeled clickable cubes in front of spawn:

  • Row 1 (closer) — 7 cubes, one per panel: each click calls openExplorerUi for that panel.
  • Row 2 (further) — gate-provoking cubes: DELAYED 5s (calls after a countdown, outside the user-gesture window), DOUBLE CALL (two calls from one click), INVALID (99) (unknown ui value). On scene load the scene also fires one automatic ON-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, yellow WAS_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) and verdict <NAME> -> <RESULT> (<code>), so the click→verdict chain is fully traceable from Explorer logs.

Steps (standard run):

metaforge explorer run 9472 -- --realm flutterecho.dcl.eth
metaforge explorer logs tail --filter "openExplorerUi"   # in a second terminal

(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):

metaforge account create --clear
metaforge explorer run 9472 -- --realm flutterecho.dcl.eth

Expected result: same as the standard run — in particular the COMMUNITIES cube must behave exactly like the sidebar does for that account.

Prerequisites

  • Explorer build from this PR (metaforge explorer run 9472)
  • Account able to open Communities from the sidebar (for TC2)

Test Steps

TC1 — happy path, all 7 panels

  1. Stand at spawn, face the cube row, click a cube (e.g. MAP).
  2. The matching panel opens fullscreen; the log shows OPENED (1).
  3. Close the panel (Esc) and repeat for each of the 7 cubes.
  4. Each opened panel must look and behave identically to opening the same section manually from the sidebar.

TC2 — Communities parity (bug fixed in this PR)

  1. Verify Communities opens from the sidebar for this account.
  2. Click the COMMUNITIES cube.
  3. Communities opens with OPENED (1). Before the fix this always returned REJECTED_FEATURE_DISABLED (4) even for allowed users.

TC3 — feature-flag rejection

  1. Relaunch: metaforge explorer run 9472 -- --realm flutterecho.dcl.eth --debug --camera-reel false
  2. Click the CAMERA_REEL cube: no panel opens, log shows REJECTED_FEATURE_DISABLED (4).
  3. Relaunch with --debug --discover false: the PLACES and EVENTS cubes are rejected the same way; MAP / SETTINGS / BACKPACK still open.

TC4 — manual UI regression

  1. Open and close every section manually (sidebar, map hotkey) before and after cube-triggered opens — no stuck panels, no double-opens.
  2. Sidebar visibility of Communities is unchanged vs dev (the gating source did not change, only the restricted action now consults it).

TC5 — user-gesture gate

  1. On scene load, before clicking anything, the HUD already shows ON-START(MAP) -> REJECTED_NO_USER_GESTURE (5) — a call with no gesture ever recorded is rejected.
  2. Click the DELAYED 5s cube (row 2) and don't click anything else during the countdown shown above it: no panel opens, the HUD shows DELAYED(MAP) -> REJECTED_NO_USER_GESTURE (5).
  3. (Any click during the countdown refreshes the gesture window and can legitimately turn the verdict into OPENED — that is not a failure, just retry without clicking.)

TC6 — rapid clicks / double call

  1. Click the DOUBLE CALL cube (fires openExplorerUi twice from one click): exactly one Map panel opens — MVCManager ignores a show command for a controller that is already showing.
  2. The HUD shows two verdicts: DOUBLE#1(MAP) -> OPENED (1) and, for DOUBLE#2(MAP), either OPENED (1) or WAS_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

  1. Click the INVALID (99) cube: no panel opens, the HUD shows INVALID(99) -> REJECTED_FEATURE_DISABLED (4).

Additional Testing Notes

  • REJECTED_NOT_CURRENT_SCENE is 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 clicking DELAYED 5s and walking to a neighbouring parcel during the countdown.
  • The scene must be built with the branch SDK from CI run ≥ 30037787217: earlier builds used the pre-reorder ExplorerUi numbering and open the wrong (off-by-one) panel. The scene deployed to flutterecho.dcl.eth is up to date; if cubes open mismatched panels, suspect a stale scene deployment, not the client.
  • Scene logs are also visible in the in-app scene console (--scene-console).
  • The same showcase exists in sdk7-test-scenes at parcel 80,-5 (scenes/80,-5-open-explorer-ui) for local-scene-development runs.

Quality Checklist

  • Changes have been tested locally
  • Documentation has been updated (if required)
  • Performance impact has been considered — the gesture stamp is a single uint write per input event; no per-frame work added
  • For SDK features: Test scene is included — flutterecho.dcl.eth world + sdk7-test-scenes/scenes/80,-5-open-explorer-ui

🤖 Generated with Claude Code

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>
@popuz popuz self-assigned this Jul 23, 2026
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

popuz and others added 10 commits July 23, 2026 19:42
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>
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>
@popuz
popuz marked this pull request as ready for review July 24, 2026 11:44
@popuz
popuz requested review from a team as code owners July 24, 2026 11:44
@decentraland-bot
decentraland-bot self-requested a review July 24, 2026 11:44
@popuz popuz changed the title feat: add openExplorerUi restricted action feat: open Explorer UI by the SDK scene Jul 24, 2026
@popuz popuz changed the title feat: open Explorer UI by the SDK scene feat: open Explorer UI from SDK scene Jul 24, 2026
Comment thread scripts/package.json Outdated

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

  1. Entity/resource managed: The explore panel's open/close state and the act of opening a section.
  2. Existing owners searched:
    • ExplorePanelController — the MVC controller that owns the panel lifecycle. Lives in DCL.Social.
    • SidebarController — opens explore panel sections from the sidebar. Lives in DCL.Social.
    • ExplorePanelPlugin — constructs the ExplorePanelController and wires its dependencies.
  3. Can the logic live at those points? No. The restricted-actions API lives in SceneRuntime, which cannot reference DCL.Social (the dependency runs the other way). The asmref that compiles ExplorerUiActions into DCL.Social while keeping the file in the RestrictedActions/ 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

⚠️ Unrelated cleanup included: The PR removes three members from 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

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

badge

Warnings count reduced: 14087 => 14078

Warnings/errors in files changed by this PR (91)
Assets/DCL/TeleportPrompt/TeleportPromptController.cs:42  CSharpWarnings::CS8602  Dereference of a possibly null reference
Assets/DCL/TeleportPrompt/TeleportPromptController.cs:71  CSharpWarnings::CS8602  Dereference of a possibly null reference
Assets/DCL/TeleportPrompt/TeleportPromptController.cs:92  CSharpWarnings::CS8602  Dereference of a possibly null reference
Assets/DCL/TeleportPrompt/TeleportPromptController.cs:92  CSharpWarnings::CS8602  Dereference of a possibly null reference
Assets/DCL/TeleportPrompt/TeleportPromptController.cs:114  CSharpWarnings::CS8602  Dereference of a possibly null reference
Assets/DCL/TeleportPrompt/TeleportPromptController.cs:115  CSharpWarnings::CS8602  Dereference of a possibly null reference
Assets/DCL/TeleportPrompt/TeleportPromptController.cs:123  CSharpWarnings::CS8602  Dereference of a possibly null reference
Assets/DCL/TeleportPrompt/TeleportPromptController.cs:130  CSharpWarnings::CS8602  Dereference of a possibly null reference
Assets/DCL/Infrastructure/SceneRunner/Tests/SceneFacadeShould.cs:136  CSharpWarnings::CS8618  Non-nullable field 'engineFactory' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs:34  CSharpWarnings::CS8618  Non-nullable field 'explorerUiActions' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs:31  CSharpWarnings::CS8618  Non-nullable field 'globalWorldActions' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs:29  CSharpWarnings::CS8618  Non-nullable field 'mvcManager' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/Infrastructure/SceneRunner/Tests/SceneFacadeShould.cs:148  CSharpWarnings::CS8618  Non-nullable field 'path' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs:87  CSharpWarnings::CS8618  Non-nullable field 'permissionsProvider' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs:28  CSharpWarnings::CS8618  Non-nullable field 'restrictedActionsAPIImplementation' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs:32  CSharpWarnings::CS8618  Non-nullable field 'sceneData' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs:30  CSharpWarnings::CS8618  Non-nullable field 'sceneStateProvider' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs:35  CSharpWarnings::CS8618  Non-nullable field 'sceneWorld' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs:33  CSharpWarnings::CS8618  Non-nullable field 'systemClipboard' is uninitialized. Consider adding the 'required' modifier or declaring the field as nullable.
Assets/DCL/TeleportPrompt/TeleportPromptController.cs:26  CSharpWarnings::CS8618  Non-nullable fields 'resultCallback', 'cts' must contain non-null values when exiting constructor. Consider adding the 'required' modifiers or declaring the fields as nullable.
Assets/DCL/ExplorePanel/ExplorePanelController.cs:75  CSharpWarnings::CS8618  Non-nullable fields 'tabsBySections', 'exploreSections', 'sectionSelectorController', 'setupExploreSectionsCts', 'checkForLiveEventsCts' must contain non-null values when exiting constructor. Consider adding the 'required' modifiers or declaring the fields as nullable.
Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs:741  CSharpWarnings::CS8618  Non-nullable property 'EventInfoPrefab' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs:739  CSharpWarnings::CS8618  Non-nullable property 'PlaceCategoriesSO' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs:740  CSharpWarnings::CS8618  Non-nullable property 'PlaceDetailPanelPrefab' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs:742  CSharpWarnings::CS8618  Non-nullable property 'QualityPresets' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs:727  CSharpWarnings::CS8618  Non-nullable property 'pointAtMarkerVisibilitySettings' is uninitialized. Consider adding the 'required' modifier or declaring the property as nullable.
Assets/DCL/ExplorePanel/ExplorePanelController.cs:205  ConditionalAccessQualifierIsNonNullableAccordingToAPIContract  Conditional access qualifier expression is never null according to nullable reference types' annotations
Assets/DCL/ExplorePanel/ExplorePanelController.cs:205  ConditionalAccessQualifierIsNonNullableAccordingToAPIContract  Conditional access qualifier expression is never null according to nullable reference types' annotations
Assets/DCL/ExplorePanel/ExplorePanelController.cs:205  ConditionalAccessQualifierIsNonNullableAccordingToAPIContract  Conditional access qualifier expression is never null according to nullable reference types' annotations
Assets/DCL/TeleportPrompt/TeleportPromptController.cs:83  ConditionalAccessQualifierIsNonNullableAccordingToAPIContract  Conditional access qualifier expression is never null according to nullable reference types' annotations
Assets/DCL/TeleportPrompt/TeleportPromptController.cs:86  ConditionalAccessQualifierIsNonNullableAccordingToAPIContract  Conditional access qualifier expression is never null according to nullable reference types' annotations
Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs:196  InconsistentNaming  Name 'CommunicationsControllerAPI' does not match rule 'members_should_be_pascal_case'. Suggested name is 'CommunicationsControllerApi'.
Assets/Protocol/DecentralandProtocol/ComponentID.gen.cs:4  InconsistentNaming  Name 'ComponentID' does not match rule 'members_should_be_pascal_case'. Suggested name is 'ComponentId'.
Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs:201  InconsistentNaming  Name 'EngineAPI' does not match rule 'members_should_be_pascal_case'. Suggested name is 'EngineApi'.
Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs:741  InconsistentNaming  Name 'EventInfoPrefab' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'eventInfoPrefab'.
Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/IRestrictedActionsAPI.cs:9  InconsistentNaming  Name 'IRestrictedActionsAPI' does not match rule 'interfaces_should_be_pascal_case_with_i_prefix'. Suggested name is 'IRestrictedActionsApi'.
Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs:740  InconsistentNaming  Name 'PlaceDetailPanelPrefab' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'placeDetailPanelPrefab'.
Assets/DCL/PluginSystem/Global/ExplorePanelPlugin.cs:742  InconsistentNaming  Name 'QualityPresets' does not match rule 'non_public_members_should_be_camel_case'. Suggested name is 'qualityPresets'.
Assets/DCL/Infrastructure/SceneRuntime/ISceneRuntime.cs:149  InconsistentNaming  Name 'RegisterEngineAPI' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RegisterEngineApi'.
Assets/DCL/Infrastructure/SceneRuntime/ISceneRuntime.cs:156  InconsistentNaming  Name 'RegisterEngineAPI' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RegisterEngineApi'.
Assets/DCL/Infrastructure/SceneRuntime/ISceneRuntime.cs:61  InconsistentNaming  Name 'RegisterEngineAPIWrapper' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RegisterEngineApiWrapper'.
Assets/DCL/Infrastructure/SceneRuntime/ISceneRuntime.cs:230  InconsistentNaming  Name 'RegisterSDKMessageBusCommsApi' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RegisterSdkMessageBusCommsApi'.
Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs:192  InconsistentNaming  Name 'RestrictedActionsAPI' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RestrictedActionsApi'.
Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs:27  InconsistentNaming  Name 'RestrictedActionsAPIImplementation' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RestrictedActionsApiImplementation'.
Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs:26  InconsistentNaming  Name 'RestrictedActionsAPIImplementationShould' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RestrictedActionsApiImplementationShould'.
Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/RestrictedActionsAPIWrapper.cs:12  InconsistentNaming  Name 'RestrictedActionsAPIWrapper' does not match rule 'members_should_be_pascal_case'. Suggested name is 'RestrictedActionsApiWrapper'.
Assets/DCL/Infrastructure/SceneRunner/Tests/SceneFacadeShould.cs:401  InconsistentNaming  Name 'TestAPIWrapper' does not match rule 'members_should_be_pascal_case'. Suggested name is 'TestApiWrapper'.
Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs:270  InconsistentNaming  Name 'WithRuntimeAndJsAPI' does not match rule 'members_should_be_pascal_case'. Suggested name is 'WithRuntimeAndJsApi'.
Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs:190  InconsistentNaming  Name 'WithRuntimeAndJsAPIBase' does not match rule 'members_should_be_pascal_case'. Suggested name is 'WithRuntimeAndJsApiBase'.
Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs:298  InconsistentNaming  Name 'WithRuntimeJsAndSDKObservablesEngineAPI' does not match rule 'members_should_be_pascal_case'. Suggested name is 'WithRuntimeJsAndSdkObservablesEngineApi'.

…and 41 more (see the csharp-lint-reports artifact).

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Tests: 24410 passed, 0 failed

@balducciv

Copy link
Copy Markdown

PR reviewed and approved by QA on both platforms following the PR test instructions. ✅
Smoke test performed on Windows and Mac (standard account + fresh account) to verify the normal flow is working as expected. ✅

Build: feat/scene-triggered-explorer-ui @ 8d16d00e (PR #9472 HEAD)
OS: Windows 11 (Katana 15 B12VFK) / macOS — Apple M3 Pro (Mac15,6)

Test results:

  • TC1 — All 7 panels open with OPENED (1) from the cube row — confirmed on both platforms, both accounts
  • TC2 — Communities parity: COMMUNITIES cube returns OPENED (1) on all runs — the previous REJECTED_FEATURE_DISABLED (4) bug does not reproduce
  • TC3 — Feature-flag rejection, confirmed on both platforms:
    • --camera-reel false: CAMERA_REELREJECTED_FEATURE_DISABLED (4) (Mac + Windows)
    • --discover false: PLACES / EVENTSREJECTED_FEATURE_DISABLED (4), other panels unaffected (Mac + Windows)
  • TC4 — Manual UI regression: all 7 panels open cleanly with no stuck state on a standard relaunch, on both platforms
  • TC5 — Gesture gate: ON-START(MAP) -> REJECTED_NO_USER_GESTURE (5) on scene load with no prior click, on all runs. DELAYED cube correctly rejects when no click occurs during the countdown (5 separate confirmations on Mac + Windows); one case where the actual MAP cube was clicked mid-countdown produced a legitimate OPENED (1) on the subsequent MAP click — consistent with the documented gesture-refresh behavior
  • TC6 — Double call: DOUBLE#1(MAP) / DOUBLE#2(MAP) both return OPENED (1) on every run (never WAS_ALREADY_OPEN), with only one Map panel visibly opening — consistent with the eager-flag race already flagged as a non-blocking P2 in code review, not a new issue
  • TC7 — INVALID (99) cube returns REJECTED_FEATURE_DISABLED (4) on every occurrence, across all runs (multiple repeats on Mac + Windows)

Unrelated errors noted (do not affect verdict):

  • Curl error 42 / Curl error 23 — scene-transition callback aborts, pre-existing noise
  • EventsApiException: Error fetching events (one Windows run) — from SidebarController.FillLiveEventsAsync, unrelated to the openExplorerUi action
  • livekit::rtc_engine reconnection errors (one Mac run) — Pulse/multiplayer transport noise around scene transitions, unrelated to this fix
  • DisposableUtils.SafeDispose / DynamicWorldContainer disposal exceptions — all occur after [ExitUtils] Exit requested, standard shutdown-sequence noise
  • No new runtime errors in any of the TC3–TC7 logs on either platform

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
log 9472 windows new user.txt
log 9472 windows old user.txt
Player 9472 new user windows.log
Player 9472 old user windows.log
Scene console - 9472 Mac.rtf
scene console new acc 9472 Mac.rtf

24.07.2026_15.10.14_REC.9472.new.user.mp4
24.07.2026_15.03.48_REC.9472.old.user.mp4

TC 3
log 9472 windows step 3-3.txt
log 9472 windows step 3.txt
Player 9472 TC3-1 Mac.log
Player 9473 step 3-1.log
Player 9473 step 3-3 windows.log
TC-3-1 mac.rtf
TC3 - Mac - Camera Reel step 1.rtf

24.07.2026_15.51.15_REC.TC.3-1.mp4
24.07.2026_15.31.06_REC.TC3.step.1.Mac.camera.reel.mp4

TC 4
Player 9472 TC 4 Mac.log
Player TC4 Windows.log
TC-4 OK Mac.rtf

24.07.2026_15.53.10_REC.TC.4.Mac.OK.mp4

TC 5-6-7
log 9472 windows step 5-6-7.txt
Player 5-6-7 windows.log
Player TC-5-6-7 Mac.log
TC-5 Mac.rtf
TC-5-6-7 Mac.rtf

24.07.2026_15.56.19_REC.TC.5.mp4
24.07.2026_15.58.05_REC.TC.6.mp4

popuz and others added 2 commits July 27, 2026 12:36

@dalkia dalkia left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved! Some minor comments for consideration

Comment thread Explorer/Assets/DCL/Communities/CommunitiesFeatureAccess.cs Outdated
Review follow-up: IsUserAllowedCached() stayed false until another
feature flow had run the allowlist check at least once (e.g. the user
opening Communities from the sidebar), so openExplorerUi(COMMUNITIES)
was rejected for allowed users on a fresh session.

- CommunitiesFeatureAccess resolves the allowlist-aware check in the
  background from its constructor, so the cached verdict no longer
  depends on another flow having run the check first; the cache still
  resets on identity change
- split the two predicates crammed into
  IsUserAllowedToUseTheFeatureAsync(ct, ignoreAllowedList, cacheResult):
  a sync flag-only IsFeatureEnabled() for pre-login bootstrap paths and
  the allowlist-aware IsUserAllowedToUseTheFeatureAsync(ct) that always
  caches - flag-only callers can no longer wipe or poison the cache
- CommunitiesContainer.CreateAsync became sync Create (its only await
  is gone)
- consolidated the triplicated editor force-enable hack into a single
  ForceEnabledInEditor property (one #if, aggressively inlined)
- removed dead storedMembersCounterResult/storedAnnouncementsResult
  fields and the unused UnityEngine using

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@pravusjif pravusjif left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! just make sure to update the protocol befor merging (needs protocol change in main -> synched into experimental).

And ideally it would be good to have a test scene for this feature at sdk7testscenes, can you create a new one there with the same test code you have in flutterecho.dcl.eth? so that QA can still test the feature there while doing the RC candidate testing and you can keep re-deploying on your world safely.

popuz added a commit to decentraland/protocol that referenced this pull request Jul 28, 2026
…iResult) (#451)

## What

Adds an `OpenExplorerUi` RPC to the existing `RestrictedActionsService` so SDK7 scenes can open native explorer UI panels (Map, Settings, Backpack, Camera Reel, Communities, Places, Events) through `~system/RestrictedActions`.

- New enum `ExplorerUi` — 7 fullscreen sections; `EU_SETTINGS` holds the zero value so an unset `ui` field defaults to the least-intrusive panel.
- New enum `OpenExplorerUiResult` — the open verdict: `UNSPECIFIED`, `OPENED`, `WAS_ALREADY_OPEN`, `REJECTED_NOT_CURRENT_SCENE`, `REJECTED_FEATURE_DISABLED`, `REJECTED_NO_USER_GESTURE`.
- New messages `OpenExplorerUiRequest { ExplorerUi ui }` / `OpenExplorerUiResponse { OpenExplorerUiResult open_result }` (dedicated response, not the shared `SuccessResponse`; the request documents a `oneof` extension point for future per-panel params).
- `rpc OpenExplorerUi(OpenExplorerUiRequest) returns (OpenExplorerUiResponse)` on `RestrictedActionsService`.

Additive and non-breaking. Only `proto/decentraland/kernel/apis/restricted_actions.proto` changed.

## Context

Iteration 1 of the Scene-triggered Explorer UI feature. This is the **review PR into `main`** per the protocol contribution flow; #446 carries the exact same change on `experimental` for Unity testing and will be closed once the main→experimental sync lands.

## Testing

Consumed downstream by js-sdk-toolchain (decentraland/js-sdk-toolchain#1485, `~system/RestrictedActions` codegen) and unity-explorer (decentraland/unity-explorer#9472, C# implementation with QA test scene) on matching `feat/scene-triggered-explorer-ui` branches.
popuz added a commit to decentraland/js-sdk-toolchain that referenced this pull request Jul 28, 2026
## What
Bumps `@dcl/protocol` to the **main** build `1.0.0-30373157983.commit-2c475cb` ([decentraland/protocol#451](decentraland/protocol#451), merged) so `~system/RestrictedActions` regenerates with:
- `openExplorerUi(body: OpenExplorerUiRequest): Promise<OpenExplorerUiResponse>`
- `ExplorerUi` enum (7 sections: `EU_MAP`, `EU_SETTINGS`, `EU_BACKPACK`, `EU_CAMERA_REEL`, `EU_COMMUNITIES`, `EU_PLACES`, `EU_EVENTS`)
- `OpenExplorerUiResult` enum + `OpenExplorerUiResponse`

Main-based counterpart of #1485 (experimental, pinned a protocol branch build): once this lands and main syncs into experimental, #1485 will be closed.

## Why
Lets SDK7 scenes open native Explorer fullscreen panels from a user gesture and receive a typed verdict. Client implementation: [decentraland/unity-explorer#9472](decentraland/unity-explorer#9472). Test world: `flutterecho.dcl.eth` + `sdk7-test-scenes` parcel `80,-5`.
@popuz
popuz requested a review from decentraland-bot July 29, 2026 10:04

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: feat: open Explorer UI from SDK scene

STEP 1 — Context & Scope

This PR adds a new openExplorerUi restricted action that lets SDK7 scenes request native Explorer panels from a user gesture. It touches:

  • Protocol bindings (RestrictedActions.gen.cs, AvatarEmoteCommand.gen.cs) — generated code, not reviewed for style.
  • Scene runtime API (IRestrictedActionsAPI, RestrictedActionsAPIWrapper, RestrictedActions.js).
  • New IExplorerUiActions / ExplorerUiActions bridge.
  • CommunitiesFeatureAccess refactoring (sync methods, background warm-up).
  • Gesture stamping in WritePointerEventResultsSystem.
  • ExplorePanelController dead-code cleanup.
  • Test coverage for both CommunitiesFeatureAccess and RestrictedActionsAPIImplementation.

Subsystem docs loaded: CLAUDE.md.


STEP 2 — Root-cause check

Problem: SDK7 scenes had no way to open native Explorer panels. Additionally, Communities access was gated through FeaturesRegistry where FeatureId.COMMUNITIES is never populated, causing false rejections.

Fix: A new TryOpenExplorerUi method in RestrictedActionsAPIImplementation with a proper gate chain. Communities is now gated through CommunitiesFeatureAccess (identity-dependent flag + wallets allowlist), matching what the sidebar uses.

Verdict: ✅ This addresses the root cause, not a symptom.


STEP 3 — Design & integration

Owner search — ExplorerUiActions:

  • Entity managed: Explore panel open/close state for the SDK bridge.
  • Existing owners searched: ExplorePanelController owns the panel lifecycle; MVCManager owns show/close; SceneInstanceDeps owns per-scene runtime dependencies.
  • ExplorerUiActions bridges two assembly layers: the restricted-actions API (SceneRuntime) needs to open ExplorePanelController (DCL.Social). The asmref (RestrictedActions.ExplorerUi.asmref → DCL.Social) is the established pattern for cross-assembly bridges in this codebase. The interface IExplorerUiActions stays in SceneRuntime and is mocked in tests — it's a genuine test seam, not a single-impl anti-pattern.
  • The class does NOT duplicate an existing lifecycle: no existing code provides "open a specific explore-panel section and return a typed verdict" from the scene runtime layer.

Owner search — LastUserInputTick:

  • Stamped by WritePointerEventResultsSystem (the existing system that processes pointer events for the scene). This is the natural owner — the tick stamp is a one-uint-write side-effect of the existing non-hover filter. No new system introduced.
  • Read by TryOpenExplorerUi in RestrictedActionsAPIImplementation — the consumer is in the right place (the restricted-actions gate chain).

Teardown / subscription trace:

  • ExplorerUiActions subscribes mvcManager.OnViewShowed += OnViewShowed and mvcManager.OnViewClosed += OnViewClosed — both unsubscribed in Dispose() (lines 33–34).
  • ✅ Disposal chain verified: RestrictedActionsAPIWrapper.Dispose()api.Dispose()RestrictedActionsAPIImplementation.Dispose()explorerUiActions.Dispose().
  • CommunitiesFeatureAccess.OnIdentityChanged subscription (pre-existing, not introduced by this PR) — [Singleton] lifetime, acceptable.

CommunitiesFeatureAccess.CreateAsyncCreate: The async path was awaiting IsUserAllowedToUseTheFeatureAsync which blocks container creation until the identity is available. The new design fires the allowlist check in the background via .SuppressToResultAsync().Forget() and provides synchronous access via IsFeatureEnabled() and IsUserAllowedCached(). This is correct — the container no longer blocks startup, and the cached result is available before any scene can call the restricted action (the warm-up runs in the constructor, well before scene load).

Verdict: ✅ PASS — no duplicate lifecycle, no per-frame reconciliation, proper teardown.


STEP 4 — Member audit

Member Consumers Verdict
IExplorerUiActions.OpenSection RestrictedActionsAPIImplementation.TryOpenExplorerUi, test mock ✅ 2 consumers (impl + mock), justified interface
CommunitiesFeatureAccess.IsFeatureEnabled() CommunitiesContainer.Create, ExplorePanelPlugin.InitializeAsync ✅ 2 consumers — flag-only sync check for bootstrap paths
CommunitiesFeatureAccess.IsUserAllowedCached() ExplorerUiActions.OpenSection, tests ✅ Sync counterpart of async check, returns cached result or safe default (false)
ISceneStateProvider.LastUserInputTick WritePointerEventResultsSystem (write), RestrictedActionsAPIImplementation.TryOpenExplorerUi (read), SceneStateProvider.Start (reset) ✅ Clear write/read/reset lifecycle
ExplorerUiActions.isExplorePanelOpen OpenSection (read), OnViewShowed/OnViewClosed (write) ✅ Private field, 3 internal consumers
RestrictedActionsAPIImplementation.TryMapExplorerUi TryOpenExplorerUi ✅ Private static, 1 consumer — extracted for readability

No single-use derived predicates, no absent-≠-false conflation, no re-derivation of existing properties.


STEP 5 — Line-level review

Pass A — Blocking issues: None found.

Pass B — Design/encapsulation smells: None found.

Security review: Completed via sub-agent. No vulnerabilities. The gate chain (current-scene → gesture-window → enum-validation → feature-flag → communities-allowlist → already-open) is defense-in-depth. The JS bridge passes only an int in and returns only an int — no injection surface. IsUserAllowedCached() defaults to false (deny) when the warm-up hasn't resolved — safe by default.


STEP 6 — Complexity

COMPLEX — touches restricted actions API, scene runtime, gesture stamping in an ECS system, feature flags, MVC integration, assembly definitions, protocol bindings, and the Communities feature-access singleton.


STEP 7 — QA assessment

QA_REQUIRED: YES — runtime code changes affecting user-facing UI opening behavior; new SDK action that scenes can invoke; comprehensive test plan provided with world flutterecho.dcl.eth and 7 test cases.


STEP 8 — Non-blocking warnings

None. Main.unity not modified.


STEP 9 — Verdict

REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Touches restricted-actions API, scene runtime, gesture stamping in an ECS system, MVC integration, feature flags, assembly definitions, and protocol bindings.
QA_REQUIRED: YES


Reviewed by Jarvis 🤖 · Requested by popuz via GitHub

@popuz
popuz merged commit 8e1d2b6 into dev Jul 29, 2026
27 of 28 checks passed
@popuz
popuz deleted the feat/scene-triggered-explorer-ui branch July 29, 2026 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants