From ff1d39c652e3f8e686b279012203749743cf84ae Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 23 Jul 2026 19:38:28 +0200 Subject: [PATCH 1/9] feat: add openExplorerUi restricted action 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 --- CONTEXT.md | 47 ++ .../RestrictedActions/IExplorerUiActions.cs | 19 + .../IExplorerUiActions.cs.meta | 2 + .../RestrictedActionsAPIImplementation.cs | 95 +++- ...estrictedActionsAPIImplementationShould.cs | 88 +++- .../SceneRunner/ExplorerUiActions.cs | 60 +++ .../SceneRunner/ExplorerUiActions.cs.meta | 2 + .../SceneRunner/Scene/ISceneStateProvider.cs | 7 + .../SceneRunner/Scene/SceneStateProvider.cs | 3 + .../SceneRunner/SceneInstanceDeps.cs | 5 +- .../SceneRunner/Tests/SceneFacadeShould.cs | 2 +- .../IRestrictedActionsAPI.cs | 7 +- .../RestrictedActionsAPIWrapper.cs | 11 +- .../Modules/UserActions/UserActionsWrapper.cs | 2 +- .../SceneRuntime/ISceneRuntime.cs | 2 +- .../Systems/WritePointerEventResultsSystem.cs | 11 +- .../AvatarEmoteCommand.gen.cs | 69 ++- .../RestrictedActions.gen.cs | 468 +++++++++++++++++- .../Js/Modules/RestrictedActions.js | 35 ++ .../DCL.EditMode.Tests.csproj.DotSettings | 1 + Explorer/SceneRuntime.csproj.DotSettings | 1 + scripts/package-lock.json | 12 +- scripts/package.json | 2 +- 23 files changed, 921 insertions(+), 30 deletions(-) create mode 100644 CONTEXT.md create mode 100644 Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs.meta diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 00000000000..d84527419d3 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,47 @@ +# Feature context — Scene-triggered Explorer UI (`openExplorerUi`) + +> NOTE: This file was reconstructed after the original working copy was lost. It +> captures the authoritative domain language for the feature; please review and +> refine the wording if the original had additional nuance. + +## What the feature is + +A restricted action `openExplorerUi` that lets an SDK7 scene ask the Explorer to +open a native fullscreen panel (Map / Settings / Backpack / CameraReel / +Communities / Places / Events) via `~system/RestrictedActions`, returning a typed +verdict. Iteration 1 = open + verdict only (no event channel, no params). + +## Domain language (authoritative — use these terms) + +- **open result / verdict** — the outcome of an `openExplorerUi` call is an + **open result verdict**, modeled as the enum `OpenExplorerUiResult`, never a + `bool success`. New outcomes are expressed by adding enum values, not booleans. +- **`OpenExplorerUiResult`** values (protocol bare names; C# members are the + PascalCase forms; host-JS runtime keys mirror the bare names): + - `UNSPECIFIED` (0) — default / unset. + - `OPENED` (1) — the panel was opened. + - `REJECTED_NOT_CURRENT_SCENE` (2) — the standard restricted-actions + current-scene gate rejected the call. + - `REJECTED_ALREADY_OPEN` (3) — a fullscreen panel is already open (also covers + repeat / rapid re-invocation). Section-switching on an already-open panel is + out of scope for now. + - `REJECTED_FEATURE_DISABLED` (4) — the requested section is hidden by feature + flags, or the requested `ExplorerUi` value is unsupported. + - `REJECTED_NO_USER_GESTURE` (5) — the call did not originate from a user + gesture (see gesture rule below). +- **`ExplorerUi`** (`EU_*`) — identifies which fullscreen panel to target. +- **gesture rule ("once per frame")** — a call is honored only if a user pointer + input happened in the current or immediately preceding scene tick. The window + is a hardcoded constant, not a feature flag. + +## Layering (runtime data flow) + +``` +scene src/index.ts → ~system/RestrictedActions (host module, provided by Explorer) + → Explorer host JS StreamingAssets/Js/Modules/RestrictedActions.js + → C# RestrictedActionsAPIWrapper.OpenExplorerUi(int) + → RestrictedActionsAPIImplementation.TryOpenExplorerUi(int) → gates → verdict +``` + +`~system/*` are HOST modules (runtime values provided by the Explorer), so the +host JS must export the enum *values*, not just the `.d.ts` types. diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs new file mode 100644 index 00000000000..3ee75bd2766 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs @@ -0,0 +1,19 @@ +using DCL.UI; +using System; + +namespace DCL.CrdtEcsBridge.JsModulesImplementation +{ + /// + /// Outcome of an request. + /// + public enum OpenSectionResult + { + Opened, + AlreadyOpen, + } + + public interface IExplorerUiActions : IDisposable + { + OpenSectionResult OpenSection(ExploreSections section); + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs.meta b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs.meta new file mode 100644 index 00000000000..632e1eb0e11 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9f1003dd0c500f04994ca5ebb17d3a51 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs index 32f001023ed..f82a6ba934c 100644 --- a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs @@ -2,16 +2,20 @@ using DCL.AvatarRendering.Emotes; using DCL.ChangeRealmPrompt; using DCL.Clipboard; +using DCL.CrdtEcsBridge.JsModulesImplementation; using DCL.Diagnostics; using DCL.ECSComponents; +using DCL.FeatureFlags; +using DCL.UI; using Utility.Arch; using DCL.ExternalUrlPrompt; using DCL.NftPrompt; +using DCL.SceneRuntime.Apis.RestrictedActionsApi; using DCL.TeleportPrompt; using DCL.Utilities; +using Decentraland.Kernel.Apis; using MVC; using SceneRunner.Scene; -using SceneRuntime.Apis.Modules.RestrictedActionsApi; using SceneRuntime.ScenePermissions; using System; using System.Threading; @@ -22,12 +26,15 @@ namespace CrdtEcsBridge.RestrictedActions { public class RestrictedActionsAPIImplementation : IRestrictedActionsAPI { + private const uint USER_GESTURE_WINDOW_TICKS = 1; + private readonly IMVCManager mvcManager; private readonly ISceneStateProvider sceneStateProvider; private readonly IGlobalWorldActions globalWorldActions; private readonly ISceneData sceneData; private readonly IJsApiPermissionsProvider permissionsProvider; private readonly ISystemClipboard systemClipboard; + private readonly IExplorerUiActions explorerUiActions; private readonly Arch.Core.World sceneWorld; private readonly Arch.Core.Entity scenePlayerEntity; @@ -39,7 +46,8 @@ public RestrictedActionsAPIImplementation( IJsApiPermissionsProvider permissionsProvider, ISystemClipboard systemClipboard, Arch.Core.World sceneWorld, - Arch.Core.Entity scenePlayerEntity) + Arch.Core.Entity scenePlayerEntity, + IExplorerUiActions explorerUiActions) { this.mvcManager = mvcManager; this.sceneStateProvider = sceneStateProvider; @@ -49,6 +57,7 @@ public RestrictedActionsAPIImplementation( this.systemClipboard = systemClipboard; this.sceneWorld = sceneWorld; this.scenePlayerEntity = scenePlayerEntity; + this.explorerUiActions = explorerUiActions; } public bool TryOpenExternalUrl(string url) @@ -200,6 +209,44 @@ public bool TryOpenNftDialog(string urn) return true; } + public int TryOpenExplorerUi(int ui) + { + if (!sceneStateProvider.IsCurrent) + return (int)OpenExplorerUiResult.RejectedNotCurrentScene; + + // Underflow-safe recent-gesture check: never subtract unsigned ticks. The "== 0" guard is + // load-bearing — a scene where no user input has ever been recorded must be rejected. + uint lastUserInputTick = sceneStateProvider.LastUserInputTick; + + if (lastUserInputTick == 0 || lastUserInputTick + USER_GESTURE_WINDOW_TICKS < sceneStateProvider.TickNumber) + { + ReportHub.Log(ReportCategory.RESTRICTED_ACTIONS, "OpenExplorerUi: rejected, call did not originate from a recent user gesture"); + return (int)OpenExplorerUiResult.RejectedNoUserGesture; + } + + if (!TryMapExplorerUi((ExplorerUi)ui, out ExploreSections section, out FeatureId? gatingFeature)) + { + ReportHub.LogWarning(ReportCategory.RESTRICTED_ACTIONS, $"OpenExplorerUi: unsupported ui value '{ui}'"); + return (int)OpenExplorerUiResult.RejectedFeatureDisabled; + } + + if (gatingFeature.HasValue && !FeaturesRegistry.Instance.IsEnabled(gatingFeature.Value)) + { + ReportHub.Log(ReportCategory.RESTRICTED_ACTIONS, $"OpenExplorerUi: feature '{gatingFeature.Value}' is disabled"); + return (int)OpenExplorerUiResult.RejectedFeatureDisabled; + } + + if (explorerUiActions.OpenSection(section) == OpenSectionResult.AlreadyOpen) + return (int)OpenExplorerUiResult.RejectedAlreadyOpen; + + return (int)OpenExplorerUiResult.Opened; + } + + public void Dispose() + { + explorerUiActions.Dispose(); + } + public void TryCopyToClipboard(string text) { if (!sceneStateProvider.IsCurrent) @@ -253,5 +300,49 @@ private async UniTask CopyToClipboardAsync(string text) await UniTask.SwitchToMainThread(); systemClipboard.Set(text); } + + /// + /// Maps a protocol value to its explore panel section and, when the + /// section is behind a synchronous feature flag, the that gates it. + /// Returns false for unspecified or unknown values so the caller can reject the request. + /// + private static bool TryMapExplorerUi(ExplorerUi ui, out ExploreSections section, out FeatureId? gatingFeature) + { + switch (ui) + { + case ExplorerUi.EuMap: + section = ExploreSections.Navmap; + gatingFeature = null; + return true; + case ExplorerUi.EuSettings: + section = ExploreSections.Settings; + gatingFeature = null; + return true; + case ExplorerUi.EuBackpack: + section = ExploreSections.Backpack; + gatingFeature = null; + return true; + case ExplorerUi.EuCameraReel: + section = ExploreSections.CameraReel; + gatingFeature = FeatureId.CAMERA_REEL; + return true; + case ExplorerUi.EuCommunities: + section = ExploreSections.Communities; + gatingFeature = FeatureId.COMMUNITIES; + return true; + case ExplorerUi.EuPlaces: + section = ExploreSections.Places; + gatingFeature = FeatureId.DISCOVER; + return true; + case ExplorerUi.EuEvents: + section = ExploreSections.Events; + gatingFeature = FeatureId.DISCOVER; + return true; + default: + section = default(ExploreSections); + gatingFeature = null; + return false; + } + } } } diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs index ba9b6d7e18c..284e0843a8b 100644 --- a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs @@ -3,10 +3,14 @@ using Cysharp.Threading.Tasks; using DCL.ChangeRealmPrompt; using DCL.Clipboard; +using DCL.CrdtEcsBridge.JsModulesImplementation; using DCL.ECSComponents; using DCL.ExternalUrlPrompt; using DCL.NftPrompt; using DCL.TeleportPrompt; +using DCL.UI; +using Decentraland.Kernel.Apis; +using ECS.TestSuite; using MVC; using NSubstitute; using NUnit.Framework; @@ -27,14 +31,22 @@ public class RestrictedActionsAPIImplementationShould private IGlobalWorldActions globalWorldActions; private ISceneData sceneData; private ISystemClipboard systemClipboard; + private IExplorerUiActions explorerUiActions; private World sceneWorld; [SetUp] public void SetUp() { + EcsTestsUtils.SetUpFeaturesRegistry(); + mvcManager = Substitute.For(); sceneStateProvider = Substitute.For(); sceneStateProvider.IsCurrent.Returns(true); + + // Stamp a recent user gesture so the OpenExplorerUi gesture gate passes by default. + sceneStateProvider.TickNumber.Returns((uint)10); + sceneStateProvider.LastUserInputTick.Returns((uint)10); + globalWorldActions = Substitute.For(); sceneData = Substitute.For(); sceneData.Geometry.Returns(ParcelMathHelper.UNDEFINED_SCENE_GEOMETRY); @@ -45,6 +57,8 @@ public void SetUp() new Vector2Int(0, 2), }); systemClipboard = Substitute.For(); + explorerUiActions = Substitute.For(); + explorerUiActions.OpenSection(Arg.Any()).Returns(OpenSectionResult.Opened); sceneWorld = World.Create(); Entity scenePlayerEntity = sceneWorld.Create(); restrictedActionsAPIImplementation = new RestrictedActionsAPIImplementation( @@ -55,13 +69,15 @@ public void SetUp() new AllowEverythingJsApiPermissionsProvider(), systemClipboard, sceneWorld, - scenePlayerEntity); + scenePlayerEntity, + explorerUiActions); } [TearDown] public void TearDown() { World.Destroy(sceneWorld); + EcsTestsUtils.TearDownFeaturesRegistry(); } [Test] @@ -145,6 +161,76 @@ public void OpenNftDialog() mvcManager.Received(1).ShowAsync(NftPromptController.IssueCommand(new NftPromptController.Params("ethereum", "0x06012c8cf97bead5deae237070f9587f8e7a266d", "1540722"))); } + [Test] + public void OpenExplorerUi_MapOpensNavmap() + { + // Act + int result = restrictedActionsAPIImplementation.TryOpenExplorerUi((int)ExplorerUi.EuMap); + + // Assert + Assert.AreEqual((int)OpenExplorerUiResult.Opened, result); + explorerUiActions.Received(1).OpenSection(ExploreSections.Navmap); + } + + [Test] + public void OpenExplorerUi_NotCurrentScene_Rejects() + { + // Arrange + sceneStateProvider.IsCurrent.Returns(false); + + // Act + int result = restrictedActionsAPIImplementation.TryOpenExplorerUi((int)ExplorerUi.EuMap); + + // Assert + Assert.AreEqual((int)OpenExplorerUiResult.RejectedNotCurrentScene, result); + explorerUiActions.DidNotReceive().OpenSection(Arg.Any()); + } + + [Test] + [TestCase(0)] // underflow-safe: no user gesture has ever been recorded + [TestCase(5)] // stale: the last gesture is older than the allowed window + public void OpenExplorerUi_NoRecentGesture_Rejects(int lastUserInputTick) + { + // Arrange + sceneStateProvider.TickNumber.Returns((uint)10); + sceneStateProvider.LastUserInputTick.Returns((uint)lastUserInputTick); + + // Act + int result = restrictedActionsAPIImplementation.TryOpenExplorerUi((int)ExplorerUi.EuMap); + + // Assert + Assert.AreEqual((int)OpenExplorerUiResult.RejectedNoUserGesture, result); + explorerUiActions.DidNotReceive().OpenSection(Arg.Any()); + } + + [Test] + public void OpenExplorerUi_AlreadyOpen_Rejects() + { + // Arrange + explorerUiActions.OpenSection(Arg.Any()).Returns(OpenSectionResult.AlreadyOpen); + + // Act + int result = restrictedActionsAPIImplementation.TryOpenExplorerUi((int)ExplorerUi.EuMap); + + // Assert + Assert.AreEqual((int)OpenExplorerUiResult.RejectedAlreadyOpen, result); + } + + [Test] + public void OpenExplorerUi_FeatureDisabled_Rejects() + { + // Arrange + // COMMUNITIES is not force-enabled in the editor (unlike CAMERA_REEL / DISCOVER), + // so the default features registry reports it as disabled. + + // Act + int result = restrictedActionsAPIImplementation.TryOpenExplorerUi((int)ExplorerUi.EuCommunities); + + // Assert + Assert.AreEqual((int)OpenExplorerUiResult.RejectedFeatureDisabled, result); + explorerUiActions.DidNotReceive().OpenSection(Arg.Any()); + } + [Test] public void CopyToClipboard() { diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs new file mode 100644 index 00000000000..6896851f6cb --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs @@ -0,0 +1,60 @@ +using Cysharp.Threading.Tasks; +using DCL.CrdtEcsBridge.JsModulesImplementation; +using DCL.ExplorePanel; +using DCL.UI; +using MVC; + +namespace DCL.SceneRunner +{ + /// + /// DCL.Plugins-side implementation of . It lives here (rather than + /// next to the restricted-actions API) because it references + /// from DCL.Social, an assembly that already depends on SceneRuntime. + /// + public class ExplorerUiActions : IExplorerUiActions + { + private readonly IMVCManager mvcManager; + + private bool isExplorePanelOpen; + + public ExplorerUiActions(IMVCManager mvcManager) + { + this.mvcManager = mvcManager; + mvcManager.OnViewShowed += OnViewShowed; + mvcManager.OnViewClosed += OnViewClosed; + } + + public void Dispose() + { + mvcManager.OnViewShowed -= OnViewShowed; + mvcManager.OnViewClosed -= OnViewClosed; + } + + public OpenSectionResult OpenSection(ExploreSections section) + { + if (isExplorePanelOpen) + return OpenSectionResult.AlreadyOpen; + + OpenSectionAsync(section).Forget(); + return OpenSectionResult.Opened; + } + + private void OnViewShowed(IController controller) + { + if (controller is ExplorePanelController) + isExplorePanelOpen = true; + } + + private void OnViewClosed(IController controller) + { + if (controller is ExplorePanelController) + isExplorePanelOpen = false; + } + + private async UniTask OpenSectionAsync(ExploreSections section) + { + await UniTask.SwitchToMainThread(); + await mvcManager.ShowAsync(ExplorePanelController.IssueCommand(new ExplorePanelParameter(section))); + } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs.meta b/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs.meta new file mode 100644 index 00000000000..613dac862ef --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ac414eae9e741154fbef2275a6bb3b14 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/ISceneStateProvider.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/ISceneStateProvider.cs index 5b1f6d71ff3..6e762b5efc8 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/ISceneStateProvider.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/ISceneStateProvider.cs @@ -13,6 +13,13 @@ public interface ISceneStateProvider uint TickNumber { get; set; } + /// + /// Tick at which the most recent non-hover pointer (down/up) result was written for this scene while + /// it was current. Zero means no user gesture has ever been recorded. Consumed by gesture-gated + /// restricted actions to reject calls that do not originate from recent user input. + /// + uint LastUserInputTick { get; set; } + ref readonly SceneEngineStartInfo EngineStartInfo { get; } void Start(SceneEngineStartInfo startInfo); diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/SceneStateProvider.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/SceneStateProvider.cs index e2fb065089b..77b770feb75 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/SceneStateProvider.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/SceneStateProvider.cs @@ -15,6 +15,8 @@ public class SceneStateProvider : ISceneStateProvider public uint TickNumber { get; set; } + public uint LastUserInputTick { get; set; } + public ref readonly SceneEngineStartInfo EngineStartInfo => ref engineStartInfo; public void Start(SceneEngineStartInfo startInfo) @@ -22,6 +24,7 @@ public void Start(SceneEngineStartInfo startInfo) State.Set(SceneState.Starting); engineStartInfo = startInfo; TickNumber = 0; + LastUserInputTick = 0; } } } diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs index 1ba1f18fe67..b6d047eed72 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs @@ -21,6 +21,8 @@ using DCL.PluginSystem.World.Dependencies; using DCL.Profiles; using DCL.Profiling; +using DCL.SceneRunner; +using DCL.SceneRuntime.Apis.RestrictedActionsApi; using DCL.SDKComponents.MediaStream; using DCL.SkyBox; using DCL.Utilities; @@ -38,7 +40,6 @@ using SceneRuntime.Apis.Modules.CommunicationsControllerApi; using SceneRuntime.Apis.Modules.EngineApi; using SceneRuntime.Apis.Modules.FetchApi; -using SceneRuntime.Apis.Modules.RestrictedActionsApi; using SceneRuntime.Apis.Modules.Runtime; using SceneRuntime.Apis.Modules.SceneApi; using SceneRuntime.ScenePermissions; @@ -241,7 +242,7 @@ protected WithRuntimeAndJsAPIBase( string installSource) : this( engineApi, - new RestrictedActionsAPIImplementation(mvcManager, syncDeps.ecsWorldSharedDependencies.SceneStateProvider, globalWorldActions, syncDeps.sceneData, syncDeps.permissionsProvider, systemClipboard, syncDeps.ECSWorldFacade.EcsWorld, syncDeps.ECSWorldFacade.PersistentEntities.Player), + new RestrictedActionsAPIImplementation(mvcManager, syncDeps.ecsWorldSharedDependencies.SceneStateProvider, globalWorldActions, syncDeps.sceneData, syncDeps.permissionsProvider, systemClipboard, syncDeps.ECSWorldFacade.EcsWorld, syncDeps.ECSWorldFacade.PersistentEntities.Player, new ExplorerUiActions(mvcManager)), new RuntimeImplementation(jsOperations, syncDeps.sceneData, realmData, webRequestController, skyboxSettings, roomHub, installSource), new SceneApiImplementation(syncDeps.sceneData), new ClientWebSocketApiImplementation(syncDeps.PoolsProvider, jsOperations, syncDeps.permissionsProvider), diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Tests/SceneFacadeShould.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Tests/SceneFacadeShould.cs index ccc38f68390..4f90d61f71c 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Tests/SceneFacadeShould.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Tests/SceneFacadeShould.cs @@ -23,6 +23,7 @@ using DCL.PluginSystem.World; using DCL.PluginSystem.World.Dependencies; using DCL.Profiles; +using DCL.SceneRuntime.Apis.RestrictedActionsApi; using DCL.SkyBox; using DCL.Utilities.Extensions; using DCL.Web3; @@ -45,7 +46,6 @@ using SceneRuntime.Apis.Modules.CommunicationsControllerApi; using SceneRuntime.Apis.Modules.EngineApi; using SceneRuntime.Apis.Modules.FetchApi; -using SceneRuntime.Apis.Modules.RestrictedActionsApi; using SceneRuntime.Apis.Modules.Runtime; using SceneRuntime.Apis.Modules.SceneApi; using SceneRuntime.Factory; diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/IRestrictedActionsAPI.cs b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/IRestrictedActionsAPI.cs index 599b716d98e..80fec74144c 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/IRestrictedActionsAPI.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/IRestrictedActionsAPI.cs @@ -1,11 +1,12 @@ using Cysharp.Threading.Tasks; using DCL.ECSComponents; +using System; using System.Threading; using UnityEngine; -namespace SceneRuntime.Apis.Modules.RestrictedActionsApi +namespace DCL.SceneRuntime.Apis.RestrictedActionsApi { - public interface IRestrictedActionsAPI + public interface IRestrictedActionsAPI : IDisposable { bool TryOpenExternalUrl(string url); @@ -21,6 +22,8 @@ public interface IRestrictedActionsAPI bool TryOpenNftDialog(string urn); + int TryOpenExplorerUi(int ui); + void TryCopyToClipboard(string text); void TryStopEmote(); diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/RestrictedActionsAPIWrapper.cs b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/RestrictedActionsAPIWrapper.cs index 44fecfbc5f5..02b66df953c 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/RestrictedActionsAPIWrapper.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/RestrictedActionsAPIWrapper.cs @@ -1,11 +1,13 @@ using Cysharp.Threading.Tasks; using DCL.ECSComponents; using JetBrains.Annotations; +using SceneRuntime; +using SceneRuntime.Apis; using System.Threading; using UnityEngine; using Utility; -namespace SceneRuntime.Apis.Modules.RestrictedActionsApi +namespace DCL.SceneRuntime.Apis.RestrictedActionsApi { public class RestrictedActionsAPIWrapper : JsApiWrapper { @@ -93,6 +95,10 @@ public void CopyToClipboard(string text) => public bool OpenNftDialog(string urn) => api.TryOpenNftDialog(urn); + [UsedImplicitly] + public int OpenExplorerUi(int ui) => + api.TryOpenExplorerUi(ui); + [UsedImplicitly] public object StopEmote() { @@ -109,6 +115,7 @@ async UniTask StopEmoteAsync() } } - public override void Dispose() { } + public override void Dispose() => + api.Dispose(); } } diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/UserActions/UserActionsWrapper.cs b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/UserActions/UserActionsWrapper.cs index 28928b65719..41b23760745 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/UserActions/UserActionsWrapper.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/UserActions/UserActionsWrapper.cs @@ -1,6 +1,6 @@ using DCL.Diagnostics; +using DCL.SceneRuntime.Apis.RestrictedActionsApi; using JetBrains.Annotations; -using SceneRuntime.Apis.Modules.RestrictedActionsApi; using System; using System.Threading; using UnityEngine; diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/ISceneRuntime.cs b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/ISceneRuntime.cs index ac9bba31d85..a8016bf6884 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/ISceneRuntime.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/ISceneRuntime.cs @@ -6,6 +6,7 @@ using DCL.Multiplayer.Profiles.Poses; using DCL.Profiles; using DCL.Profiling; +using DCL.SceneRuntime.Apis.RestrictedActionsApi; using DCL.Web3; using DCL.Web3.Identities; using DCL.WebRequests; @@ -24,7 +25,6 @@ using SceneRuntime.Apis.Modules.FetchApi; using SceneRuntime.Apis.Modules.Players; using SceneRuntime.Apis.Modules.PortableExperiencesApi; -using SceneRuntime.Apis.Modules.RestrictedActionsApi; using SceneRuntime.Apis.Modules.Runtime; using SceneRuntime.Apis.Modules.SceneApi; using SceneRuntime.Apis.Modules.SignedFetch; diff --git a/Explorer/Assets/DCL/Interaction/Systems/WritePointerEventResultsSystem.cs b/Explorer/Assets/DCL/Interaction/Systems/WritePointerEventResultsSystem.cs index aec1bc7c9f4..bc75422c579 100644 --- a/Explorer/Assets/DCL/Interaction/Systems/WritePointerEventResultsSystem.cs +++ b/Explorer/Assets/DCL/Interaction/Systems/WritePointerEventResultsSystem.cs @@ -113,7 +113,16 @@ private bool AppendSingleResult(CRDTEntity sdkEntity, in Vector3 scenePosition, RaycastHit raycastHit = raycastHitPool.Get(); raycastHit.FillSDKRaycastHit(scenePosition, intent, sdkEntity); AppendMessage(sdkEntity, raycastHit, button, eventType); - return eventType is not (PointerEventType.PetHoverEnter or PointerEventType.PetHoverLeave); + + bool isNonHover = eventType is not (PointerEventType.PetHoverEnter or PointerEventType.PetHoverLeave); + + // Record a user gesture so gesture-gated restricted actions (e.g. OpenExplorerUi) can verify the + // call originated from recent input. Uses the same tick source as the appended result. Hover + // enter/leave are not gestures. + if (isNonHover) + sceneStateProvider.LastUserInputTick = sceneStateProvider.TickNumber; + + return isNonHover; } private void AppendMessage(CRDTEntity sdkEntity, RaycastHit? sdkHit, InputAction button, PointerEventType eventType) diff --git a/Explorer/Assets/Protocol/DecentralandProtocol/AvatarEmoteCommand.gen.cs b/Explorer/Assets/Protocol/DecentralandProtocol/AvatarEmoteCommand.gen.cs index 003d69284a9..20a3c8ac920 100644 --- a/Explorer/Assets/Protocol/DecentralandProtocol/AvatarEmoteCommand.gen.cs +++ b/Explorer/Assets/Protocol/DecentralandProtocol/AvatarEmoteCommand.gen.cs @@ -25,14 +25,17 @@ static AvatarEmoteCommandReflection() { byte[] descriptorData = global::System.Convert.FromBase64String( string.Concat( "CjZkZWNlbnRyYWxhbmQvc2RrL2NvbXBvbmVudHMvYXZhdGFyX2Vtb3RlX2Nv", - "bW1hbmQucHJvdG8SG2RlY2VudHJhbGFuZC5zZGsuY29tcG9uZW50cyJKChRQ", - "QkF2YXRhckVtb3RlQ29tbWFuZBIRCgllbW90ZV91cm4YASABKAkSDAoEbG9v", - "cBgCIAEoCBIRCgl0aW1lc3RhbXAYAyABKA1CFKoCEURDTC5FQ1NDb21wb25l", - "bnRzYgZwcm90bzM=")); + "bW1hbmQucHJvdG8SG2RlY2VudHJhbGFuZC5zZGsuY29tcG9uZW50cxo0ZGVj", + "ZW50cmFsYW5kL3Nkay9jb21wb25lbnRzL2NvbW1vbi9hdmF0YXJfbWFzay5w", + "cm90byKWAQoUUEJBdmF0YXJFbW90ZUNvbW1hbmQSEQoJZW1vdGVfdXJuGAEg", + "ASgJEgwKBGxvb3AYAiABKAgSEQoJdGltZXN0YW1wGAMgASgNEkEKBG1hc2sY", + "BCABKA4yLi5kZWNlbnRyYWxhbmQuc2RrLmNvbXBvbmVudHMuY29tbW9uLkF2", + "YXRhck1hc2tIAIgBAUIHCgVfbWFza0IUqgIRRENMLkVDU0NvbXBvbmVudHNi", + "BnByb3RvMw==")); descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, - new pbr::FileDescriptor[] { }, + new pbr::FileDescriptor[] { global::DCL.ECSComponents.AvatarMaskReflection.Descriptor, }, new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] { - new pbr::GeneratedClrTypeInfo(typeof(global::DCL.ECSComponents.PBAvatarEmoteCommand), global::DCL.ECSComponents.PBAvatarEmoteCommand.Parser, new[]{ "EmoteUrn", "Loop", "Timestamp" }, null, null, null, null) + new pbr::GeneratedClrTypeInfo(typeof(global::DCL.ECSComponents.PBAvatarEmoteCommand), global::DCL.ECSComponents.PBAvatarEmoteCommand.Parser, new[]{ "EmoteUrn", "Loop", "Timestamp", "Mask" }, new[]{ "Mask" }, null, null, null) })); } #endregion @@ -51,6 +54,7 @@ public sealed partial class PBAvatarEmoteCommand : pb::IMessage _parser = new pb::MessageParser(() => new PBAvatarEmoteCommand()); private pb::UnknownFieldSet _unknownFields; + private int _hasBits0; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pb::MessageParser Parser { get { return _parser; } } @@ -78,9 +82,11 @@ public PBAvatarEmoteCommand() { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public PBAvatarEmoteCommand(PBAvatarEmoteCommand other) : this() { + _hasBits0 = other._hasBits0; emoteUrn_ = other.emoteUrn_; loop_ = other.loop_; timestamp_ = other.timestamp_; + mask_ = other.mask_; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } @@ -129,6 +135,33 @@ public uint Timestamp { } } + /// Field number for the "mask" field. + public const int MaskFieldNumber = 4; + private readonly static global::DCL.ECSComponents.AvatarMask MaskDefaultValue = global::DCL.ECSComponents.AvatarMask.AmUpperBody; + + private global::DCL.ECSComponents.AvatarMask mask_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::DCL.ECSComponents.AvatarMask Mask { + get { if ((_hasBits0 & 1) != 0) { return mask_; } else { return MaskDefaultValue; } } + set { + _hasBits0 |= 1; + mask_ = value; + } + } + /// Gets whether the "mask" field is set + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool HasMask { + get { return (_hasBits0 & 1) != 0; } + } + /// Clears the value of the "mask" field + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void ClearMask() { + _hasBits0 &= ~1; + } + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override bool Equals(object other) { @@ -147,6 +180,7 @@ public bool Equals(PBAvatarEmoteCommand other) { if (EmoteUrn != other.EmoteUrn) return false; if (Loop != other.Loop) return false; if (Timestamp != other.Timestamp) return false; + if (Mask != other.Mask) return false; return Equals(_unknownFields, other._unknownFields); } @@ -157,6 +191,7 @@ public override int GetHashCode() { if (EmoteUrn.Length != 0) hash ^= EmoteUrn.GetHashCode(); if (Loop != false) hash ^= Loop.GetHashCode(); if (Timestamp != 0) hash ^= Timestamp.GetHashCode(); + if (HasMask) hash ^= Mask.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -187,6 +222,10 @@ public void WriteTo(pb::CodedOutputStream output) { output.WriteRawTag(24); output.WriteUInt32(Timestamp); } + if (HasMask) { + output.WriteRawTag(32); + output.WriteEnum((int) Mask); + } if (_unknownFields != null) { _unknownFields.WriteTo(output); } @@ -209,6 +248,10 @@ public void WriteTo(pb::CodedOutputStream output) { output.WriteRawTag(24); output.WriteUInt32(Timestamp); } + if (HasMask) { + output.WriteRawTag(32); + output.WriteEnum((int) Mask); + } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); } @@ -228,6 +271,9 @@ public int CalculateSize() { if (Timestamp != 0) { size += 1 + pb::CodedOutputStream.ComputeUInt32Size(Timestamp); } + if (HasMask) { + size += 1 + pb::CodedOutputStream.ComputeEnumSize((int) Mask); + } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); } @@ -249,6 +295,9 @@ public void MergeFrom(PBAvatarEmoteCommand other) { if (other.Timestamp != 0) { Timestamp = other.Timestamp; } + if (other.HasMask) { + Mask = other.Mask; + } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -280,6 +329,10 @@ public void MergeFrom(pb::CodedInputStream input) { Timestamp = input.ReadUInt32(); break; } + case 32: { + Mask = (global::DCL.ECSComponents.AvatarMask) input.ReadEnum(); + break; + } } } #endif @@ -311,6 +364,10 @@ public void MergeFrom(pb::CodedInputStream input) { Timestamp = input.ReadUInt32(); break; } + case 32: { + Mask = (global::DCL.ECSComponents.AvatarMask) input.ReadEnum(); + break; + } } } } diff --git a/Explorer/Assets/Protocol/DecentralandProtocol/RestrictedActions.gen.cs b/Explorer/Assets/Protocol/DecentralandProtocol/RestrictedActions.gen.cs index 23f30a74040..d235fb41162 100644 --- a/Explorer/Assets/Protocol/DecentralandProtocol/RestrictedActions.gen.cs +++ b/Explorer/Assets/Protocol/DecentralandProtocol/RestrictedActions.gen.cs @@ -50,7 +50,18 @@ static RestrictedActionsReflection() { "Y2Nlc3MYASABKAgiFgoUVHJpZ2dlckVtb3RlUmVzcG9uc2UiJwoUTW92ZVBs", "YXllclRvUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCIUChJUZWxlcG9ydFRv", "UmVzcG9uc2UiJgoWQ29weVRvQ2xpcGJvYXJkUmVxdWVzdBIMCgR0ZXh0GAEg", - "ASgJIg8KDUVtcHR5UmVzcG9uc2UiEgoQU3RvcEVtb3RlUmVxdWVzdDL1CAoY", + "ASgJIg8KDUVtcHR5UmVzcG9uc2UiEgoQU3RvcEVtb3RlUmVxdWVzdCJJChVP", + "cGVuRXhwbG9yZXJVaVJlcXVlc3QSMAoCdWkYASABKA4yJC5kZWNlbnRyYWxh", + "bmQua2VybmVsLmFwaXMuRXhwbG9yZXJVaSJdChZPcGVuRXhwbG9yZXJVaVJl", + "c3BvbnNlEkMKC29wZW5fcmVzdWx0GAEgASgOMi4uZGVjZW50cmFsYW5kLmtl", + "cm5lbC5hcGlzLk9wZW5FeHBsb3JlclVpUmVzdWx0KpQBCgpFeHBsb3JlclVp", + "EhIKDkVVX1VOU1BFQ0lGSUVEEAASCgoGRVVfTUFQEAESDwoLRVVfU0VUVElO", + "R1MQAhIPCgtFVV9CQUNLUEFDSxADEhIKDkVVX0NBTUVSQV9SRUVMEAQSEgoO", + "RVVfQ09NTVVOSVRJRVMQBRINCglFVV9QTEFDRVMQBhINCglFVV9FVkVOVFMQ", + "ByqrAQoUT3BlbkV4cGxvcmVyVWlSZXN1bHQSDwoLVU5TUEVDSUZJRUQQABIK", + "CgZPUEVORUQQARIeChpSRUpFQ1RFRF9OT1RfQ1VSUkVOVF9TQ0VORRACEhkK", + "FVJFSkVDVEVEX0FMUkVBRFlfT1BFThADEh0KGVJFSkVDVEVEX0ZFQVRVUkVf", + "RElTQUJMRUQQBBIcChhSRUpFQ1RFRF9OT19VU0VSX0dFU1RVUkUQBTLsCQoY", "UmVzdHJpY3RlZEFjdGlvbnNTZXJ2aWNlEm8KDE1vdmVQbGF5ZXJUbxItLmRl", "Y2VudHJhbGFuZC5rZXJuZWwuYXBpcy5Nb3ZlUGxheWVyVG9SZXF1ZXN0Gi4u", "ZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlzLk1vdmVQbGF5ZXJUb1Jlc3BvbnNl", @@ -76,10 +87,13 @@ static RestrictedActionsReflection() { "dWVzdBonLmRlY2VudHJhbGFuZC5rZXJuZWwuYXBpcy5FbXB0eVJlc3BvbnNl", "IgASZAoJU3RvcEVtb3RlEiouZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlzLlN0", "b3BFbW90ZVJlcXVlc3QaKS5kZWNlbnRyYWxhbmQua2VybmVsLmFwaXMuU3Vj", - "Y2Vzc1Jlc3BvbnNlIgBiBnByb3RvMw==")); + "Y2Vzc1Jlc3BvbnNlIgASdQoOT3BlbkV4cGxvcmVyVWkSLy5kZWNlbnRyYWxh", + "bmQua2VybmVsLmFwaXMuT3BlbkV4cGxvcmVyVWlSZXF1ZXN0GjAuZGVjZW50", + "cmFsYW5kLmtlcm5lbC5hcGlzLk9wZW5FeHBsb3JlclVpUmVzcG9uc2UiAGIG", + "cHJvdG8z")); descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, new pbr::FileDescriptor[] { global::Decentraland.Common.VectorsReflection.Descriptor, global::DCL.ECSComponents.AvatarMaskReflection.Descriptor, }, - new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] { + new pbr::GeneratedClrTypeInfo(new[] {typeof(global::Decentraland.Kernel.Apis.ExplorerUi), typeof(global::Decentraland.Kernel.Apis.OpenExplorerUiResult), }, null, new pbr::GeneratedClrTypeInfo[] { new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.MovePlayerToRequest), global::Decentraland.Kernel.Apis.MovePlayerToRequest.Parser, new[]{ "NewRelativePosition", "CameraTarget", "AvatarTarget", "Duration" }, new[]{ "CameraTarget", "AvatarTarget", "Duration" }, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.TeleportToRequest), global::Decentraland.Kernel.Apis.TeleportToRequest.Parser, new[]{ "WorldCoordinates" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.TriggerEmoteRequest), global::Decentraland.Kernel.Apis.TriggerEmoteRequest.Parser, new[]{ "PredefinedEmote", "Mask" }, new[]{ "Mask" }, null, null, null), @@ -95,12 +109,57 @@ static RestrictedActionsReflection() { new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.TeleportToResponse), global::Decentraland.Kernel.Apis.TeleportToResponse.Parser, null, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.CopyToClipboardRequest), global::Decentraland.Kernel.Apis.CopyToClipboardRequest.Parser, new[]{ "Text" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.EmptyResponse), global::Decentraland.Kernel.Apis.EmptyResponse.Parser, null, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.StopEmoteRequest), global::Decentraland.Kernel.Apis.StopEmoteRequest.Parser, null, null, null, null, null) + new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.StopEmoteRequest), global::Decentraland.Kernel.Apis.StopEmoteRequest.Parser, null, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.OpenExplorerUiRequest), global::Decentraland.Kernel.Apis.OpenExplorerUiRequest.Parser, new[]{ "Ui" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.OpenExplorerUiResponse), global::Decentraland.Kernel.Apis.OpenExplorerUiResponse.Parser, new[]{ "OpenResult" }, null, null, null, null) })); } #endregion } + #region Enums + /// + /// Identifies which fullscreen explorer panel OpenExplorerUi targets. + /// Additive: new panels are appended with the next number; existing values never change. + /// + public enum ExplorerUi { + [pbr::OriginalName("EU_UNSPECIFIED")] EuUnspecified = 0, + [pbr::OriginalName("EU_MAP")] EuMap = 1, + [pbr::OriginalName("EU_SETTINGS")] EuSettings = 2, + [pbr::OriginalName("EU_BACKPACK")] EuBackpack = 3, + [pbr::OriginalName("EU_CAMERA_REEL")] EuCameraReel = 4, + [pbr::OriginalName("EU_COMMUNITIES")] EuCommunities = 5, + [pbr::OriginalName("EU_PLACES")] EuPlaces = 6, + [pbr::OriginalName("EU_EVENTS")] EuEvents = 7, + } + + /// + /// Verdict of an OpenExplorerUi request (enum rather than a bool so new outcomes stay expressible). + /// Additive: new rejection reasons may be appended; existing values never change. + /// + public enum OpenExplorerUiResult { + [pbr::OriginalName("UNSPECIFIED")] Unspecified = 0, + [pbr::OriginalName("OPENED")] Opened = 1, + /// + /// the standard restricted-actions current-scene gate rejected the call + /// + [pbr::OriginalName("REJECTED_NOT_CURRENT_SCENE")] RejectedNotCurrentScene = 2, + /// + /// a fullscreen panel is already open + /// + [pbr::OriginalName("REJECTED_ALREADY_OPEN")] RejectedAlreadyOpen = 3, + /// + /// the requested section is hidden by feature flags + /// + [pbr::OriginalName("REJECTED_FEATURE_DISABLED")] RejectedFeatureDisabled = 4, + /// + /// rejected: the call did not originate from a user gesture + /// + [pbr::OriginalName("REJECTED_NO_USER_GESTURE")] RejectedNoUserGesture = 5, + } + + #endregion + #region Messages [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] public sealed partial class MovePlayerToRequest : pb::IMessage @@ -3463,6 +3522,407 @@ public void MergeFrom(pb::CodedInputStream input) { } + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class OpenExplorerUiRequest : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new OpenExplorerUiRequest()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::Decentraland.Kernel.Apis.RestrictedActionsReflection.Descriptor.MessageTypes[16]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public OpenExplorerUiRequest() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public OpenExplorerUiRequest(OpenExplorerUiRequest other) : this() { + ui_ = other.ui_; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public OpenExplorerUiRequest Clone() { + return new OpenExplorerUiRequest(this); + } + + /// Field number for the "ui" field. + public const int UiFieldNumber = 1; + private global::Decentraland.Kernel.Apis.ExplorerUi ui_ = global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Decentraland.Kernel.Apis.ExplorerUi Ui { + get { return ui_; } + set { + ui_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as OpenExplorerUiRequest); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(OpenExplorerUiRequest other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (Ui != other.Ui) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified) hash ^= Ui.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified) { + output.WriteRawTag(8); + output.WriteEnum((int) Ui); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified) { + output.WriteRawTag(8); + output.WriteEnum((int) Ui); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified) { + size += 1 + pb::CodedOutputStream.ComputeEnumSize((int) Ui); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(OpenExplorerUiRequest other) { + if (other == null) { + return; + } + if (other.Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified) { + Ui = other.Ui; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 8: { + Ui = (global::Decentraland.Kernel.Apis.ExplorerUi) input.ReadEnum(); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 8: { + Ui = (global::Decentraland.Kernel.Apis.ExplorerUi) input.ReadEnum(); + break; + } + } + } + } + #endif + + } + + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class OpenExplorerUiResponse : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new OpenExplorerUiResponse()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::Decentraland.Kernel.Apis.RestrictedActionsReflection.Descriptor.MessageTypes[17]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public OpenExplorerUiResponse() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public OpenExplorerUiResponse(OpenExplorerUiResponse other) : this() { + openResult_ = other.openResult_; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public OpenExplorerUiResponse Clone() { + return new OpenExplorerUiResponse(this); + } + + /// Field number for the "open_result" field. + public const int OpenResultFieldNumber = 1; + private global::Decentraland.Kernel.Apis.OpenExplorerUiResult openResult_ = global::Decentraland.Kernel.Apis.OpenExplorerUiResult.Unspecified; + /// + /// Carries only the verdict of the open action. Post-open signals (panel closed, flow + /// outcomes, milestones) are deliberately out of scope here and are delivered through a + /// separate additive event channel (a grow-only scene component), keeping this contract stable. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Decentraland.Kernel.Apis.OpenExplorerUiResult OpenResult { + get { return openResult_; } + set { + openResult_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as OpenExplorerUiResponse); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(OpenExplorerUiResponse other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (OpenResult != other.OpenResult) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (OpenResult != global::Decentraland.Kernel.Apis.OpenExplorerUiResult.Unspecified) hash ^= OpenResult.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (OpenResult != global::Decentraland.Kernel.Apis.OpenExplorerUiResult.Unspecified) { + output.WriteRawTag(8); + output.WriteEnum((int) OpenResult); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (OpenResult != global::Decentraland.Kernel.Apis.OpenExplorerUiResult.Unspecified) { + output.WriteRawTag(8); + output.WriteEnum((int) OpenResult); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (OpenResult != global::Decentraland.Kernel.Apis.OpenExplorerUiResult.Unspecified) { + size += 1 + pb::CodedOutputStream.ComputeEnumSize((int) OpenResult); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(OpenExplorerUiResponse other) { + if (other == null) { + return; + } + if (other.OpenResult != global::Decentraland.Kernel.Apis.OpenExplorerUiResult.Unspecified) { + OpenResult = other.OpenResult; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 8: { + OpenResult = (global::Decentraland.Kernel.Apis.OpenExplorerUiResult) input.ReadEnum(); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 8: { + OpenResult = (global::Decentraland.Kernel.Apis.OpenExplorerUiResult) input.ReadEnum(); + break; + } + } + } + } + #endif + + } + #endregion } diff --git a/Explorer/Assets/StreamingAssets/Js/Modules/RestrictedActions.js b/Explorer/Assets/StreamingAssets/Js/Modules/RestrictedActions.js index 211d5d0889e..dcef49923a5 100644 --- a/Explorer/Assets/StreamingAssets/Js/Modules/RestrictedActions.js +++ b/Explorer/Assets/StreamingAssets/Js/Modules/RestrictedActions.js @@ -1,6 +1,36 @@ // Responses should always correspond to the protocol definitions at // https://github.com/decentraland/protocol/blob/main/proto/decentraland/kernel/apis/restricted_actions.proto +// Protocol enums exposed to scenes as runtime values (mirroring the generated +// ts-proto enums: both forward name->value and reverse value->name mappings). +// Scenes import these from `~system/RestrictedActions` and use them at runtime, +// e.g. `openExplorerUi({ ui: ExplorerUi.EU_MAP })` and `OpenExplorerUiResult[openResult]`. +function makeEnum(entries) { + const e = {}; + for (const [name, value] of entries) { e[name] = value; e[value] = name; } + return e; +} + +module.exports.ExplorerUi = makeEnum([ + ['EU_UNSPECIFIED', 0], + ['EU_MAP', 1], + ['EU_SETTINGS', 2], + ['EU_BACKPACK', 3], + ['EU_CAMERA_REEL', 4], + ['EU_COMMUNITIES', 5], + ['EU_PLACES', 6], + ['EU_EVENTS', 7], +]); + +module.exports.OpenExplorerUiResult = makeEnum([ + ['UNSPECIFIED', 0], + ['OPENED', 1], + ['REJECTED_NOT_CURRENT_SCENE', 2], + ['REJECTED_ALREADY_OPEN', 3], + ['REJECTED_FEATURE_DISABLED', 4], + ['REJECTED_NO_USER_GESTURE', 5], +]); + module.exports.movePlayerTo = async function(message) { const cameraTarget = message.cameraTarget != undefined const avatarTarget = message.avatarTarget != undefined @@ -62,6 +92,11 @@ module.exports.openNftDialog = async function(message) { }; } +module.exports.openExplorerUi = async function(message) { + const openResult = UnityRestrictedActionsApi.OpenExplorerUi(message.ui) + return { openResult }; +} + module.exports.setCommunicationsAdapter = async function(message) { console.log('JSMODULE: setCommunicationsAdapter') return { diff --git a/Explorer/DCL.EditMode.Tests.csproj.DotSettings b/Explorer/DCL.EditMode.Tests.csproj.DotSettings index 6769ea07494..c5777b99330 100644 --- a/Explorer/DCL.EditMode.Tests.csproj.DotSettings +++ b/Explorer/DCL.EditMode.Tests.csproj.DotSettings @@ -5,6 +5,7 @@ True True True + True True True True diff --git a/Explorer/SceneRuntime.csproj.DotSettings b/Explorer/SceneRuntime.csproj.DotSettings index b6666aa0390..798fbd4ac03 100644 --- a/Explorer/SceneRuntime.csproj.DotSettings +++ b/Explorer/SceneRuntime.csproj.DotSettings @@ -1,5 +1,6 @@  True + True True True True \ No newline at end of file diff --git a/scripts/package-lock.json b/scripts/package-lock.json index 42f0aa2d89b..84119a6d5c1 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "Apache-2.0", "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-29744166153.commit-0e8b20c.tgz", "@protobuf-ts/protoc": "^2.11.0", "@types/fs-extra": "^11.0.1", "@types/glob": "^8.0.1", @@ -37,9 +37,9 @@ } }, "node_modules/@dcl/protocol": { - "version": "1.0.0-29092627614.commit-d2f272f", - "resolved": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-29092627614.commit-d2f272f.tgz", - "integrity": "sha512-ETnLajHgCy0KCwnQ+oC5faEY53sa/y29Z1/0kHmiTsq5EWZLl1r6kwnFBSV0NrZD7iw8Cmz0XX694XonYqH37w==", + "version": "1.0.0-29744166153.commit-0e8b20c", + "resolved": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-29744166153.commit-0e8b20c.tgz", + "integrity": "sha512-8npy9TaxTARV2XMqZjJlLVO/aG+ZHvLUV3GsI+yrNFxx4Rh+3Z6IJQWEwFM9fT5O46K0LnAgMrqEYqhypxk4hg==", "license": "Apache-2.0", "dependencies": { "@dcl/ts-proto": "1.154.0", @@ -608,8 +608,8 @@ } }, "@dcl/protocol": { - "version": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-29092627614.commit-d2f272f.tgz", - "integrity": "sha512-ETnLajHgCy0KCwnQ+oC5faEY53sa/y29Z1/0kHmiTsq5EWZLl1r6kwnFBSV0NrZD7iw8Cmz0XX694XonYqH37w==", + "version": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-29744166153.commit-0e8b20c.tgz", + "integrity": "sha512-8npy9TaxTARV2XMqZjJlLVO/aG+ZHvLUV3GsI+yrNFxx4Rh+3Z6IJQWEwFM9fT5O46K0LnAgMrqEYqhypxk4hg==", "requires": { "@dcl/ts-proto": "1.154.0", "protobufjs": "7.2.4" diff --git a/scripts/package.json b/scripts/package.json index f57bf4cb1c0..1de4469e5ec 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -16,7 +16,7 @@ "typescript": "^4.2.3" }, "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-29744166153.commit-0e8b20c.tgz", "@protobuf-ts/protoc": "^2.11.0", "@types/fs-extra": "^11.0.1", "@types/glob": "^8.0.1", From 73083b7783a2ffc108c09d9847c745eb636e73db Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 23 Jul 2026 19:38:28 +0200 Subject: [PATCH 2/9] feat: add openExplorerUi restricted action 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 --- .../RestrictedActions/IExplorerUiActions.cs | 19 + .../IExplorerUiActions.cs.meta | 2 + .../RestrictedActionsAPIImplementation.cs | 95 +++- ...estrictedActionsAPIImplementationShould.cs | 88 +++- .../SceneRunner/ExplorerUiActions.cs | 60 +++ .../SceneRunner/ExplorerUiActions.cs.meta | 2 + .../SceneRunner/Scene/ISceneStateProvider.cs | 7 + .../SceneRunner/Scene/SceneStateProvider.cs | 3 + .../SceneRunner/SceneInstanceDeps.cs | 5 +- .../SceneRunner/Tests/SceneFacadeShould.cs | 2 +- .../IRestrictedActionsAPI.cs | 7 +- .../RestrictedActionsAPIWrapper.cs | 11 +- .../Modules/UserActions/UserActionsWrapper.cs | 2 +- .../SceneRuntime/ISceneRuntime.cs | 2 +- .../Systems/WritePointerEventResultsSystem.cs | 11 +- .../AvatarEmoteCommand.gen.cs | 69 ++- .../RestrictedActions.gen.cs | 468 +++++++++++++++++- .../Js/Modules/RestrictedActions.js | 35 ++ .../DCL.EditMode.Tests.csproj.DotSettings | 1 + Explorer/SceneRuntime.csproj.DotSettings | 1 + scripts/package-lock.json | 12 +- scripts/package.json | 2 +- 22 files changed, 874 insertions(+), 30 deletions(-) create mode 100644 Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs.meta create mode 100644 Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs create mode 100644 Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs.meta diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs new file mode 100644 index 00000000000..3ee75bd2766 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs @@ -0,0 +1,19 @@ +using DCL.UI; +using System; + +namespace DCL.CrdtEcsBridge.JsModulesImplementation +{ + /// + /// Outcome of an request. + /// + public enum OpenSectionResult + { + Opened, + AlreadyOpen, + } + + public interface IExplorerUiActions : IDisposable + { + OpenSectionResult OpenSection(ExploreSections section); + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs.meta b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs.meta new file mode 100644 index 00000000000..632e1eb0e11 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9f1003dd0c500f04994ca5ebb17d3a51 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs index 32f001023ed..f82a6ba934c 100644 --- a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs @@ -2,16 +2,20 @@ using DCL.AvatarRendering.Emotes; using DCL.ChangeRealmPrompt; using DCL.Clipboard; +using DCL.CrdtEcsBridge.JsModulesImplementation; using DCL.Diagnostics; using DCL.ECSComponents; +using DCL.FeatureFlags; +using DCL.UI; using Utility.Arch; using DCL.ExternalUrlPrompt; using DCL.NftPrompt; +using DCL.SceneRuntime.Apis.RestrictedActionsApi; using DCL.TeleportPrompt; using DCL.Utilities; +using Decentraland.Kernel.Apis; using MVC; using SceneRunner.Scene; -using SceneRuntime.Apis.Modules.RestrictedActionsApi; using SceneRuntime.ScenePermissions; using System; using System.Threading; @@ -22,12 +26,15 @@ namespace CrdtEcsBridge.RestrictedActions { public class RestrictedActionsAPIImplementation : IRestrictedActionsAPI { + private const uint USER_GESTURE_WINDOW_TICKS = 1; + private readonly IMVCManager mvcManager; private readonly ISceneStateProvider sceneStateProvider; private readonly IGlobalWorldActions globalWorldActions; private readonly ISceneData sceneData; private readonly IJsApiPermissionsProvider permissionsProvider; private readonly ISystemClipboard systemClipboard; + private readonly IExplorerUiActions explorerUiActions; private readonly Arch.Core.World sceneWorld; private readonly Arch.Core.Entity scenePlayerEntity; @@ -39,7 +46,8 @@ public RestrictedActionsAPIImplementation( IJsApiPermissionsProvider permissionsProvider, ISystemClipboard systemClipboard, Arch.Core.World sceneWorld, - Arch.Core.Entity scenePlayerEntity) + Arch.Core.Entity scenePlayerEntity, + IExplorerUiActions explorerUiActions) { this.mvcManager = mvcManager; this.sceneStateProvider = sceneStateProvider; @@ -49,6 +57,7 @@ public RestrictedActionsAPIImplementation( this.systemClipboard = systemClipboard; this.sceneWorld = sceneWorld; this.scenePlayerEntity = scenePlayerEntity; + this.explorerUiActions = explorerUiActions; } public bool TryOpenExternalUrl(string url) @@ -200,6 +209,44 @@ public bool TryOpenNftDialog(string urn) return true; } + public int TryOpenExplorerUi(int ui) + { + if (!sceneStateProvider.IsCurrent) + return (int)OpenExplorerUiResult.RejectedNotCurrentScene; + + // Underflow-safe recent-gesture check: never subtract unsigned ticks. The "== 0" guard is + // load-bearing — a scene where no user input has ever been recorded must be rejected. + uint lastUserInputTick = sceneStateProvider.LastUserInputTick; + + if (lastUserInputTick == 0 || lastUserInputTick + USER_GESTURE_WINDOW_TICKS < sceneStateProvider.TickNumber) + { + ReportHub.Log(ReportCategory.RESTRICTED_ACTIONS, "OpenExplorerUi: rejected, call did not originate from a recent user gesture"); + return (int)OpenExplorerUiResult.RejectedNoUserGesture; + } + + if (!TryMapExplorerUi((ExplorerUi)ui, out ExploreSections section, out FeatureId? gatingFeature)) + { + ReportHub.LogWarning(ReportCategory.RESTRICTED_ACTIONS, $"OpenExplorerUi: unsupported ui value '{ui}'"); + return (int)OpenExplorerUiResult.RejectedFeatureDisabled; + } + + if (gatingFeature.HasValue && !FeaturesRegistry.Instance.IsEnabled(gatingFeature.Value)) + { + ReportHub.Log(ReportCategory.RESTRICTED_ACTIONS, $"OpenExplorerUi: feature '{gatingFeature.Value}' is disabled"); + return (int)OpenExplorerUiResult.RejectedFeatureDisabled; + } + + if (explorerUiActions.OpenSection(section) == OpenSectionResult.AlreadyOpen) + return (int)OpenExplorerUiResult.RejectedAlreadyOpen; + + return (int)OpenExplorerUiResult.Opened; + } + + public void Dispose() + { + explorerUiActions.Dispose(); + } + public void TryCopyToClipboard(string text) { if (!sceneStateProvider.IsCurrent) @@ -253,5 +300,49 @@ private async UniTask CopyToClipboardAsync(string text) await UniTask.SwitchToMainThread(); systemClipboard.Set(text); } + + /// + /// Maps a protocol value to its explore panel section and, when the + /// section is behind a synchronous feature flag, the that gates it. + /// Returns false for unspecified or unknown values so the caller can reject the request. + /// + private static bool TryMapExplorerUi(ExplorerUi ui, out ExploreSections section, out FeatureId? gatingFeature) + { + switch (ui) + { + case ExplorerUi.EuMap: + section = ExploreSections.Navmap; + gatingFeature = null; + return true; + case ExplorerUi.EuSettings: + section = ExploreSections.Settings; + gatingFeature = null; + return true; + case ExplorerUi.EuBackpack: + section = ExploreSections.Backpack; + gatingFeature = null; + return true; + case ExplorerUi.EuCameraReel: + section = ExploreSections.CameraReel; + gatingFeature = FeatureId.CAMERA_REEL; + return true; + case ExplorerUi.EuCommunities: + section = ExploreSections.Communities; + gatingFeature = FeatureId.COMMUNITIES; + return true; + case ExplorerUi.EuPlaces: + section = ExploreSections.Places; + gatingFeature = FeatureId.DISCOVER; + return true; + case ExplorerUi.EuEvents: + section = ExploreSections.Events; + gatingFeature = FeatureId.DISCOVER; + return true; + default: + section = default(ExploreSections); + gatingFeature = null; + return false; + } + } } } diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs index ba9b6d7e18c..284e0843a8b 100644 --- a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs @@ -3,10 +3,14 @@ using Cysharp.Threading.Tasks; using DCL.ChangeRealmPrompt; using DCL.Clipboard; +using DCL.CrdtEcsBridge.JsModulesImplementation; using DCL.ECSComponents; using DCL.ExternalUrlPrompt; using DCL.NftPrompt; using DCL.TeleportPrompt; +using DCL.UI; +using Decentraland.Kernel.Apis; +using ECS.TestSuite; using MVC; using NSubstitute; using NUnit.Framework; @@ -27,14 +31,22 @@ public class RestrictedActionsAPIImplementationShould private IGlobalWorldActions globalWorldActions; private ISceneData sceneData; private ISystemClipboard systemClipboard; + private IExplorerUiActions explorerUiActions; private World sceneWorld; [SetUp] public void SetUp() { + EcsTestsUtils.SetUpFeaturesRegistry(); + mvcManager = Substitute.For(); sceneStateProvider = Substitute.For(); sceneStateProvider.IsCurrent.Returns(true); + + // Stamp a recent user gesture so the OpenExplorerUi gesture gate passes by default. + sceneStateProvider.TickNumber.Returns((uint)10); + sceneStateProvider.LastUserInputTick.Returns((uint)10); + globalWorldActions = Substitute.For(); sceneData = Substitute.For(); sceneData.Geometry.Returns(ParcelMathHelper.UNDEFINED_SCENE_GEOMETRY); @@ -45,6 +57,8 @@ public void SetUp() new Vector2Int(0, 2), }); systemClipboard = Substitute.For(); + explorerUiActions = Substitute.For(); + explorerUiActions.OpenSection(Arg.Any()).Returns(OpenSectionResult.Opened); sceneWorld = World.Create(); Entity scenePlayerEntity = sceneWorld.Create(); restrictedActionsAPIImplementation = new RestrictedActionsAPIImplementation( @@ -55,13 +69,15 @@ public void SetUp() new AllowEverythingJsApiPermissionsProvider(), systemClipboard, sceneWorld, - scenePlayerEntity); + scenePlayerEntity, + explorerUiActions); } [TearDown] public void TearDown() { World.Destroy(sceneWorld); + EcsTestsUtils.TearDownFeaturesRegistry(); } [Test] @@ -145,6 +161,76 @@ public void OpenNftDialog() mvcManager.Received(1).ShowAsync(NftPromptController.IssueCommand(new NftPromptController.Params("ethereum", "0x06012c8cf97bead5deae237070f9587f8e7a266d", "1540722"))); } + [Test] + public void OpenExplorerUi_MapOpensNavmap() + { + // Act + int result = restrictedActionsAPIImplementation.TryOpenExplorerUi((int)ExplorerUi.EuMap); + + // Assert + Assert.AreEqual((int)OpenExplorerUiResult.Opened, result); + explorerUiActions.Received(1).OpenSection(ExploreSections.Navmap); + } + + [Test] + public void OpenExplorerUi_NotCurrentScene_Rejects() + { + // Arrange + sceneStateProvider.IsCurrent.Returns(false); + + // Act + int result = restrictedActionsAPIImplementation.TryOpenExplorerUi((int)ExplorerUi.EuMap); + + // Assert + Assert.AreEqual((int)OpenExplorerUiResult.RejectedNotCurrentScene, result); + explorerUiActions.DidNotReceive().OpenSection(Arg.Any()); + } + + [Test] + [TestCase(0)] // underflow-safe: no user gesture has ever been recorded + [TestCase(5)] // stale: the last gesture is older than the allowed window + public void OpenExplorerUi_NoRecentGesture_Rejects(int lastUserInputTick) + { + // Arrange + sceneStateProvider.TickNumber.Returns((uint)10); + sceneStateProvider.LastUserInputTick.Returns((uint)lastUserInputTick); + + // Act + int result = restrictedActionsAPIImplementation.TryOpenExplorerUi((int)ExplorerUi.EuMap); + + // Assert + Assert.AreEqual((int)OpenExplorerUiResult.RejectedNoUserGesture, result); + explorerUiActions.DidNotReceive().OpenSection(Arg.Any()); + } + + [Test] + public void OpenExplorerUi_AlreadyOpen_Rejects() + { + // Arrange + explorerUiActions.OpenSection(Arg.Any()).Returns(OpenSectionResult.AlreadyOpen); + + // Act + int result = restrictedActionsAPIImplementation.TryOpenExplorerUi((int)ExplorerUi.EuMap); + + // Assert + Assert.AreEqual((int)OpenExplorerUiResult.RejectedAlreadyOpen, result); + } + + [Test] + public void OpenExplorerUi_FeatureDisabled_Rejects() + { + // Arrange + // COMMUNITIES is not force-enabled in the editor (unlike CAMERA_REEL / DISCOVER), + // so the default features registry reports it as disabled. + + // Act + int result = restrictedActionsAPIImplementation.TryOpenExplorerUi((int)ExplorerUi.EuCommunities); + + // Assert + Assert.AreEqual((int)OpenExplorerUiResult.RejectedFeatureDisabled, result); + explorerUiActions.DidNotReceive().OpenSection(Arg.Any()); + } + [Test] public void CopyToClipboard() { diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs new file mode 100644 index 00000000000..6896851f6cb --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs @@ -0,0 +1,60 @@ +using Cysharp.Threading.Tasks; +using DCL.CrdtEcsBridge.JsModulesImplementation; +using DCL.ExplorePanel; +using DCL.UI; +using MVC; + +namespace DCL.SceneRunner +{ + /// + /// DCL.Plugins-side implementation of . It lives here (rather than + /// next to the restricted-actions API) because it references + /// from DCL.Social, an assembly that already depends on SceneRuntime. + /// + public class ExplorerUiActions : IExplorerUiActions + { + private readonly IMVCManager mvcManager; + + private bool isExplorePanelOpen; + + public ExplorerUiActions(IMVCManager mvcManager) + { + this.mvcManager = mvcManager; + mvcManager.OnViewShowed += OnViewShowed; + mvcManager.OnViewClosed += OnViewClosed; + } + + public void Dispose() + { + mvcManager.OnViewShowed -= OnViewShowed; + mvcManager.OnViewClosed -= OnViewClosed; + } + + public OpenSectionResult OpenSection(ExploreSections section) + { + if (isExplorePanelOpen) + return OpenSectionResult.AlreadyOpen; + + OpenSectionAsync(section).Forget(); + return OpenSectionResult.Opened; + } + + private void OnViewShowed(IController controller) + { + if (controller is ExplorePanelController) + isExplorePanelOpen = true; + } + + private void OnViewClosed(IController controller) + { + if (controller is ExplorePanelController) + isExplorePanelOpen = false; + } + + private async UniTask OpenSectionAsync(ExploreSections section) + { + await UniTask.SwitchToMainThread(); + await mvcManager.ShowAsync(ExplorePanelController.IssueCommand(new ExplorePanelParameter(section))); + } + } +} diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs.meta b/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs.meta new file mode 100644 index 00000000000..613dac862ef --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ac414eae9e741154fbef2275a6bb3b14 \ No newline at end of file diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/ISceneStateProvider.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/ISceneStateProvider.cs index 5b1f6d71ff3..6e762b5efc8 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/ISceneStateProvider.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/ISceneStateProvider.cs @@ -13,6 +13,13 @@ public interface ISceneStateProvider uint TickNumber { get; set; } + /// + /// Tick at which the most recent non-hover pointer (down/up) result was written for this scene while + /// it was current. Zero means no user gesture has ever been recorded. Consumed by gesture-gated + /// restricted actions to reject calls that do not originate from recent user input. + /// + uint LastUserInputTick { get; set; } + ref readonly SceneEngineStartInfo EngineStartInfo { get; } void Start(SceneEngineStartInfo startInfo); diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/SceneStateProvider.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/SceneStateProvider.cs index e2fb065089b..77b770feb75 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/SceneStateProvider.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/SceneStateProvider.cs @@ -15,6 +15,8 @@ public class SceneStateProvider : ISceneStateProvider public uint TickNumber { get; set; } + public uint LastUserInputTick { get; set; } + public ref readonly SceneEngineStartInfo EngineStartInfo => ref engineStartInfo; public void Start(SceneEngineStartInfo startInfo) @@ -22,6 +24,7 @@ public void Start(SceneEngineStartInfo startInfo) State.Set(SceneState.Starting); engineStartInfo = startInfo; TickNumber = 0; + LastUserInputTick = 0; } } } diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs index 1ba1f18fe67..b6d047eed72 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs @@ -21,6 +21,8 @@ using DCL.PluginSystem.World.Dependencies; using DCL.Profiles; using DCL.Profiling; +using DCL.SceneRunner; +using DCL.SceneRuntime.Apis.RestrictedActionsApi; using DCL.SDKComponents.MediaStream; using DCL.SkyBox; using DCL.Utilities; @@ -38,7 +40,6 @@ using SceneRuntime.Apis.Modules.CommunicationsControllerApi; using SceneRuntime.Apis.Modules.EngineApi; using SceneRuntime.Apis.Modules.FetchApi; -using SceneRuntime.Apis.Modules.RestrictedActionsApi; using SceneRuntime.Apis.Modules.Runtime; using SceneRuntime.Apis.Modules.SceneApi; using SceneRuntime.ScenePermissions; @@ -241,7 +242,7 @@ protected WithRuntimeAndJsAPIBase( string installSource) : this( engineApi, - new RestrictedActionsAPIImplementation(mvcManager, syncDeps.ecsWorldSharedDependencies.SceneStateProvider, globalWorldActions, syncDeps.sceneData, syncDeps.permissionsProvider, systemClipboard, syncDeps.ECSWorldFacade.EcsWorld, syncDeps.ECSWorldFacade.PersistentEntities.Player), + new RestrictedActionsAPIImplementation(mvcManager, syncDeps.ecsWorldSharedDependencies.SceneStateProvider, globalWorldActions, syncDeps.sceneData, syncDeps.permissionsProvider, systemClipboard, syncDeps.ECSWorldFacade.EcsWorld, syncDeps.ECSWorldFacade.PersistentEntities.Player, new ExplorerUiActions(mvcManager)), new RuntimeImplementation(jsOperations, syncDeps.sceneData, realmData, webRequestController, skyboxSettings, roomHub, installSource), new SceneApiImplementation(syncDeps.sceneData), new ClientWebSocketApiImplementation(syncDeps.PoolsProvider, jsOperations, syncDeps.permissionsProvider), diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Tests/SceneFacadeShould.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Tests/SceneFacadeShould.cs index ccc38f68390..4f90d61f71c 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Tests/SceneFacadeShould.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Tests/SceneFacadeShould.cs @@ -23,6 +23,7 @@ using DCL.PluginSystem.World; using DCL.PluginSystem.World.Dependencies; using DCL.Profiles; +using DCL.SceneRuntime.Apis.RestrictedActionsApi; using DCL.SkyBox; using DCL.Utilities.Extensions; using DCL.Web3; @@ -45,7 +46,6 @@ using SceneRuntime.Apis.Modules.CommunicationsControllerApi; using SceneRuntime.Apis.Modules.EngineApi; using SceneRuntime.Apis.Modules.FetchApi; -using SceneRuntime.Apis.Modules.RestrictedActionsApi; using SceneRuntime.Apis.Modules.Runtime; using SceneRuntime.Apis.Modules.SceneApi; using SceneRuntime.Factory; diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/IRestrictedActionsAPI.cs b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/IRestrictedActionsAPI.cs index 599b716d98e..80fec74144c 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/IRestrictedActionsAPI.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/IRestrictedActionsAPI.cs @@ -1,11 +1,12 @@ using Cysharp.Threading.Tasks; using DCL.ECSComponents; +using System; using System.Threading; using UnityEngine; -namespace SceneRuntime.Apis.Modules.RestrictedActionsApi +namespace DCL.SceneRuntime.Apis.RestrictedActionsApi { - public interface IRestrictedActionsAPI + public interface IRestrictedActionsAPI : IDisposable { bool TryOpenExternalUrl(string url); @@ -21,6 +22,8 @@ public interface IRestrictedActionsAPI bool TryOpenNftDialog(string urn); + int TryOpenExplorerUi(int ui); + void TryCopyToClipboard(string text); void TryStopEmote(); diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/RestrictedActionsAPIWrapper.cs b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/RestrictedActionsAPIWrapper.cs index 44fecfbc5f5..02b66df953c 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/RestrictedActionsAPIWrapper.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/RestrictedActionsApi/RestrictedActionsAPIWrapper.cs @@ -1,11 +1,13 @@ using Cysharp.Threading.Tasks; using DCL.ECSComponents; using JetBrains.Annotations; +using SceneRuntime; +using SceneRuntime.Apis; using System.Threading; using UnityEngine; using Utility; -namespace SceneRuntime.Apis.Modules.RestrictedActionsApi +namespace DCL.SceneRuntime.Apis.RestrictedActionsApi { public class RestrictedActionsAPIWrapper : JsApiWrapper { @@ -93,6 +95,10 @@ public void CopyToClipboard(string text) => public bool OpenNftDialog(string urn) => api.TryOpenNftDialog(urn); + [UsedImplicitly] + public int OpenExplorerUi(int ui) => + api.TryOpenExplorerUi(ui); + [UsedImplicitly] public object StopEmote() { @@ -109,6 +115,7 @@ async UniTask StopEmoteAsync() } } - public override void Dispose() { } + public override void Dispose() => + api.Dispose(); } } diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/UserActions/UserActionsWrapper.cs b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/UserActions/UserActionsWrapper.cs index 28928b65719..41b23760745 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/UserActions/UserActionsWrapper.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/Apis/Modules/UserActions/UserActionsWrapper.cs @@ -1,6 +1,6 @@ using DCL.Diagnostics; +using DCL.SceneRuntime.Apis.RestrictedActionsApi; using JetBrains.Annotations; -using SceneRuntime.Apis.Modules.RestrictedActionsApi; using System; using System.Threading; using UnityEngine; diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/ISceneRuntime.cs b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/ISceneRuntime.cs index ac9bba31d85..a8016bf6884 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRuntime/ISceneRuntime.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRuntime/ISceneRuntime.cs @@ -6,6 +6,7 @@ using DCL.Multiplayer.Profiles.Poses; using DCL.Profiles; using DCL.Profiling; +using DCL.SceneRuntime.Apis.RestrictedActionsApi; using DCL.Web3; using DCL.Web3.Identities; using DCL.WebRequests; @@ -24,7 +25,6 @@ using SceneRuntime.Apis.Modules.FetchApi; using SceneRuntime.Apis.Modules.Players; using SceneRuntime.Apis.Modules.PortableExperiencesApi; -using SceneRuntime.Apis.Modules.RestrictedActionsApi; using SceneRuntime.Apis.Modules.Runtime; using SceneRuntime.Apis.Modules.SceneApi; using SceneRuntime.Apis.Modules.SignedFetch; diff --git a/Explorer/Assets/DCL/Interaction/Systems/WritePointerEventResultsSystem.cs b/Explorer/Assets/DCL/Interaction/Systems/WritePointerEventResultsSystem.cs index aec1bc7c9f4..bc75422c579 100644 --- a/Explorer/Assets/DCL/Interaction/Systems/WritePointerEventResultsSystem.cs +++ b/Explorer/Assets/DCL/Interaction/Systems/WritePointerEventResultsSystem.cs @@ -113,7 +113,16 @@ private bool AppendSingleResult(CRDTEntity sdkEntity, in Vector3 scenePosition, RaycastHit raycastHit = raycastHitPool.Get(); raycastHit.FillSDKRaycastHit(scenePosition, intent, sdkEntity); AppendMessage(sdkEntity, raycastHit, button, eventType); - return eventType is not (PointerEventType.PetHoverEnter or PointerEventType.PetHoverLeave); + + bool isNonHover = eventType is not (PointerEventType.PetHoverEnter or PointerEventType.PetHoverLeave); + + // Record a user gesture so gesture-gated restricted actions (e.g. OpenExplorerUi) can verify the + // call originated from recent input. Uses the same tick source as the appended result. Hover + // enter/leave are not gestures. + if (isNonHover) + sceneStateProvider.LastUserInputTick = sceneStateProvider.TickNumber; + + return isNonHover; } private void AppendMessage(CRDTEntity sdkEntity, RaycastHit? sdkHit, InputAction button, PointerEventType eventType) diff --git a/Explorer/Assets/Protocol/DecentralandProtocol/AvatarEmoteCommand.gen.cs b/Explorer/Assets/Protocol/DecentralandProtocol/AvatarEmoteCommand.gen.cs index 003d69284a9..20a3c8ac920 100644 --- a/Explorer/Assets/Protocol/DecentralandProtocol/AvatarEmoteCommand.gen.cs +++ b/Explorer/Assets/Protocol/DecentralandProtocol/AvatarEmoteCommand.gen.cs @@ -25,14 +25,17 @@ static AvatarEmoteCommandReflection() { byte[] descriptorData = global::System.Convert.FromBase64String( string.Concat( "CjZkZWNlbnRyYWxhbmQvc2RrL2NvbXBvbmVudHMvYXZhdGFyX2Vtb3RlX2Nv", - "bW1hbmQucHJvdG8SG2RlY2VudHJhbGFuZC5zZGsuY29tcG9uZW50cyJKChRQ", - "QkF2YXRhckVtb3RlQ29tbWFuZBIRCgllbW90ZV91cm4YASABKAkSDAoEbG9v", - "cBgCIAEoCBIRCgl0aW1lc3RhbXAYAyABKA1CFKoCEURDTC5FQ1NDb21wb25l", - "bnRzYgZwcm90bzM=")); + "bW1hbmQucHJvdG8SG2RlY2VudHJhbGFuZC5zZGsuY29tcG9uZW50cxo0ZGVj", + "ZW50cmFsYW5kL3Nkay9jb21wb25lbnRzL2NvbW1vbi9hdmF0YXJfbWFzay5w", + "cm90byKWAQoUUEJBdmF0YXJFbW90ZUNvbW1hbmQSEQoJZW1vdGVfdXJuGAEg", + "ASgJEgwKBGxvb3AYAiABKAgSEQoJdGltZXN0YW1wGAMgASgNEkEKBG1hc2sY", + "BCABKA4yLi5kZWNlbnRyYWxhbmQuc2RrLmNvbXBvbmVudHMuY29tbW9uLkF2", + "YXRhck1hc2tIAIgBAUIHCgVfbWFza0IUqgIRRENMLkVDU0NvbXBvbmVudHNi", + "BnByb3RvMw==")); descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, - new pbr::FileDescriptor[] { }, + new pbr::FileDescriptor[] { global::DCL.ECSComponents.AvatarMaskReflection.Descriptor, }, new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] { - new pbr::GeneratedClrTypeInfo(typeof(global::DCL.ECSComponents.PBAvatarEmoteCommand), global::DCL.ECSComponents.PBAvatarEmoteCommand.Parser, new[]{ "EmoteUrn", "Loop", "Timestamp" }, null, null, null, null) + new pbr::GeneratedClrTypeInfo(typeof(global::DCL.ECSComponents.PBAvatarEmoteCommand), global::DCL.ECSComponents.PBAvatarEmoteCommand.Parser, new[]{ "EmoteUrn", "Loop", "Timestamp", "Mask" }, new[]{ "Mask" }, null, null, null) })); } #endregion @@ -51,6 +54,7 @@ public sealed partial class PBAvatarEmoteCommand : pb::IMessage _parser = new pb::MessageParser(() => new PBAvatarEmoteCommand()); private pb::UnknownFieldSet _unknownFields; + private int _hasBits0; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public static pb::MessageParser Parser { get { return _parser; } } @@ -78,9 +82,11 @@ public PBAvatarEmoteCommand() { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public PBAvatarEmoteCommand(PBAvatarEmoteCommand other) : this() { + _hasBits0 = other._hasBits0; emoteUrn_ = other.emoteUrn_; loop_ = other.loop_; timestamp_ = other.timestamp_; + mask_ = other.mask_; _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); } @@ -129,6 +135,33 @@ public uint Timestamp { } } + /// Field number for the "mask" field. + public const int MaskFieldNumber = 4; + private readonly static global::DCL.ECSComponents.AvatarMask MaskDefaultValue = global::DCL.ECSComponents.AvatarMask.AmUpperBody; + + private global::DCL.ECSComponents.AvatarMask mask_; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::DCL.ECSComponents.AvatarMask Mask { + get { if ((_hasBits0 & 1) != 0) { return mask_; } else { return MaskDefaultValue; } } + set { + _hasBits0 |= 1; + mask_ = value; + } + } + /// Gets whether the "mask" field is set + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool HasMask { + get { return (_hasBits0 & 1) != 0; } + } + /// Clears the value of the "mask" field + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void ClearMask() { + _hasBits0 &= ~1; + } + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override bool Equals(object other) { @@ -147,6 +180,7 @@ public bool Equals(PBAvatarEmoteCommand other) { if (EmoteUrn != other.EmoteUrn) return false; if (Loop != other.Loop) return false; if (Timestamp != other.Timestamp) return false; + if (Mask != other.Mask) return false; return Equals(_unknownFields, other._unknownFields); } @@ -157,6 +191,7 @@ public override int GetHashCode() { if (EmoteUrn.Length != 0) hash ^= EmoteUrn.GetHashCode(); if (Loop != false) hash ^= Loop.GetHashCode(); if (Timestamp != 0) hash ^= Timestamp.GetHashCode(); + if (HasMask) hash ^= Mask.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -187,6 +222,10 @@ public void WriteTo(pb::CodedOutputStream output) { output.WriteRawTag(24); output.WriteUInt32(Timestamp); } + if (HasMask) { + output.WriteRawTag(32); + output.WriteEnum((int) Mask); + } if (_unknownFields != null) { _unknownFields.WriteTo(output); } @@ -209,6 +248,10 @@ public void WriteTo(pb::CodedOutputStream output) { output.WriteRawTag(24); output.WriteUInt32(Timestamp); } + if (HasMask) { + output.WriteRawTag(32); + output.WriteEnum((int) Mask); + } if (_unknownFields != null) { _unknownFields.WriteTo(ref output); } @@ -228,6 +271,9 @@ public int CalculateSize() { if (Timestamp != 0) { size += 1 + pb::CodedOutputStream.ComputeUInt32Size(Timestamp); } + if (HasMask) { + size += 1 + pb::CodedOutputStream.ComputeEnumSize((int) Mask); + } if (_unknownFields != null) { size += _unknownFields.CalculateSize(); } @@ -249,6 +295,9 @@ public void MergeFrom(PBAvatarEmoteCommand other) { if (other.Timestamp != 0) { Timestamp = other.Timestamp; } + if (other.HasMask) { + Mask = other.Mask; + } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); } @@ -280,6 +329,10 @@ public void MergeFrom(pb::CodedInputStream input) { Timestamp = input.ReadUInt32(); break; } + case 32: { + Mask = (global::DCL.ECSComponents.AvatarMask) input.ReadEnum(); + break; + } } } #endif @@ -311,6 +364,10 @@ public void MergeFrom(pb::CodedInputStream input) { Timestamp = input.ReadUInt32(); break; } + case 32: { + Mask = (global::DCL.ECSComponents.AvatarMask) input.ReadEnum(); + break; + } } } } diff --git a/Explorer/Assets/Protocol/DecentralandProtocol/RestrictedActions.gen.cs b/Explorer/Assets/Protocol/DecentralandProtocol/RestrictedActions.gen.cs index 23f30a74040..d235fb41162 100644 --- a/Explorer/Assets/Protocol/DecentralandProtocol/RestrictedActions.gen.cs +++ b/Explorer/Assets/Protocol/DecentralandProtocol/RestrictedActions.gen.cs @@ -50,7 +50,18 @@ static RestrictedActionsReflection() { "Y2Nlc3MYASABKAgiFgoUVHJpZ2dlckVtb3RlUmVzcG9uc2UiJwoUTW92ZVBs", "YXllclRvUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCIUChJUZWxlcG9ydFRv", "UmVzcG9uc2UiJgoWQ29weVRvQ2xpcGJvYXJkUmVxdWVzdBIMCgR0ZXh0GAEg", - "ASgJIg8KDUVtcHR5UmVzcG9uc2UiEgoQU3RvcEVtb3RlUmVxdWVzdDL1CAoY", + "ASgJIg8KDUVtcHR5UmVzcG9uc2UiEgoQU3RvcEVtb3RlUmVxdWVzdCJJChVP", + "cGVuRXhwbG9yZXJVaVJlcXVlc3QSMAoCdWkYASABKA4yJC5kZWNlbnRyYWxh", + "bmQua2VybmVsLmFwaXMuRXhwbG9yZXJVaSJdChZPcGVuRXhwbG9yZXJVaVJl", + "c3BvbnNlEkMKC29wZW5fcmVzdWx0GAEgASgOMi4uZGVjZW50cmFsYW5kLmtl", + "cm5lbC5hcGlzLk9wZW5FeHBsb3JlclVpUmVzdWx0KpQBCgpFeHBsb3JlclVp", + "EhIKDkVVX1VOU1BFQ0lGSUVEEAASCgoGRVVfTUFQEAESDwoLRVVfU0VUVElO", + "R1MQAhIPCgtFVV9CQUNLUEFDSxADEhIKDkVVX0NBTUVSQV9SRUVMEAQSEgoO", + "RVVfQ09NTVVOSVRJRVMQBRINCglFVV9QTEFDRVMQBhINCglFVV9FVkVOVFMQ", + "ByqrAQoUT3BlbkV4cGxvcmVyVWlSZXN1bHQSDwoLVU5TUEVDSUZJRUQQABIK", + "CgZPUEVORUQQARIeChpSRUpFQ1RFRF9OT1RfQ1VSUkVOVF9TQ0VORRACEhkK", + "FVJFSkVDVEVEX0FMUkVBRFlfT1BFThADEh0KGVJFSkVDVEVEX0ZFQVRVUkVf", + "RElTQUJMRUQQBBIcChhSRUpFQ1RFRF9OT19VU0VSX0dFU1RVUkUQBTLsCQoY", "UmVzdHJpY3RlZEFjdGlvbnNTZXJ2aWNlEm8KDE1vdmVQbGF5ZXJUbxItLmRl", "Y2VudHJhbGFuZC5rZXJuZWwuYXBpcy5Nb3ZlUGxheWVyVG9SZXF1ZXN0Gi4u", "ZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlzLk1vdmVQbGF5ZXJUb1Jlc3BvbnNl", @@ -76,10 +87,13 @@ static RestrictedActionsReflection() { "dWVzdBonLmRlY2VudHJhbGFuZC5rZXJuZWwuYXBpcy5FbXB0eVJlc3BvbnNl", "IgASZAoJU3RvcEVtb3RlEiouZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlzLlN0", "b3BFbW90ZVJlcXVlc3QaKS5kZWNlbnRyYWxhbmQua2VybmVsLmFwaXMuU3Vj", - "Y2Vzc1Jlc3BvbnNlIgBiBnByb3RvMw==")); + "Y2Vzc1Jlc3BvbnNlIgASdQoOT3BlbkV4cGxvcmVyVWkSLy5kZWNlbnRyYWxh", + "bmQua2VybmVsLmFwaXMuT3BlbkV4cGxvcmVyVWlSZXF1ZXN0GjAuZGVjZW50", + "cmFsYW5kLmtlcm5lbC5hcGlzLk9wZW5FeHBsb3JlclVpUmVzcG9uc2UiAGIG", + "cHJvdG8z")); descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, new pbr::FileDescriptor[] { global::Decentraland.Common.VectorsReflection.Descriptor, global::DCL.ECSComponents.AvatarMaskReflection.Descriptor, }, - new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] { + new pbr::GeneratedClrTypeInfo(new[] {typeof(global::Decentraland.Kernel.Apis.ExplorerUi), typeof(global::Decentraland.Kernel.Apis.OpenExplorerUiResult), }, null, new pbr::GeneratedClrTypeInfo[] { new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.MovePlayerToRequest), global::Decentraland.Kernel.Apis.MovePlayerToRequest.Parser, new[]{ "NewRelativePosition", "CameraTarget", "AvatarTarget", "Duration" }, new[]{ "CameraTarget", "AvatarTarget", "Duration" }, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.TeleportToRequest), global::Decentraland.Kernel.Apis.TeleportToRequest.Parser, new[]{ "WorldCoordinates" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.TriggerEmoteRequest), global::Decentraland.Kernel.Apis.TriggerEmoteRequest.Parser, new[]{ "PredefinedEmote", "Mask" }, new[]{ "Mask" }, null, null, null), @@ -95,12 +109,57 @@ static RestrictedActionsReflection() { new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.TeleportToResponse), global::Decentraland.Kernel.Apis.TeleportToResponse.Parser, null, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.CopyToClipboardRequest), global::Decentraland.Kernel.Apis.CopyToClipboardRequest.Parser, new[]{ "Text" }, null, null, null, null), new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.EmptyResponse), global::Decentraland.Kernel.Apis.EmptyResponse.Parser, null, null, null, null, null), - new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.StopEmoteRequest), global::Decentraland.Kernel.Apis.StopEmoteRequest.Parser, null, null, null, null, null) + new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.StopEmoteRequest), global::Decentraland.Kernel.Apis.StopEmoteRequest.Parser, null, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.OpenExplorerUiRequest), global::Decentraland.Kernel.Apis.OpenExplorerUiRequest.Parser, new[]{ "Ui" }, null, null, null, null), + new pbr::GeneratedClrTypeInfo(typeof(global::Decentraland.Kernel.Apis.OpenExplorerUiResponse), global::Decentraland.Kernel.Apis.OpenExplorerUiResponse.Parser, new[]{ "OpenResult" }, null, null, null, null) })); } #endregion } + #region Enums + /// + /// Identifies which fullscreen explorer panel OpenExplorerUi targets. + /// Additive: new panels are appended with the next number; existing values never change. + /// + public enum ExplorerUi { + [pbr::OriginalName("EU_UNSPECIFIED")] EuUnspecified = 0, + [pbr::OriginalName("EU_MAP")] EuMap = 1, + [pbr::OriginalName("EU_SETTINGS")] EuSettings = 2, + [pbr::OriginalName("EU_BACKPACK")] EuBackpack = 3, + [pbr::OriginalName("EU_CAMERA_REEL")] EuCameraReel = 4, + [pbr::OriginalName("EU_COMMUNITIES")] EuCommunities = 5, + [pbr::OriginalName("EU_PLACES")] EuPlaces = 6, + [pbr::OriginalName("EU_EVENTS")] EuEvents = 7, + } + + /// + /// Verdict of an OpenExplorerUi request (enum rather than a bool so new outcomes stay expressible). + /// Additive: new rejection reasons may be appended; existing values never change. + /// + public enum OpenExplorerUiResult { + [pbr::OriginalName("UNSPECIFIED")] Unspecified = 0, + [pbr::OriginalName("OPENED")] Opened = 1, + /// + /// the standard restricted-actions current-scene gate rejected the call + /// + [pbr::OriginalName("REJECTED_NOT_CURRENT_SCENE")] RejectedNotCurrentScene = 2, + /// + /// a fullscreen panel is already open + /// + [pbr::OriginalName("REJECTED_ALREADY_OPEN")] RejectedAlreadyOpen = 3, + /// + /// the requested section is hidden by feature flags + /// + [pbr::OriginalName("REJECTED_FEATURE_DISABLED")] RejectedFeatureDisabled = 4, + /// + /// rejected: the call did not originate from a user gesture + /// + [pbr::OriginalName("REJECTED_NO_USER_GESTURE")] RejectedNoUserGesture = 5, + } + + #endregion + #region Messages [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] public sealed partial class MovePlayerToRequest : pb::IMessage @@ -3463,6 +3522,407 @@ public void MergeFrom(pb::CodedInputStream input) { } + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class OpenExplorerUiRequest : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new OpenExplorerUiRequest()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::Decentraland.Kernel.Apis.RestrictedActionsReflection.Descriptor.MessageTypes[16]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public OpenExplorerUiRequest() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public OpenExplorerUiRequest(OpenExplorerUiRequest other) : this() { + ui_ = other.ui_; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public OpenExplorerUiRequest Clone() { + return new OpenExplorerUiRequest(this); + } + + /// Field number for the "ui" field. + public const int UiFieldNumber = 1; + private global::Decentraland.Kernel.Apis.ExplorerUi ui_ = global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Decentraland.Kernel.Apis.ExplorerUi Ui { + get { return ui_; } + set { + ui_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as OpenExplorerUiRequest); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(OpenExplorerUiRequest other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (Ui != other.Ui) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified) hash ^= Ui.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified) { + output.WriteRawTag(8); + output.WriteEnum((int) Ui); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified) { + output.WriteRawTag(8); + output.WriteEnum((int) Ui); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified) { + size += 1 + pb::CodedOutputStream.ComputeEnumSize((int) Ui); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(OpenExplorerUiRequest other) { + if (other == null) { + return; + } + if (other.Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified) { + Ui = other.Ui; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 8: { + Ui = (global::Decentraland.Kernel.Apis.ExplorerUi) input.ReadEnum(); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 8: { + Ui = (global::Decentraland.Kernel.Apis.ExplorerUi) input.ReadEnum(); + break; + } + } + } + } + #endif + + } + + [global::System.Diagnostics.DebuggerDisplayAttribute("{ToString(),nq}")] + public sealed partial class OpenExplorerUiResponse : pb::IMessage + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + , pb::IBufferMessage + #endif + { + private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new OpenExplorerUiResponse()); + private pb::UnknownFieldSet _unknownFields; + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pb::MessageParser Parser { get { return _parser; } } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public static pbr::MessageDescriptor Descriptor { + get { return global::Decentraland.Kernel.Apis.RestrictedActionsReflection.Descriptor.MessageTypes[17]; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + pbr::MessageDescriptor pb::IMessage.Descriptor { + get { return Descriptor; } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public OpenExplorerUiResponse() { + OnConstruction(); + } + + partial void OnConstruction(); + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public OpenExplorerUiResponse(OpenExplorerUiResponse other) : this() { + openResult_ = other.openResult_; + _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public OpenExplorerUiResponse Clone() { + return new OpenExplorerUiResponse(this); + } + + /// Field number for the "open_result" field. + public const int OpenResultFieldNumber = 1; + private global::Decentraland.Kernel.Apis.OpenExplorerUiResult openResult_ = global::Decentraland.Kernel.Apis.OpenExplorerUiResult.Unspecified; + /// + /// Carries only the verdict of the open action. Post-open signals (panel closed, flow + /// outcomes, milestones) are deliberately out of scope here and are delivered through a + /// separate additive event channel (a grow-only scene component), keeping this contract stable. + /// + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public global::Decentraland.Kernel.Apis.OpenExplorerUiResult OpenResult { + get { return openResult_; } + set { + openResult_ = value; + } + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override bool Equals(object other) { + return Equals(other as OpenExplorerUiResponse); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public bool Equals(OpenExplorerUiResponse other) { + if (ReferenceEquals(other, null)) { + return false; + } + if (ReferenceEquals(other, this)) { + return true; + } + if (OpenResult != other.OpenResult) return false; + return Equals(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override int GetHashCode() { + int hash = 1; + if (OpenResult != global::Decentraland.Kernel.Apis.OpenExplorerUiResult.Unspecified) hash ^= OpenResult.GetHashCode(); + if (_unknownFields != null) { + hash ^= _unknownFields.GetHashCode(); + } + return hash; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public override string ToString() { + return pb::JsonFormatter.ToDiagnosticString(this); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void WriteTo(pb::CodedOutputStream output) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + output.WriteRawMessage(this); + #else + if (OpenResult != global::Decentraland.Kernel.Apis.OpenExplorerUiResult.Unspecified) { + output.WriteRawTag(8); + output.WriteEnum((int) OpenResult); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(output); + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { + if (OpenResult != global::Decentraland.Kernel.Apis.OpenExplorerUiResult.Unspecified) { + output.WriteRawTag(8); + output.WriteEnum((int) OpenResult); + } + if (_unknownFields != null) { + _unknownFields.WriteTo(ref output); + } + } + #endif + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public int CalculateSize() { + int size = 0; + if (OpenResult != global::Decentraland.Kernel.Apis.OpenExplorerUiResult.Unspecified) { + size += 1 + pb::CodedOutputStream.ComputeEnumSize((int) OpenResult); + } + if (_unknownFields != null) { + size += _unknownFields.CalculateSize(); + } + return size; + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(OpenExplorerUiResponse other) { + if (other == null) { + return; + } + if (other.OpenResult != global::Decentraland.Kernel.Apis.OpenExplorerUiResult.Unspecified) { + OpenResult = other.OpenResult; + } + _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); + } + + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + public void MergeFrom(pb::CodedInputStream input) { + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + input.ReadRawMessage(this); + #else + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input); + break; + case 8: { + OpenResult = (global::Decentraland.Kernel.Apis.OpenExplorerUiResult) input.ReadEnum(); + break; + } + } + } + #endif + } + + #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE + [global::System.Diagnostics.DebuggerNonUserCodeAttribute] + [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] + void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) { + uint tag; + while ((tag = input.ReadTag()) != 0) { + if ((tag & 7) == 4) { + // Abort on any end group tag. + return; + } + switch(tag) { + default: + _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input); + break; + case 8: { + OpenResult = (global::Decentraland.Kernel.Apis.OpenExplorerUiResult) input.ReadEnum(); + break; + } + } + } + } + #endif + + } + #endregion } diff --git a/Explorer/Assets/StreamingAssets/Js/Modules/RestrictedActions.js b/Explorer/Assets/StreamingAssets/Js/Modules/RestrictedActions.js index 211d5d0889e..dcef49923a5 100644 --- a/Explorer/Assets/StreamingAssets/Js/Modules/RestrictedActions.js +++ b/Explorer/Assets/StreamingAssets/Js/Modules/RestrictedActions.js @@ -1,6 +1,36 @@ // Responses should always correspond to the protocol definitions at // https://github.com/decentraland/protocol/blob/main/proto/decentraland/kernel/apis/restricted_actions.proto +// Protocol enums exposed to scenes as runtime values (mirroring the generated +// ts-proto enums: both forward name->value and reverse value->name mappings). +// Scenes import these from `~system/RestrictedActions` and use them at runtime, +// e.g. `openExplorerUi({ ui: ExplorerUi.EU_MAP })` and `OpenExplorerUiResult[openResult]`. +function makeEnum(entries) { + const e = {}; + for (const [name, value] of entries) { e[name] = value; e[value] = name; } + return e; +} + +module.exports.ExplorerUi = makeEnum([ + ['EU_UNSPECIFIED', 0], + ['EU_MAP', 1], + ['EU_SETTINGS', 2], + ['EU_BACKPACK', 3], + ['EU_CAMERA_REEL', 4], + ['EU_COMMUNITIES', 5], + ['EU_PLACES', 6], + ['EU_EVENTS', 7], +]); + +module.exports.OpenExplorerUiResult = makeEnum([ + ['UNSPECIFIED', 0], + ['OPENED', 1], + ['REJECTED_NOT_CURRENT_SCENE', 2], + ['REJECTED_ALREADY_OPEN', 3], + ['REJECTED_FEATURE_DISABLED', 4], + ['REJECTED_NO_USER_GESTURE', 5], +]); + module.exports.movePlayerTo = async function(message) { const cameraTarget = message.cameraTarget != undefined const avatarTarget = message.avatarTarget != undefined @@ -62,6 +92,11 @@ module.exports.openNftDialog = async function(message) { }; } +module.exports.openExplorerUi = async function(message) { + const openResult = UnityRestrictedActionsApi.OpenExplorerUi(message.ui) + return { openResult }; +} + module.exports.setCommunicationsAdapter = async function(message) { console.log('JSMODULE: setCommunicationsAdapter') return { diff --git a/Explorer/DCL.EditMode.Tests.csproj.DotSettings b/Explorer/DCL.EditMode.Tests.csproj.DotSettings index 6769ea07494..c5777b99330 100644 --- a/Explorer/DCL.EditMode.Tests.csproj.DotSettings +++ b/Explorer/DCL.EditMode.Tests.csproj.DotSettings @@ -5,6 +5,7 @@ True True True + True True True True diff --git a/Explorer/SceneRuntime.csproj.DotSettings b/Explorer/SceneRuntime.csproj.DotSettings index b6666aa0390..798fbd4ac03 100644 --- a/Explorer/SceneRuntime.csproj.DotSettings +++ b/Explorer/SceneRuntime.csproj.DotSettings @@ -1,5 +1,6 @@  True + True True True True \ No newline at end of file diff --git a/scripts/package-lock.json b/scripts/package-lock.json index 42f0aa2d89b..84119a6d5c1 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "Apache-2.0", "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-29744166153.commit-0e8b20c.tgz", "@protobuf-ts/protoc": "^2.11.0", "@types/fs-extra": "^11.0.1", "@types/glob": "^8.0.1", @@ -37,9 +37,9 @@ } }, "node_modules/@dcl/protocol": { - "version": "1.0.0-29092627614.commit-d2f272f", - "resolved": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-29092627614.commit-d2f272f.tgz", - "integrity": "sha512-ETnLajHgCy0KCwnQ+oC5faEY53sa/y29Z1/0kHmiTsq5EWZLl1r6kwnFBSV0NrZD7iw8Cmz0XX694XonYqH37w==", + "version": "1.0.0-29744166153.commit-0e8b20c", + "resolved": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-29744166153.commit-0e8b20c.tgz", + "integrity": "sha512-8npy9TaxTARV2XMqZjJlLVO/aG+ZHvLUV3GsI+yrNFxx4Rh+3Z6IJQWEwFM9fT5O46K0LnAgMrqEYqhypxk4hg==", "license": "Apache-2.0", "dependencies": { "@dcl/ts-proto": "1.154.0", @@ -608,8 +608,8 @@ } }, "@dcl/protocol": { - "version": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-29092627614.commit-d2f272f.tgz", - "integrity": "sha512-ETnLajHgCy0KCwnQ+oC5faEY53sa/y29Z1/0kHmiTsq5EWZLl1r6kwnFBSV0NrZD7iw8Cmz0XX694XonYqH37w==", + "version": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-29744166153.commit-0e8b20c.tgz", + "integrity": "sha512-8npy9TaxTARV2XMqZjJlLVO/aG+ZHvLUV3GsI+yrNFxx4Rh+3Z6IJQWEwFM9fT5O46K0LnAgMrqEYqhypxk4hg==", "requires": { "@dcl/ts-proto": "1.154.0", "protobufjs": "7.2.4" diff --git a/scripts/package.json b/scripts/package.json index f57bf4cb1c0..1de4469e5ec 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -16,7 +16,7 @@ "typescript": "^4.2.3" }, "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-29744166153.commit-0e8b20c.tgz", "@protobuf-ts/protoc": "^2.11.0", "@types/fs-extra": "^11.0.1", "@types/glob": "^8.0.1", From ac4351c8cba7f47bd111047c5679bd05474a63e8 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 23 Jul 2026 21:18:07 +0200 Subject: [PATCH 3/9] comments clean up --- .../RestrictedActionsAPIImplementation.cs | 1 + .../Infrastructure/SceneRunner/ExplorerUiActions.cs | 12 ++++++------ .../SceneRunner/Scene/ISceneStateProvider.cs | 4 +--- .../Systems/WritePointerEventResultsSystem.cs | 3 --- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs index f82a6ba934c..3000605929a 100644 --- a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs @@ -338,6 +338,7 @@ private static bool TryMapExplorerUi(ExplorerUi ui, out ExploreSections section, section = ExploreSections.Events; gatingFeature = FeatureId.DISCOVER; return true; + case ExplorerUi.EuUnspecified: default: section = default(ExploreSections); gatingFeature = null; diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs index 6896851f6cb..323fae586aa 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs @@ -39,6 +39,12 @@ public OpenSectionResult OpenSection(ExploreSections section) return OpenSectionResult.Opened; } + private async UniTask OpenSectionAsync(ExploreSections section) + { + await UniTask.SwitchToMainThread(); + await mvcManager.ShowAsync(ExplorePanelController.IssueCommand(new ExplorePanelParameter(section))); + } + private void OnViewShowed(IController controller) { if (controller is ExplorePanelController) @@ -50,11 +56,5 @@ private void OnViewClosed(IController controller) if (controller is ExplorePanelController) isExplorePanelOpen = false; } - - private async UniTask OpenSectionAsync(ExploreSections section) - { - await UniTask.SwitchToMainThread(); - await mvcManager.ShowAsync(ExplorePanelController.IssueCommand(new ExplorePanelParameter(section))); - } } } diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/ISceneStateProvider.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/ISceneStateProvider.cs index 6e762b5efc8..b26c90df127 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/ISceneStateProvider.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/Scene/ISceneStateProvider.cs @@ -14,9 +14,7 @@ public interface ISceneStateProvider uint TickNumber { get; set; } /// - /// Tick at which the most recent non-hover pointer (down/up) result was written for this scene while - /// it was current. Zero means no user gesture has ever been recorded. Consumed by gesture-gated - /// restricted actions to reject calls that do not originate from recent user input. + /// Tick at which the most recent non-hover pointer (down/up) result was written for this scene. /// uint LastUserInputTick { get; set; } diff --git a/Explorer/Assets/DCL/Interaction/Systems/WritePointerEventResultsSystem.cs b/Explorer/Assets/DCL/Interaction/Systems/WritePointerEventResultsSystem.cs index bc75422c579..a878a99e786 100644 --- a/Explorer/Assets/DCL/Interaction/Systems/WritePointerEventResultsSystem.cs +++ b/Explorer/Assets/DCL/Interaction/Systems/WritePointerEventResultsSystem.cs @@ -116,9 +116,6 @@ private bool AppendSingleResult(CRDTEntity sdkEntity, in Vector3 scenePosition, bool isNonHover = eventType is not (PointerEventType.PetHoverEnter or PointerEventType.PetHoverLeave); - // Record a user gesture so gesture-gated restricted actions (e.g. OpenExplorerUi) can verify the - // call originated from recent input. Uses the same tick source as the appended result. Hover - // enter/leave are not gestures. if (isNonHover) sceneStateProvider.LastUserInputTick = sceneStateProvider.TickNumber; From 02b083a06109ead2705936cb8b5e7dd5657a2236 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Thu, 23 Jul 2026 21:29:22 +0200 Subject: [PATCH 4/9] adjust ExplorerUi: SETTINGS as zero value, drop UNSPECIFIED 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 --- .../RestrictedActions/IExplorerUiActions.cs | 12 +- .../RestrictedActionsAPIImplementation.cs | 8 +- ...estrictedActionsAPIImplementationShould.cs | 19 ++- .../SceneRunner/ExplorerUiActions.cs | 7 +- .../RestrictedActions.gen.cs | 115 +++++++++--------- .../Js/Modules/RestrictedActions.js | 17 ++- scripts/package-lock.json | 12 +- scripts/package.json | 2 +- 8 files changed, 93 insertions(+), 99 deletions(-) diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs index 3ee75bd2766..afbf90ddb09 100644 --- a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/IExplorerUiActions.cs @@ -1,19 +1,11 @@ using DCL.UI; +using Decentraland.Kernel.Apis; using System; namespace DCL.CrdtEcsBridge.JsModulesImplementation { - /// - /// Outcome of an request. - /// - public enum OpenSectionResult - { - Opened, - AlreadyOpen, - } - public interface IExplorerUiActions : IDisposable { - OpenSectionResult OpenSection(ExploreSections section); + OpenExplorerUiResult OpenSection(ExploreSections section); } } diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs index 3000605929a..325b891b2b7 100644 --- a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs @@ -236,10 +236,7 @@ public int TryOpenExplorerUi(int ui) return (int)OpenExplorerUiResult.RejectedFeatureDisabled; } - if (explorerUiActions.OpenSection(section) == OpenSectionResult.AlreadyOpen) - return (int)OpenExplorerUiResult.RejectedAlreadyOpen; - - return (int)OpenExplorerUiResult.Opened; + return (int)explorerUiActions.OpenSection(section); } public void Dispose() @@ -304,7 +301,7 @@ private async UniTask CopyToClipboardAsync(string text) /// /// Maps a protocol value to its explore panel section and, when the /// section is behind a synchronous feature flag, the that gates it. - /// Returns false for unspecified or unknown values so the caller can reject the request. + /// Returns false for unknown values so the caller can reject the request. /// private static bool TryMapExplorerUi(ExplorerUi ui, out ExploreSections section, out FeatureId? gatingFeature) { @@ -338,7 +335,6 @@ private static bool TryMapExplorerUi(ExplorerUi ui, out ExploreSections section, section = ExploreSections.Events; gatingFeature = FeatureId.DISCOVER; return true; - case ExplorerUi.EuUnspecified: default: section = default(ExploreSections); gatingFeature = null; diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs index 284e0843a8b..5a8b9510082 100644 --- a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs @@ -58,7 +58,7 @@ public void SetUp() }); systemClipboard = Substitute.For(); explorerUiActions = Substitute.For(); - explorerUiActions.OpenSection(Arg.Any()).Returns(OpenSectionResult.Opened); + explorerUiActions.OpenSection(Arg.Any()).Returns(OpenExplorerUiResult.Opened); sceneWorld = World.Create(); Entity scenePlayerEntity = sceneWorld.Create(); restrictedActionsAPIImplementation = new RestrictedActionsAPIImplementation( @@ -204,16 +204,27 @@ public void OpenExplorerUi_NoRecentGesture_Rejects(int lastUserInputTick) } [Test] - public void OpenExplorerUi_AlreadyOpen_Rejects() + public void OpenExplorerUi_AlreadyOpen_ReturnsWasAlreadyOpen() { // Arrange - explorerUiActions.OpenSection(Arg.Any()).Returns(OpenSectionResult.AlreadyOpen); + explorerUiActions.OpenSection(Arg.Any()).Returns(OpenExplorerUiResult.WasAlreadyOpen); // Act int result = restrictedActionsAPIImplementation.TryOpenExplorerUi((int)ExplorerUi.EuMap); // Assert - Assert.AreEqual((int)OpenExplorerUiResult.RejectedAlreadyOpen, result); + Assert.AreEqual((int)OpenExplorerUiResult.WasAlreadyOpen, result); + } + + [Test] + public void OpenExplorerUi_UnknownUiValue_Rejects() + { + // Act: 99 is not a member of the ExplorerUi enum, so the section mapping must fail. + int result = restrictedActionsAPIImplementation.TryOpenExplorerUi(99); + + // Assert + Assert.AreEqual((int)OpenExplorerUiResult.RejectedFeatureDisabled, result); + explorerUiActions.DidNotReceive().OpenSection(Arg.Any()); } [Test] diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs index 323fae586aa..7ece75306e4 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs @@ -2,6 +2,7 @@ using DCL.CrdtEcsBridge.JsModulesImplementation; using DCL.ExplorePanel; using DCL.UI; +using Decentraland.Kernel.Apis; using MVC; namespace DCL.SceneRunner @@ -30,13 +31,13 @@ public void Dispose() mvcManager.OnViewClosed -= OnViewClosed; } - public OpenSectionResult OpenSection(ExploreSections section) + public OpenExplorerUiResult OpenSection(ExploreSections section) { if (isExplorePanelOpen) - return OpenSectionResult.AlreadyOpen; + return OpenExplorerUiResult.WasAlreadyOpen; OpenSectionAsync(section).Forget(); - return OpenSectionResult.Opened; + return OpenExplorerUiResult.Opened; } private async UniTask OpenSectionAsync(ExploreSections section) diff --git a/Explorer/Assets/Protocol/DecentralandProtocol/RestrictedActions.gen.cs b/Explorer/Assets/Protocol/DecentralandProtocol/RestrictedActions.gen.cs index d235fb41162..809b9a191e3 100644 --- a/Explorer/Assets/Protocol/DecentralandProtocol/RestrictedActions.gen.cs +++ b/Explorer/Assets/Protocol/DecentralandProtocol/RestrictedActions.gen.cs @@ -54,43 +54,42 @@ static RestrictedActionsReflection() { "cGVuRXhwbG9yZXJVaVJlcXVlc3QSMAoCdWkYASABKA4yJC5kZWNlbnRyYWxh", "bmQua2VybmVsLmFwaXMuRXhwbG9yZXJVaSJdChZPcGVuRXhwbG9yZXJVaVJl", "c3BvbnNlEkMKC29wZW5fcmVzdWx0GAEgASgOMi4uZGVjZW50cmFsYW5kLmtl", - "cm5lbC5hcGlzLk9wZW5FeHBsb3JlclVpUmVzdWx0KpQBCgpFeHBsb3JlclVp", - "EhIKDkVVX1VOU1BFQ0lGSUVEEAASCgoGRVVfTUFQEAESDwoLRVVfU0VUVElO", - "R1MQAhIPCgtFVV9CQUNLUEFDSxADEhIKDkVVX0NBTUVSQV9SRUVMEAQSEgoO", - "RVVfQ09NTVVOSVRJRVMQBRINCglFVV9QTEFDRVMQBhINCglFVV9FVkVOVFMQ", - "ByqrAQoUT3BlbkV4cGxvcmVyVWlSZXN1bHQSDwoLVU5TUEVDSUZJRUQQABIK", - "CgZPUEVORUQQARIeChpSRUpFQ1RFRF9OT1RfQ1VSUkVOVF9TQ0VORRACEhkK", - "FVJFSkVDVEVEX0FMUkVBRFlfT1BFThADEh0KGVJFSkVDVEVEX0ZFQVRVUkVf", - "RElTQUJMRUQQBBIcChhSRUpFQ1RFRF9OT19VU0VSX0dFU1RVUkUQBTLsCQoY", - "UmVzdHJpY3RlZEFjdGlvbnNTZXJ2aWNlEm8KDE1vdmVQbGF5ZXJUbxItLmRl", - "Y2VudHJhbGFuZC5rZXJuZWwuYXBpcy5Nb3ZlUGxheWVyVG9SZXF1ZXN0Gi4u", - "ZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlzLk1vdmVQbGF5ZXJUb1Jlc3BvbnNl", - "IgASaQoKVGVsZXBvcnRUbxIrLmRlY2VudHJhbGFuZC5rZXJuZWwuYXBpcy5U", - "ZWxlcG9ydFRvUmVxdWVzdBosLmRlY2VudHJhbGFuZC5rZXJuZWwuYXBpcy5U", - "ZWxlcG9ydFRvUmVzcG9uc2UiABJvCgxUcmlnZ2VyRW1vdGUSLS5kZWNlbnRy", - "YWxhbmQua2VybmVsLmFwaXMuVHJpZ2dlckVtb3RlUmVxdWVzdBouLmRlY2Vu", - "dHJhbGFuZC5rZXJuZWwuYXBpcy5UcmlnZ2VyRW1vdGVSZXNwb25zZSIAEmgK", - "C0NoYW5nZVJlYWxtEiwuZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlzLkNoYW5n", - "ZVJlYWxtUmVxdWVzdBopLmRlY2VudHJhbGFuZC5rZXJuZWwuYXBpcy5TdWNj", - "ZXNzUmVzcG9uc2UiABJwCg9PcGVuRXh0ZXJuYWxVcmwSMC5kZWNlbnRyYWxh", - "bmQua2VybmVsLmFwaXMuT3BlbkV4dGVybmFsVXJsUmVxdWVzdBopLmRlY2Vu", - "dHJhbGFuZC5rZXJuZWwuYXBpcy5TdWNjZXNzUmVzcG9uc2UiABJsCg1PcGVu", - "TmZ0RGlhbG9nEi4uZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlzLk9wZW5OZnRE", - "aWFsb2dSZXF1ZXN0GikuZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlzLlN1Y2Nl", - "c3NSZXNwb25zZSIAEnYKGFNldENvbW11bmljYXRpb25zQWRhcHRlchItLmRl", - "Y2VudHJhbGFuZC5rZXJuZWwuYXBpcy5Db21tc0FkYXB0ZXJSZXF1ZXN0Giku", - "ZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlzLlN1Y2Nlc3NSZXNwb25zZSIAEnQK", - "EVRyaWdnZXJTY2VuZUVtb3RlEjIuZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlz", - "LlRyaWdnZXJTY2VuZUVtb3RlUmVxdWVzdBopLmRlY2VudHJhbGFuZC5rZXJu", - "ZWwuYXBpcy5TdWNjZXNzUmVzcG9uc2UiABJuCg9Db3B5VG9DbGlwYm9hcmQS", - "MC5kZWNlbnRyYWxhbmQua2VybmVsLmFwaXMuQ29weVRvQ2xpcGJvYXJkUmVx", - "dWVzdBonLmRlY2VudHJhbGFuZC5rZXJuZWwuYXBpcy5FbXB0eVJlc3BvbnNl", - "IgASZAoJU3RvcEVtb3RlEiouZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlzLlN0", - "b3BFbW90ZVJlcXVlc3QaKS5kZWNlbnRyYWxhbmQua2VybmVsLmFwaXMuU3Vj", - "Y2Vzc1Jlc3BvbnNlIgASdQoOT3BlbkV4cGxvcmVyVWkSLy5kZWNlbnRyYWxh", - "bmQua2VybmVsLmFwaXMuT3BlbkV4cGxvcmVyVWlSZXF1ZXN0GjAuZGVjZW50", - "cmFsYW5kLmtlcm5lbC5hcGlzLk9wZW5FeHBsb3JlclVpUmVzcG9uc2UiAGIG", - "cHJvdG8z")); + "cm5lbC5hcGlzLk9wZW5FeHBsb3JlclVpUmVzdWx0KoABCgpFeHBsb3JlclVp", + "Eg8KC0VVX1NFVFRJTkdTEAASCgoGRVVfTUFQEAESDwoLRVVfQkFDS1BBQ0sQ", + "AhISCg5FVV9DQU1FUkFfUkVFTBADEhIKDkVVX0NPTU1VTklUSUVTEAQSDQoJ", + "RVVfUExBQ0VTEAUSDQoJRVVfRVZFTlRTEAYqpgEKFE9wZW5FeHBsb3JlclVp", + "UmVzdWx0Eg8KC1VOU1BFQ0lGSUVEEAASCgoGT1BFTkVEEAESFAoQV0FTX0FM", + "UkVBRFlfT1BFThACEh4KGlJFSkVDVEVEX05PVF9DVVJSRU5UX1NDRU5FEAMS", + "HQoZUkVKRUNURURfRkVBVFVSRV9ESVNBQkxFRBAEEhwKGFJFSkVDVEVEX05P", + "X1VTRVJfR0VTVFVSRRAFMuwJChhSZXN0cmljdGVkQWN0aW9uc1NlcnZpY2US", + "bwoMTW92ZVBsYXllclRvEi0uZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlzLk1v", + "dmVQbGF5ZXJUb1JlcXVlc3QaLi5kZWNlbnRyYWxhbmQua2VybmVsLmFwaXMu", + "TW92ZVBsYXllclRvUmVzcG9uc2UiABJpCgpUZWxlcG9ydFRvEisuZGVjZW50", + "cmFsYW5kLmtlcm5lbC5hcGlzLlRlbGVwb3J0VG9SZXF1ZXN0GiwuZGVjZW50", + "cmFsYW5kLmtlcm5lbC5hcGlzLlRlbGVwb3J0VG9SZXNwb25zZSIAEm8KDFRy", + "aWdnZXJFbW90ZRItLmRlY2VudHJhbGFuZC5rZXJuZWwuYXBpcy5UcmlnZ2Vy", + "RW1vdGVSZXF1ZXN0Gi4uZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlzLlRyaWdn", + "ZXJFbW90ZVJlc3BvbnNlIgASaAoLQ2hhbmdlUmVhbG0SLC5kZWNlbnRyYWxh", + "bmQua2VybmVsLmFwaXMuQ2hhbmdlUmVhbG1SZXF1ZXN0GikuZGVjZW50cmFs", + "YW5kLmtlcm5lbC5hcGlzLlN1Y2Nlc3NSZXNwb25zZSIAEnAKD09wZW5FeHRl", + "cm5hbFVybBIwLmRlY2VudHJhbGFuZC5rZXJuZWwuYXBpcy5PcGVuRXh0ZXJu", + "YWxVcmxSZXF1ZXN0GikuZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlzLlN1Y2Nl", + "c3NSZXNwb25zZSIAEmwKDU9wZW5OZnREaWFsb2cSLi5kZWNlbnRyYWxhbmQu", + "a2VybmVsLmFwaXMuT3Blbk5mdERpYWxvZ1JlcXVlc3QaKS5kZWNlbnRyYWxh", + "bmQua2VybmVsLmFwaXMuU3VjY2Vzc1Jlc3BvbnNlIgASdgoYU2V0Q29tbXVu", + "aWNhdGlvbnNBZGFwdGVyEi0uZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlzLkNv", + "bW1zQWRhcHRlclJlcXVlc3QaKS5kZWNlbnRyYWxhbmQua2VybmVsLmFwaXMu", + "U3VjY2Vzc1Jlc3BvbnNlIgASdAoRVHJpZ2dlclNjZW5lRW1vdGUSMi5kZWNl", + "bnRyYWxhbmQua2VybmVsLmFwaXMuVHJpZ2dlclNjZW5lRW1vdGVSZXF1ZXN0", + "GikuZGVjZW50cmFsYW5kLmtlcm5lbC5hcGlzLlN1Y2Nlc3NSZXNwb25zZSIA", + "Em4KD0NvcHlUb0NsaXBib2FyZBIwLmRlY2VudHJhbGFuZC5rZXJuZWwuYXBp", + "cy5Db3B5VG9DbGlwYm9hcmRSZXF1ZXN0GicuZGVjZW50cmFsYW5kLmtlcm5l", + "bC5hcGlzLkVtcHR5UmVzcG9uc2UiABJkCglTdG9wRW1vdGUSKi5kZWNlbnRy", + "YWxhbmQua2VybmVsLmFwaXMuU3RvcEVtb3RlUmVxdWVzdBopLmRlY2VudHJh", + "bGFuZC5rZXJuZWwuYXBpcy5TdWNjZXNzUmVzcG9uc2UiABJ1Cg5PcGVuRXhw", + "bG9yZXJVaRIvLmRlY2VudHJhbGFuZC5rZXJuZWwuYXBpcy5PcGVuRXhwbG9y", + "ZXJVaVJlcXVlc3QaMC5kZWNlbnRyYWxhbmQua2VybmVsLmFwaXMuT3BlbkV4", + "cGxvcmVyVWlSZXNwb25zZSIAYgZwcm90bzM=")); descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData, new pbr::FileDescriptor[] { global::Decentraland.Common.VectorsReflection.Descriptor, global::DCL.ECSComponents.AvatarMaskReflection.Descriptor, }, new pbr::GeneratedClrTypeInfo(new[] {typeof(global::Decentraland.Kernel.Apis.ExplorerUi), typeof(global::Decentraland.Kernel.Apis.OpenExplorerUiResult), }, null, new pbr::GeneratedClrTypeInfo[] { @@ -120,36 +119,34 @@ static RestrictedActionsReflection() { #region Enums /// /// Identifies which fullscreen explorer panel OpenExplorerUi targets. - /// Additive: new panels are appended with the next number; existing values never change. + /// EU_SETTINGS holds 0 so an unset `ui` field defaults to the least-intrusive panel. /// public enum ExplorerUi { - [pbr::OriginalName("EU_UNSPECIFIED")] EuUnspecified = 0, + [pbr::OriginalName("EU_SETTINGS")] EuSettings = 0, [pbr::OriginalName("EU_MAP")] EuMap = 1, - [pbr::OriginalName("EU_SETTINGS")] EuSettings = 2, - [pbr::OriginalName("EU_BACKPACK")] EuBackpack = 3, - [pbr::OriginalName("EU_CAMERA_REEL")] EuCameraReel = 4, - [pbr::OriginalName("EU_COMMUNITIES")] EuCommunities = 5, - [pbr::OriginalName("EU_PLACES")] EuPlaces = 6, - [pbr::OriginalName("EU_EVENTS")] EuEvents = 7, + [pbr::OriginalName("EU_BACKPACK")] EuBackpack = 2, + [pbr::OriginalName("EU_CAMERA_REEL")] EuCameraReel = 3, + [pbr::OriginalName("EU_COMMUNITIES")] EuCommunities = 4, + [pbr::OriginalName("EU_PLACES")] EuPlaces = 5, + [pbr::OriginalName("EU_EVENTS")] EuEvents = 6, } /// /// Verdict of an OpenExplorerUi request (enum rather than a bool so new outcomes stay expressible). - /// Additive: new rejection reasons may be appended; existing values never change. /// public enum OpenExplorerUiResult { [pbr::OriginalName("UNSPECIFIED")] Unspecified = 0, [pbr::OriginalName("OPENED")] Opened = 1, /// - /// the standard restricted-actions current-scene gate rejected the call + /// a fullscreen panel is already open /// - [pbr::OriginalName("REJECTED_NOT_CURRENT_SCENE")] RejectedNotCurrentScene = 2, + [pbr::OriginalName("WAS_ALREADY_OPEN")] WasAlreadyOpen = 2, /// - /// a fullscreen panel is already open + /// the standard restricted-actions current-scene gate rejected the call /// - [pbr::OriginalName("REJECTED_ALREADY_OPEN")] RejectedAlreadyOpen = 3, + [pbr::OriginalName("REJECTED_NOT_CURRENT_SCENE")] RejectedNotCurrentScene = 3, /// - /// the requested section is hidden by feature flags + /// the requested section is hidden by feature flags or client doesn't have that feature /// [pbr::OriginalName("REJECTED_FEATURE_DISABLED")] RejectedFeatureDisabled = 4, /// @@ -3569,7 +3566,7 @@ public OpenExplorerUiRequest Clone() { /// Field number for the "ui" field. public const int UiFieldNumber = 1; - private global::Decentraland.Kernel.Apis.ExplorerUi ui_ = global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified; + private global::Decentraland.Kernel.Apis.ExplorerUi ui_ = global::Decentraland.Kernel.Apis.ExplorerUi.EuSettings; [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public global::Decentraland.Kernel.Apis.ExplorerUi Ui { @@ -3602,7 +3599,7 @@ public bool Equals(OpenExplorerUiRequest other) { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public override int GetHashCode() { int hash = 1; - if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified) hash ^= Ui.GetHashCode(); + if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuSettings) hash ^= Ui.GetHashCode(); if (_unknownFields != null) { hash ^= _unknownFields.GetHashCode(); } @@ -3621,7 +3618,7 @@ public void WriteTo(pb::CodedOutputStream output) { #if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE output.WriteRawMessage(this); #else - if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified) { + if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuSettings) { output.WriteRawTag(8); output.WriteEnum((int) Ui); } @@ -3635,7 +3632,7 @@ public void WriteTo(pb::CodedOutputStream output) { [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) { - if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified) { + if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuSettings) { output.WriteRawTag(8); output.WriteEnum((int) Ui); } @@ -3649,7 +3646,7 @@ public void WriteTo(pb::CodedOutputStream output) { [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] public int CalculateSize() { int size = 0; - if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified) { + if (Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuSettings) { size += 1 + pb::CodedOutputStream.ComputeEnumSize((int) Ui); } if (_unknownFields != null) { @@ -3664,7 +3661,7 @@ public void MergeFrom(OpenExplorerUiRequest other) { if (other == null) { return; } - if (other.Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuUnspecified) { + if (other.Ui != global::Decentraland.Kernel.Apis.ExplorerUi.EuSettings) { Ui = other.Ui; } _unknownFields = pb::UnknownFieldSet.MergeFrom(_unknownFields, other._unknownFields); @@ -3769,9 +3766,7 @@ public OpenExplorerUiResponse Clone() { public const int OpenResultFieldNumber = 1; private global::Decentraland.Kernel.Apis.OpenExplorerUiResult openResult_ = global::Decentraland.Kernel.Apis.OpenExplorerUiResult.Unspecified; /// - /// Carries only the verdict of the open action. Post-open signals (panel closed, flow - /// outcomes, milestones) are deliberately out of scope here and are delivered through a - /// separate additive event channel (a grow-only scene component), keeping this contract stable. + /// Carries only the verdict of the open action. /// [global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)] diff --git a/Explorer/Assets/StreamingAssets/Js/Modules/RestrictedActions.js b/Explorer/Assets/StreamingAssets/Js/Modules/RestrictedActions.js index dcef49923a5..b853ce555bd 100644 --- a/Explorer/Assets/StreamingAssets/Js/Modules/RestrictedActions.js +++ b/Explorer/Assets/StreamingAssets/Js/Modules/RestrictedActions.js @@ -12,21 +12,20 @@ function makeEnum(entries) { } module.exports.ExplorerUi = makeEnum([ - ['EU_UNSPECIFIED', 0], + ['EU_SETTINGS', 0], ['EU_MAP', 1], - ['EU_SETTINGS', 2], - ['EU_BACKPACK', 3], - ['EU_CAMERA_REEL', 4], - ['EU_COMMUNITIES', 5], - ['EU_PLACES', 6], - ['EU_EVENTS', 7], + ['EU_BACKPACK', 2], + ['EU_CAMERA_REEL', 3], + ['EU_COMMUNITIES', 4], + ['EU_PLACES', 5], + ['EU_EVENTS', 6], ]); module.exports.OpenExplorerUiResult = makeEnum([ ['UNSPECIFIED', 0], ['OPENED', 1], - ['REJECTED_NOT_CURRENT_SCENE', 2], - ['REJECTED_ALREADY_OPEN', 3], + ['WAS_ALREADY_OPEN', 2], + ['REJECTED_NOT_CURRENT_SCENE', 3], ['REJECTED_FEATURE_DISABLED', 4], ['REJECTED_NO_USER_GESTURE', 5], ]); diff --git a/scripts/package-lock.json b/scripts/package-lock.json index 84119a6d5c1..4c51a7c3bcb 100644 --- a/scripts/package-lock.json +++ b/scripts/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "Apache-2.0", "dependencies": { - "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-29744166153.commit-0e8b20c.tgz", + "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-30037355888.commit-0e7de00.tgz", "@protobuf-ts/protoc": "^2.11.0", "@types/fs-extra": "^11.0.1", "@types/glob": "^8.0.1", @@ -37,9 +37,9 @@ } }, "node_modules/@dcl/protocol": { - "version": "1.0.0-29744166153.commit-0e8b20c", - "resolved": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-29744166153.commit-0e8b20c.tgz", - "integrity": "sha512-8npy9TaxTARV2XMqZjJlLVO/aG+ZHvLUV3GsI+yrNFxx4Rh+3Z6IJQWEwFM9fT5O46K0LnAgMrqEYqhypxk4hg==", + "version": "1.0.0-30037355888.commit-0e7de00", + "resolved": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-30037355888.commit-0e7de00.tgz", + "integrity": "sha512-zAKzFGm2WNtJztN7FjAma2Uj5b72yyymTXwFTNb+VQRFceNRnS05bEKyAWjUfX1QbKT56Ln8Bd1n261BnWPzWA==", "license": "Apache-2.0", "dependencies": { "@dcl/ts-proto": "1.154.0", @@ -608,8 +608,8 @@ } }, "@dcl/protocol": { - "version": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-29744166153.commit-0e8b20c.tgz", - "integrity": "sha512-8npy9TaxTARV2XMqZjJlLVO/aG+ZHvLUV3GsI+yrNFxx4Rh+3Z6IJQWEwFM9fT5O46K0LnAgMrqEYqhypxk4hg==", + "version": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-30037355888.commit-0e7de00.tgz", + "integrity": "sha512-zAKzFGm2WNtJztN7FjAma2Uj5b72yyymTXwFTNb+VQRFceNRnS05bEKyAWjUfX1QbKT56Ln8Bd1n261BnWPzWA==", "requires": { "@dcl/ts-proto": "1.154.0", "protobufjs": "7.2.4" diff --git a/scripts/package.json b/scripts/package.json index 1de4469e5ec..83f032d8205 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -16,7 +16,7 @@ "typescript": "^4.2.3" }, "dependencies": { - "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-29744166153.commit-0e8b20c.tgz", + "@dcl/protocol": "https://sdk-team-cdn.decentraland.org/@dcl/protocol/branch//dcl-protocol-1.0.0-30037355888.commit-0e7de00.tgz", "@protobuf-ts/protoc": "^2.11.0", "@types/fs-extra": "^11.0.1", "@types/glob": "^8.0.1", From 14ef37420d5ce9829632b2af267b8546fff3c8c9 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Fri, 24 Jul 2026 10:24:26 +0200 Subject: [PATCH 5/9] small clean up --- Explorer/Assets/DCL/ExplorePanel/ExplorePanelController.cs | 6 ------ .../Assets/DCL/TeleportPrompt/TeleportPromptController.cs | 1 - 2 files changed, 7 deletions(-) diff --git a/Explorer/Assets/DCL/ExplorePanel/ExplorePanelController.cs b/Explorer/Assets/DCL/ExplorePanel/ExplorePanelController.cs index f2af676a4aa..a7b8f416fa7 100644 --- a/Explorer/Assets/DCL/ExplorePanel/ExplorePanelController.cs +++ b/Explorer/Assets/DCL/ExplorePanel/ExplorePanelController.cs @@ -45,7 +45,6 @@ public class ExplorePanelController : ControllerBase? communitiesLiveBadgeSubscription; @@ -61,7 +60,6 @@ public class ExplorePanelController : ControllerBase CanvasOrdering.SortingLayer.FULLSCREEN; - public bool CanBeClosedByEscape => State != ControllerState.ViewShowing; - public event Action? PlacesOpenedFromStartMenu; public event Action? EventsOpenedFromStartMenu; @@ -103,11 +99,9 @@ public ExplorePanelController(ViewFactoryMethod viewFactory, this.inputBlock = inputBlock; this.includeCameraReel = FeaturesRegistry.Instance.IsEnabled(FeatureId.CAMERA_REEL); this.mvcManager = mvcManager; - this.communitiesBrowserController = communitiesBrowserController; this.includeDiscover = FeaturesRegistry.Instance.IsEnabled(FeatureId.DISCOVER); this.eventsApiService = eventsApiService; this.communitiesLiveTracker = communitiesLiveTracker; - this.creditsPanelController = creditsPanelController; CommunitiesBrowserController = communitiesBrowserController; PlacesController = placesController; diff --git a/Explorer/Assets/DCL/TeleportPrompt/TeleportPromptController.cs b/Explorer/Assets/DCL/TeleportPrompt/TeleportPromptController.cs index 823d61ed679..f0451da9317 100644 --- a/Explorer/Assets/DCL/TeleportPrompt/TeleportPromptController.cs +++ b/Explorer/Assets/DCL/TeleportPrompt/TeleportPromptController.cs @@ -3,7 +3,6 @@ using DCL.Input; using DCL.PlacesAPIService; using DCL.UI; -using DCL.WebRequests; using MVC; using System; using System.Threading; From c69ac3387e0be3e4e5273e7577219bd6a0943cef Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Fri, 24 Jul 2026 10:34:41 +0200 Subject: [PATCH 6/9] re-orginized files --- .../RestrictedActions/ExplorerUi.meta | 8 ++++++++ .../RestrictedActions/ExplorerUi}/ExplorerUiActions.cs | 8 ++++---- .../ExplorerUi}/ExplorerUiActions.cs.meta | 0 .../ExplorerUi/RestrictedActions.ExplorerUi.asmref | 3 +++ .../ExplorerUi/RestrictedActions.ExplorerUi.asmref.meta | 7 +++++++ .../DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs | 1 + Explorer/DCL.Social.csproj.DotSettings | 1 + 7 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi.meta rename Explorer/Assets/DCL/Infrastructure/{SceneRunner => CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi}/ExplorerUiActions.cs (81%) rename Explorer/Assets/DCL/Infrastructure/{SceneRunner => CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi}/ExplorerUiActions.cs.meta (100%) create mode 100644 Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/RestrictedActions.ExplorerUi.asmref create mode 100644 Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/RestrictedActions.ExplorerUi.asmref.meta diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi.meta b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi.meta new file mode 100644 index 00000000000..15080ce9d65 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4e6b917f1b8a41dfb527be137fc85f01 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/ExplorerUiActions.cs similarity index 81% rename from Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs rename to Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/ExplorerUiActions.cs index 7ece75306e4..0d63d78e5e6 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/ExplorerUiActions.cs @@ -5,12 +5,12 @@ using Decentraland.Kernel.Apis; using MVC; -namespace DCL.SceneRunner +namespace DCL.Infrastructure.CrdtEcsBridge.JsModulesImplementation.RestrictedActions { /// - /// DCL.Plugins-side implementation of . It lives here (rather than - /// next to the restricted-actions API) because it references - /// from DCL.Social, an assembly that already depends on SceneRuntime. + /// Implementation of . The sibling asmref compiles it into + /// DCL.Social (not into SceneRuntime like the rest of this folder) because it references + /// , and DCL.Social already depends on SceneRuntime. /// public class ExplorerUiActions : IExplorerUiActions { diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs.meta b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/ExplorerUiActions.cs.meta similarity index 100% rename from Explorer/Assets/DCL/Infrastructure/SceneRunner/ExplorerUiActions.cs.meta rename to Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/ExplorerUiActions.cs.meta diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/RestrictedActions.ExplorerUi.asmref b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/RestrictedActions.ExplorerUi.asmref new file mode 100644 index 00000000000..d4469b6c9b2 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/RestrictedActions.ExplorerUi.asmref @@ -0,0 +1,3 @@ +{ + "reference": "GUID:f56000518aba31544aa96f4739e50a64" +} diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/RestrictedActions.ExplorerUi.asmref.meta b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/RestrictedActions.ExplorerUi.asmref.meta new file mode 100644 index 00000000000..932991080c8 --- /dev/null +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/RestrictedActions.ExplorerUi.asmref.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f87e2f0948e04b8783b34de90fcd45df +AssemblyDefinitionReferenceImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs b/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs index b6d047eed72..30ea726b3eb 100644 --- a/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs +++ b/Explorer/Assets/DCL/Infrastructure/SceneRunner/SceneInstanceDeps.cs @@ -16,6 +16,7 @@ using CrdtEcsBridge.UpdateGate; using CrdtEcsBridge.WorldSynchronizer; using DCL.Clipboard; +using DCL.Infrastructure.CrdtEcsBridge.JsModulesImplementation.RestrictedActions; using DCL.Interaction.Utility; using DCL.Multiplayer.Connections.RoomHubs; using DCL.PluginSystem.World.Dependencies; diff --git a/Explorer/DCL.Social.csproj.DotSettings b/Explorer/DCL.Social.csproj.DotSettings index 655c63f932d..c472703d180 100644 --- a/Explorer/DCL.Social.csproj.DotSettings +++ b/Explorer/DCL.Social.csproj.DotSettings @@ -2,6 +2,7 @@ True True True + True True True True From 70fab29c157c7f1305f266a099174b96792e1ab0 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Fri, 24 Jul 2026 11:22:30 +0200 Subject: [PATCH 7/9] fix: openExplorerUi rejecting COMMUNITIES for allowed users 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. --- .../Communities/CommunitiesFeatureAccess.cs | 13 +++++++++++ .../ExplorerUi/ExplorerUiActions.cs | 10 ++++++++ .../RestrictedActionsAPIImplementation.cs | 5 +++- ...estrictedActionsAPIImplementationShould.cs | 23 ++++++++++++++++--- .../ECS/TestSuite/EcsTestsUtils.cs | 8 ++++++- 5 files changed, 54 insertions(+), 5 deletions(-) diff --git a/Explorer/Assets/DCL/Communities/CommunitiesFeatureAccess.cs b/Explorer/Assets/DCL/Communities/CommunitiesFeatureAccess.cs index 9c7f1a4e61d..b3216fcb0b4 100644 --- a/Explorer/Assets/DCL/Communities/CommunitiesFeatureAccess.cs +++ b/Explorer/Assets/DCL/Communities/CommunitiesFeatureAccess.cs @@ -65,6 +65,19 @@ public async UniTask IsUserAllowedToUseTheFeatureAsync(CancellationToken c return result; } + /// + /// Synchronous counterpart of : returns the cached + /// result of the last completed check, or false while no check has resolved for the current identity. + /// + public bool IsUserAllowedCached() + { +#if UNITY_EDITOR && !COMMUNITIES_FORCE_USER_WHITELIST + return true; +#else + return storedResult ?? false; +#endif + } + public bool TryGetCommunityIdFromAppArgs(out string? communityId) => appArgs.TryGetValue(AppArgsFlags.COMMUNITY, out communityId) && !string.IsNullOrEmpty(communityId); diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/ExplorerUiActions.cs b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/ExplorerUiActions.cs index 0d63d78e5e6..55e7e393aef 100644 --- a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/ExplorerUiActions.cs +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/ExplorerUiActions.cs @@ -1,5 +1,7 @@ using Cysharp.Threading.Tasks; +using DCL.Communities; using DCL.CrdtEcsBridge.JsModulesImplementation; +using DCL.Diagnostics; using DCL.ExplorePanel; using DCL.UI; using Decentraland.Kernel.Apis; @@ -33,6 +35,14 @@ public void Dispose() public OpenExplorerUiResult OpenSection(ExploreSections section) { + // Communities availability depends on the user identity (feature flag + wallets allowlist), + // so it cannot be gated through FeaturesRegistry like the other sections. + if (section == ExploreSections.Communities && !CommunitiesFeatureAccess.Instance.IsUserAllowedCached()) + { + ReportHub.Log(ReportCategory.RESTRICTED_ACTIONS, "OpenSection: the Communities feature is not available for this user"); + return OpenExplorerUiResult.RejectedFeatureDisabled; + } + if (isExplorePanelOpen) return OpenExplorerUiResult.WasAlreadyOpen; diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs index 325b891b2b7..a5ea1ff8454 100644 --- a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/RestrictedActionsAPIImplementation.cs @@ -325,7 +325,10 @@ private static bool TryMapExplorerUi(ExplorerUi ui, out ExploreSections section, return true; case ExplorerUi.EuCommunities: section = ExploreSections.Communities; - gatingFeature = FeatureId.COMMUNITIES; + // FeatureId.COMMUNITIES is never populated in FeaturesRegistry (its state is + // identity-dependent), so the gate lives in the IExplorerUiActions implementation, + // which can reach CommunitiesFeatureAccess. + gatingFeature = null; return true; case ExplorerUi.EuPlaces: section = ExploreSections.Places; diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs index 5a8b9510082..0c4240a78d0 100644 --- a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/Tests/RestrictedActionsAPIImplementationShould.cs @@ -231,17 +231,34 @@ public void OpenExplorerUi_UnknownUiValue_Rejects() public void OpenExplorerUi_FeatureDisabled_Rejects() { // Arrange - // COMMUNITIES is not force-enabled in the editor (unlike CAMERA_REEL / DISCOVER), - // so the default features registry reports it as disabled. + // CAMERA_REEL is force-enabled in the editor, so an app-args override is the only way + // to exercise the disabled branch of the features-registry gate. + EcsTestsUtils.TearDownFeaturesRegistry(); + EcsTestsUtils.SetUpFeaturesRegistryWithAppArgs(new[] { "--camera-reel", "false" }); // Act - int result = restrictedActionsAPIImplementation.TryOpenExplorerUi((int)ExplorerUi.EuCommunities); + int result = restrictedActionsAPIImplementation.TryOpenExplorerUi((int)ExplorerUi.EuCameraReel); // Assert Assert.AreEqual((int)OpenExplorerUiResult.RejectedFeatureDisabled, result); explorerUiActions.DidNotReceive().OpenSection(Arg.Any()); } + [Test] + public void OpenExplorerUi_CommunitiesRejectionPropagates() + { + // Arrange + // Communities availability is identity-dependent, so its gate lives inside the + // IExplorerUiActions implementation; the API must return that rejection to the scene. + explorerUiActions.OpenSection(ExploreSections.Communities).Returns(OpenExplorerUiResult.RejectedFeatureDisabled); + + // Act + int result = restrictedActionsAPIImplementation.TryOpenExplorerUi((int)ExplorerUi.EuCommunities); + + // Assert + Assert.AreEqual((int)OpenExplorerUiResult.RejectedFeatureDisabled, result); + } + [Test] public void CopyToClipboard() { diff --git a/Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/EcsTestsUtils.cs b/Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/EcsTestsUtils.cs index f25e88f6ab5..adee4dc0b97 100644 --- a/Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/EcsTestsUtils.cs +++ b/Explorer/Assets/DCL/Infrastructure/ECS/TestSuite/EcsTestsUtils.cs @@ -4,6 +4,7 @@ using ECS.Unity.Materials.Components; using ECS.Unity.Transforms.Components; using Global.AppArgs; +using System; using System.Collections.Generic; using UnityEngine; using Utility; @@ -49,12 +50,17 @@ public static MaterialComponent AddMaterialToEntity(World world, in Entity entit } public static void SetUpFeaturesRegistry(params string[] flags) + { + SetUpFeaturesRegistryWithAppArgs(Array.Empty(), flags); + } + + public static void SetUpFeaturesRegistryWithAppArgs(string[] appArgs, params string[] flags) { var featureFlagsDto = new FeatureFlagsResultDto { flags = new Dictionary() }; foreach (string flag in flags) featureFlagsDto.flags.Add(flag, true); FeatureFlagsConfiguration.Initialize(new FeatureFlagsConfiguration(featureFlagsDto)); - FeaturesRegistry.Initialize(new FeaturesRegistry(new ApplicationParametersParser(), false)); + FeaturesRegistry.Initialize(new FeaturesRegistry(new ApplicationParametersParser(appArgs), false)); } public static void TearDownFeaturesRegistry() From 554bb09a7ed7ade1862088d5ff8b45a1202aa1cd Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Fri, 24 Jul 2026 11:37:56 +0200 Subject: [PATCH 8/9] chore: untrack CONTEXT.md (local working notes) Co-Authored-By: Claude Fable 5 --- CONTEXT.md | 47 ----------------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 CONTEXT.md diff --git a/CONTEXT.md b/CONTEXT.md deleted file mode 100644 index d84527419d3..00000000000 --- a/CONTEXT.md +++ /dev/null @@ -1,47 +0,0 @@ -# Feature context — Scene-triggered Explorer UI (`openExplorerUi`) - -> NOTE: This file was reconstructed after the original working copy was lost. It -> captures the authoritative domain language for the feature; please review and -> refine the wording if the original had additional nuance. - -## What the feature is - -A restricted action `openExplorerUi` that lets an SDK7 scene ask the Explorer to -open a native fullscreen panel (Map / Settings / Backpack / CameraReel / -Communities / Places / Events) via `~system/RestrictedActions`, returning a typed -verdict. Iteration 1 = open + verdict only (no event channel, no params). - -## Domain language (authoritative — use these terms) - -- **open result / verdict** — the outcome of an `openExplorerUi` call is an - **open result verdict**, modeled as the enum `OpenExplorerUiResult`, never a - `bool success`. New outcomes are expressed by adding enum values, not booleans. -- **`OpenExplorerUiResult`** values (protocol bare names; C# members are the - PascalCase forms; host-JS runtime keys mirror the bare names): - - `UNSPECIFIED` (0) — default / unset. - - `OPENED` (1) — the panel was opened. - - `REJECTED_NOT_CURRENT_SCENE` (2) — the standard restricted-actions - current-scene gate rejected the call. - - `REJECTED_ALREADY_OPEN` (3) — a fullscreen panel is already open (also covers - repeat / rapid re-invocation). Section-switching on an already-open panel is - out of scope for now. - - `REJECTED_FEATURE_DISABLED` (4) — the requested section is hidden by feature - flags, or the requested `ExplorerUi` value is unsupported. - - `REJECTED_NO_USER_GESTURE` (5) — the call did not originate from a user - gesture (see gesture rule below). -- **`ExplorerUi`** (`EU_*`) — identifies which fullscreen panel to target. -- **gesture rule ("once per frame")** — a call is honored only if a user pointer - input happened in the current or immediately preceding scene tick. The window - is a hardcoded constant, not a feature flag. - -## Layering (runtime data flow) - -``` -scene src/index.ts → ~system/RestrictedActions (host module, provided by Explorer) - → Explorer host JS StreamingAssets/Js/Modules/RestrictedActions.js - → C# RestrictedActionsAPIWrapper.OpenExplorerUi(int) - → RestrictedActionsAPIImplementation.TryOpenExplorerUi(int) → gates → verdict -``` - -`~system/*` are HOST modules (runtime values provided by the Explorer), so the -host JS must export the enum *values*, not just the `.d.ts` types. From aef0c1d791ffed69acb95673773fee1733faa941 Mon Sep 17 00:00:00 2001 From: Vitaly Popuzin Date: Fri, 24 Jul 2026 12:23:31 +0200 Subject: [PATCH 9/9] ix: report exceptions from the detached explore panel open task. --- .../RestrictedActions/ExplorerUi/ExplorerUiActions.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/ExplorerUiActions.cs b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/ExplorerUiActions.cs index 55e7e393aef..c05452d5fac 100644 --- a/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/ExplorerUiActions.cs +++ b/Explorer/Assets/DCL/Infrastructure/CrdtEcsBridge/JsModulesImplementation/RestrictedActions/ExplorerUi/ExplorerUiActions.cs @@ -6,6 +6,7 @@ using DCL.UI; using Decentraland.Kernel.Apis; using MVC; +using System; namespace DCL.Infrastructure.CrdtEcsBridge.JsModulesImplementation.RestrictedActions { @@ -52,8 +53,13 @@ public OpenExplorerUiResult OpenSection(ExploreSections section) private async UniTask OpenSectionAsync(ExploreSections section) { - await UniTask.SwitchToMainThread(); - await mvcManager.ShowAsync(ExplorePanelController.IssueCommand(new ExplorePanelParameter(section))); + try + { + await UniTask.SwitchToMainThread(); + await mvcManager.ShowAsync(ExplorePanelController.IssueCommand(new ExplorePanelParameter(section))); + } + catch (OperationCanceledException) { } + catch (Exception e) { ReportHub.LogException(e, ReportCategory.RESTRICTED_ACTIONS); } } private void OnViewShowed(IController controller)