|
| 1 | +using Arch.Core; |
| 2 | +using CRDT; |
| 3 | +using CrdtEcsBridge.ECSToCRDTWriter; |
| 4 | +using DCL.CharacterCamera; |
| 5 | +using DCL.ECSComponents; |
| 6 | +using DCL.SDKComponents.PrimaryPointerInfo.Systems; |
| 7 | +using NSubstitute; |
| 8 | +using NUnit.Framework; |
| 9 | +using SceneRunner.Scene; |
| 10 | +using System; |
| 11 | +using System.Collections.Generic; |
| 12 | +using UnityEngine; |
| 13 | +using UnityEngine.InputSystem; |
| 14 | +using Utility; |
| 15 | +using ProtoVector3 = Decentraland.Common.Vector3; |
| 16 | + |
| 17 | +namespace DCL.SDKComponents.PrimaryPointerInfo.Tests |
| 18 | +{ |
| 19 | + // Regression coverage for https://github.com/decentraland/unity-explorer/issues/9496: |
| 20 | + // while the explorer chat is focused, ApplyInputMapsSystem disables the whole `Camera` |
| 21 | + // action map (ChatInputBlockingService.Block() -> Kind.Camera), and PrimaryPointerInfoSystem |
| 22 | + // sources its raw pointer position from `DCLInput.Instance.Camera.Point`, which is part of |
| 23 | + // that map. A disabled InputAction.ReadValue<Vector2>() returns default(Vector2), so the |
| 24 | + // system used to feed PBPrimaryPointerInfo.ScreenCoordinates = (0,0) to every scene for as |
| 25 | + // long as chat stayed focused, pinning scene-side UI (e.g. the Genesis Plaza fishing pond's |
| 26 | + // "Toggle Hints" tooltip) to the bottom-left corner instead of the real cursor. |
| 27 | + [TestFixture] |
| 28 | + public class PrimaryPointerInfoSystemCameraMapDisabledShould : InputTestFixture |
| 29 | + { |
| 30 | + private const float TOLERANCE = 1e-4f; |
| 31 | + |
| 32 | + private World sceneWorld; |
| 33 | + private World globalWorld; |
| 34 | + private Mouse mouse; |
| 35 | + private GameObject cameraGameObject; |
| 36 | + private Camera camera; |
| 37 | + private IECSToCRDTWriter ecsToCRDTWriter; |
| 38 | + private ISceneStateProvider sceneStateProvider; |
| 39 | + private IExposedCameraData exposedCameraData; |
| 40 | + private PrimaryPointerInfoSystem system; |
| 41 | + private List<(Vector2 pos, Vector2 delta, ProtoVector3 rayDir)> putCalls; |
| 42 | + |
| 43 | + [SetUp] |
| 44 | + public void SetUp() |
| 45 | + { |
| 46 | + base.Setup(); |
| 47 | + |
| 48 | + mouse = InputSystem.AddDevice<Mouse>(); |
| 49 | + DCLInput.Instance.Enable(); |
| 50 | + |
| 51 | + sceneWorld = World.Create(); |
| 52 | + globalWorld = World.Create(); |
| 53 | + |
| 54 | + cameraGameObject = new GameObject("PrimaryPointerInfoCameraMapDisabledTestCamera"); |
| 55 | + camera = cameraGameObject.AddComponent<Camera>(); |
| 56 | + globalWorld.Create(new CameraComponent(camera)); |
| 57 | + |
| 58 | + putCalls = new List<(Vector2 pos, Vector2 delta, ProtoVector3 rayDir)>(); |
| 59 | + |
| 60 | + ecsToCRDTWriter = Substitute.For<IECSToCRDTWriter>(); |
| 61 | + |
| 62 | + ecsToCRDTWriter.PutMessage( |
| 63 | + Arg.Any<Action<PBPrimaryPointerInfo, (Vector2 pos, Vector2 delta, ProtoVector3 rayDir)>>(), |
| 64 | + Arg.Any<CRDTEntity>(), |
| 65 | + Arg.Do<(Vector2 pos, Vector2 delta, ProtoVector3 rayDir)>(data => putCalls.Add(data))); |
| 66 | + |
| 67 | + sceneStateProvider = Substitute.For<ISceneStateProvider>(); |
| 68 | + sceneStateProvider.IsCurrent.Returns(true); |
| 69 | + |
| 70 | + exposedCameraData = Substitute.For<IExposedCameraData>(); |
| 71 | + exposedCameraData.PointerIsLocked.Returns(new CanBeDirty<bool>(false)); |
| 72 | + exposedCameraData.AccumulatedPointerDelta.Returns(default(CumulativePointerDelta)); |
| 73 | + |
| 74 | + system = new PrimaryPointerInfoSystem(sceneWorld, globalWorld, sceneStateProvider, ecsToCRDTWriter, exposedCameraData); |
| 75 | + system.Initialize(); |
| 76 | + |
| 77 | + // Initialize() performs one PUT; discard it so the test only sees the Update()-driven write. |
| 78 | + putCalls.Clear(); |
| 79 | + } |
| 80 | + |
| 81 | + [TearDown] |
| 82 | + public void Cleanup() |
| 83 | + { |
| 84 | + system.Dispose(); |
| 85 | + UnityEngine.Object.DestroyImmediate(cameraGameObject); |
| 86 | + sceneWorld.Dispose(); |
| 87 | + globalWorld.Dispose(); |
| 88 | + } |
| 89 | + |
| 90 | + [Test] |
| 91 | + public void NotReportZeroScreenCoordinatesWhenCameraMapDisabledByChatFocus() |
| 92 | + { |
| 93 | + // Arrange: position the simulated pointer device, then disable the whole `Camera` |
| 94 | + // action map exactly as ApplyInputMapsSystem.cs does when chat gains focus |
| 95 | + // (DCLInput.Instance.Camera.Disable()) - this puts every action in that map, |
| 96 | + // including Point, into the Disabled phase. |
| 97 | + var simulatedPosition = new Vector2(456f, 234f); |
| 98 | + Set(mouse.position, simulatedPosition); |
| 99 | + DCLInput.Instance.Camera.Disable(); |
| 100 | + |
| 101 | + // Act |
| 102 | + system.Update(0); |
| 103 | + |
| 104 | + // Assert: the scene-facing pointer feed must keep tracking the real cursor instead |
| 105 | + // of collapsing to (0,0) - the exact value a disabled InputAction.ReadValue<Vector2>() |
| 106 | + // returns, and the value that pins the fishing-pond "Toggle Hints" tooltip to the |
| 107 | + // bottom-left corner while chat is focused. |
| 108 | + Assert.IsNotEmpty(putCalls); |
| 109 | + (Vector2 pos, Vector2 _, ProtoVector3 _) = putCalls[putCalls.Count - 1]; |
| 110 | + |
| 111 | + Assert.AreNotEqual(Vector2.zero, pos, "screenCoordinates must not collapse to (0,0) while the Camera map is disabled"); |
| 112 | + Assert.AreEqual(simulatedPosition.x, pos.x, TOLERANCE); |
| 113 | + Assert.AreEqual(simulatedPosition.y, pos.y, TOLERANCE); |
| 114 | + } |
| 115 | + } |
| 116 | +} |
0 commit comments