From ce9904b9feb7f8b24c70c2b9314d2bf73d19ddb4 Mon Sep 17 00:00:00 2001 From: Darren Kelly Date: Fri, 12 Jun 2026 12:45:17 +0200 Subject: [PATCH 1/5] Add test and fix for multiple touchscreen input bug. --- .../TouchscreenMultiDisplayTests.cs | 84 +++++++++++++++++++ .../TouchscreenMultiDisplayTests.cs.meta | 2 + .../Runtime/Devices/Touchscreen.cs | 6 +- 3 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs create mode 100644 Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs.meta diff --git a/Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs b/Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs new file mode 100644 index 0000000000..e6c440ac0f --- /dev/null +++ b/Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs @@ -0,0 +1,84 @@ +using NUnit.Framework; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.LowLevel; +using TouchPhase = UnityEngine.InputSystem.TouchPhase; + +// Tests covering touch tracking across multiple physical touchscreen monitors. +// Regression coverage for IN-108611: touches from one screen incorrectly matching +// ongoing touches from another screen when both screens report the same touchId. +[TestFixture] +internal class TouchscreenMultiDisplayTests : CoreTestsFixture +{ + // When two physical touchscreens both have an active touch with the same touchId, + // a Move event from screen 2 must update screen 2's touch slot, not screen 1's. + // + // Failure mode (pre-fix): OnStateEvent matches ongoing touches by touchId alone, + // so screen 2's Move event finds screen 1's slot first (both have touchId=1) and + // incorrectly updates it, leaving screen 1's position changed and screen 2 stale. + [Test] + [Category("Devices")] + public void Devices_TouchMoveOnSecondDisplay_DoesNotUpdateTouchOnFirstDisplay() + { + var device = InputSystem.AddDevice(); + + // Finger down on display 0 (touchId=1). + InputSystem.QueueStateEvent(device, new TouchState + { + phase = TouchPhase.Began, + touchId = 1, + position = new Vector2(100, 100), + displayIndex = 0, + }); + + // Finger down on display 1 — same touchId, different screen. + InputSystem.QueueStateEvent(device, new TouchState + { + phase = TouchPhase.Began, + touchId = 1, + position = new Vector2(200, 200), + displayIndex = 1, + }); + + InputSystem.Update(); + + // Both touches should be allocated to separate slots. + Assert.That(device.touches[0].phase.ReadValue(), Is.EqualTo(TouchPhase.Began)); + Assert.That(device.touches[1].phase.ReadValue(), Is.EqualTo(TouchPhase.Began)); + + var display0SlotIndex = -1; + var display1SlotIndex = -1; + for (var i = 0; i < 2; i++) + { + var displayIdx = device.touches[i].displayIndex.ReadValue(); + if (displayIdx == 0) display0SlotIndex = i; + else if (displayIdx == 1) display1SlotIndex = i; + } + + Assert.That(display0SlotIndex, Is.Not.EqualTo(-1), "No touch slot found for display 0"); + Assert.That(display1SlotIndex, Is.Not.EqualTo(-1), "No touch slot found for display 1"); + + var display0PositionBefore = device.touches[display0SlotIndex].position.ReadValue(); + + // Swipe on display 1 (same touchId=1 as display 0's held touch). + InputSystem.QueueStateEvent(device, new TouchState + { + phase = TouchPhase.Moved, + touchId = 1, + position = new Vector2(300, 300), + displayIndex = 1, + }); + + InputSystem.Update(); + + // Display 0's touch must be unchanged. + Assert.That(device.touches[display0SlotIndex].position.ReadValue(), Is.EqualTo(display0PositionBefore), + "Touch on display 0 was incorrectly updated by a Move event from display 1"); + + // Display 1's touch must reflect the new position. + Assert.That(device.touches[display1SlotIndex].position.ReadValue(), Is.EqualTo(new Vector2(300, 300)), + "Touch on display 1 was not updated by its own Move event"); + + Assert.That(device.touches[display1SlotIndex].phase.ReadValue(), Is.EqualTo(TouchPhase.Moved)); + } +} diff --git a/Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs.meta b/Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs.meta new file mode 100644 index 0000000000..a9587bb70a --- /dev/null +++ b/Assets/Tests/InputSystem/TouchscreenMultiDisplayTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a5c39c3a4a0654c28a5b7754add41d2a \ No newline at end of file diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Devices/Touchscreen.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Devices/Touchscreen.cs index 7a82e6a177..d90811dcd4 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Devices/Touchscreen.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Devices/Touchscreen.cs @@ -694,7 +694,7 @@ protected override void FinishSetup() var touchId = newTouchState.touchId; for (var i = 0; i < touchControlCount; ++i) { - if (currentTouchState[i].touchId == touchId) + if (currentTouchState[i].touchId == touchId && currentTouchState[i].displayIndex == newTouchState.displayIndex) { // Preserve primary touch state. var isPrimaryTouch = currentTouchState[i].isPrimaryTouch; @@ -915,7 +915,7 @@ unsafe bool IInputStateCallbackReceiver.GetStateOffsetForEvent(InputControl cont for (var i = 0; i < touchControlCount; ++i) { var touch = ¤tTouchState[i]; - if (touch->touchId == eventTouchId || (!touch->isInProgress && eventTouchPhase.IsActive())) + if ((touch->touchId == eventTouchId && touch->displayIndex == eventTouchState->displayIndex) || (!touch->isInProgress && eventTouchPhase.IsActive())) { offset = primaryTouch.m_StateBlock.byteOffset + primaryTouch.m_StateBlock.alignedSizeInBytes - m_StateBlock.byteOffset + (uint)(i * UnsafeUtility.SizeOf()); @@ -1002,7 +1002,7 @@ internal static unsafe bool MergeForward(InputEventPtr currentEventPtr, InputEve var currentState = (TouchState*)currentEvent->state; var nextState = (TouchState*)nextEvent->state; - if (currentState->touchId != nextState->touchId || currentState->phaseId != nextState->phaseId || currentState->flags != nextState->flags) + if (currentState->touchId != nextState->touchId || currentState->phaseId != nextState->phaseId || currentState->flags != nextState->flags || currentState->displayIndex != nextState->displayIndex) return false; nextState->delta += currentState->delta; From 1adb45696df5ee77904a7c163547e4a6efe463eb Mon Sep 17 00:00:00 2001 From: Darren Kelly Date: Fri, 12 Jun 2026 13:11:10 +0200 Subject: [PATCH 2/5] Add fix for regression reported by u-pr. --- .../Runtime/Plugins/EnhancedTouch/TouchSimulation.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/EnhancedTouch/TouchSimulation.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/EnhancedTouch/TouchSimulation.cs index d7bbc15ccd..d3ad485357 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/EnhancedTouch/TouchSimulation.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Plugins/EnhancedTouch/TouchSimulation.cs @@ -253,6 +253,9 @@ protected void OnEnable() if (m_TouchIds == null) m_TouchIds = new int[simulatedTouchscreen.touches.Count]; + if (m_TouchDisplayIndices == null) + m_TouchDisplayIndices = new byte[simulatedTouchscreen.touches.Count]; + foreach (var device in InputSystem.devices) OnDeviceChange(device, InputDeviceChange.Added); @@ -306,10 +309,12 @@ private unsafe void UpdateTouch(int touchIndex, int pointerIndex, TouchPhase pha touch.startPosition = position; touch.touchId = ++m_LastTouchId; m_TouchIds[touchIndex] = m_LastTouchId; + m_TouchDisplayIndices[touchIndex] = displayIndex; } else { touch.touchId = m_TouchIds[touchIndex]; + touch.displayIndex = m_TouchDisplayIndices[touchIndex]; } //NOTE: Processing these events still happen in the current frame. @@ -327,6 +332,7 @@ private unsafe void UpdateTouch(int touchIndex, int pointerIndex, TouchPhase pha [NonSerialized] private int[] m_CurrentDisplayIndices; [NonSerialized] private ButtonControl[] m_Touches; [NonSerialized] private int[] m_TouchIds; + [NonSerialized] private byte[] m_TouchDisplayIndices; [NonSerialized] private int m_LastTouchId; [NonSerialized] private Action m_OnDeviceChange; From b9b935932b09e0f8384771c679f0eefe4c2dfe3b Mon Sep 17 00:00:00 2001 From: Darren Kelly Date: Fri, 12 Jun 2026 13:16:34 +0200 Subject: [PATCH 3/5] Add base test and fix for incorrect teardown / reset. --- .../InputTestFixtureTeardownTests.cs | 106 ++++++++++++++++++ .../Runtime/Actions/InputActionState.cs | 55 +++++++++ .../InputSystem/Runtime/InputSystem.cs | 27 ++++- .../TestFixture/InputTestStateManager.cs | 4 + 4 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 Assets/Tests/InputSystem.Editor/InputTestFixtureTeardownTests.cs diff --git a/Assets/Tests/InputSystem.Editor/InputTestFixtureTeardownTests.cs b/Assets/Tests/InputSystem.Editor/InputTestFixtureTeardownTests.cs new file mode 100644 index 0000000000..5e2e50872f --- /dev/null +++ b/Assets/Tests/InputSystem.Editor/InputTestFixtureTeardownTests.cs @@ -0,0 +1,106 @@ +using NUnit.Framework; +using UnityEngine; +using UnityEngine.InputSystem; + +/// +/// Regression tests for IN-107889: InputTestFixture.TearDown() fails to reset Input System state +/// between consecutive scene-based tests. +/// +/// +/// Bug: When an belonging to the project-wide +/// asset is enabled before a test's runs, the TearDown/Restore +/// cycle leaves the action map disconnected from its saved . +/// +/// Root cause: +/// 1. TestHook_DisableActions() calls Disable() + OnSetupChanged() on the +/// project-wide asset's maps. Disable() modifies the action state memory (phases → Disabled) +/// on the same InputActionState objects that were just saved in the snapshot, because +/// Disable() is called after SaveAndResetState() but references the same managed objects. +/// 2. OnSetupChanged() sets map.m_State = null, disconnecting the map from its state. +/// 3. Restore() restores s_GlobalState (the registry) but does NOT restore the per-map +/// back-references (InputActionMap.m_State, m_MapIndexInState) or the action phase +/// memory. +/// +/// The fix: +/// 1. TestHook_DisableActions() should disconnect maps without modifying the saved state's memory, +/// so the saved snapshot retains the correct enabled phases. +/// 2. Restore() should re-link the back-references after RestoreSavedState() and +/// recompute m_EnabledActionsCount from the restored action phases. +/// +internal class InputTestFixtureTeardownTests : InputTestFixture +{ + // Simulates a scene with a PlayerInput component using project-wide actions. + // Created once before any [SetUp] runs, like a scene's OnEnable(). + private InputActionAsset m_PreTestAsset; + private InputActionMap m_PreTestMap; + private InputAction m_PreTestAction; + + [OneTimeSetUp] + public void SimulateSceneLoad() + { + // Create an action asset simulating project-wide actions that a scene's + // PlayerInput component would use. + m_PreTestAsset = ScriptableObject.CreateInstance(); + m_PreTestAsset.hideFlags = HideFlags.HideAndDontSave; + m_PreTestMap = m_PreTestAsset.AddActionMap("Player"); + m_PreTestAction = m_PreTestMap.AddAction("Fire", InputActionType.Button); + + // Enable the map - creates an InputActionState and registers it in s_GlobalState. + // This simulates PlayerInput.ActivateInput() running before the test starts. + m_PreTestMap.Enable(); + + // Register as project-wide actions on the real manager. + // This causes TestHook_DisableActions() to treat our asset like project-wide actions + // (i.e. call Disable() + OnSetupChanged() on it during each Setup()). + InputSystem.s_Manager.actions = m_PreTestAsset; + } + + [OneTimeTearDown] + public void VerifyPreTestStatePreserved() + { + // After all test TearDowns, the action map should still be linked to its saved + // InputActionState and the enabled count should reflect the pre-test enabled state. + // + // With the bug: map.m_State is null and map.enabled is false because: + // - TestHook_DisableActions() called Disable() on the map (clearing enabled state + // in the shared state memory) then OnSetupChanged() (setting m_State = null) + // - Restore() restores s_GlobalState but not map.m_State or m_EnabledActionsCount + // + // With the fix: map.m_State is properly re-linked and map.enabled is true because: + // - TestHook_DisableActions() no longer modifies the saved state's memory + // - Restore() calls RelinkRestoredStates() to restore back-references and + // recompute m_EnabledActionsCount from the action phase memory + + Assert.That(m_PreTestMap.m_State, Is.Not.Null, + "Action map should be linked to its saved InputActionState after Restore(). " + + "m_State was cleared by OnSetupChanged() during TestHook_DisableActions() " + + "and was not re-linked by Restore()."); + + Assert.That(m_PreTestMap.enabled, Is.True, + "Action map should be enabled after Restore(): it was enabled before any test ran " + + "and should be restored to that enabled state after TearDown()."); + + // Clean up + InputSystem.s_Manager.actions = null; + Object.DestroyImmediate(m_PreTestAsset); + } + + [Test] + [Order(1)] + public void TearDown_FirstTest_ProjectWideActionsAreReenabledForTest() + { + // During this test, project-wide actions may have been re-enabled by TestHook_EnableActions + // (or left disabled if TestHook_EnableActions is a no-op for the test manager). + // We're just running to trigger a Setup/TearDown cycle. + Assert.Pass("First test ran successfully (triggering Setup/TearDown cycle)"); + } + + [Test] + [Order(2)] + public void TearDown_SecondTest_StateRemainsCorrectAfterSecondCycle() + { + // After the first test's TearDown() + this Setup(), verify setup completes without errors. + // The [OneTimeTearDown] contains the actual assertion for the post-Restore() state. + Assert.Pass("Second test ran successfully (triggering second Setup/TearDown cycle)"); + } +} diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs index 4325fc1988..2f4a940129 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs @@ -4532,6 +4532,61 @@ internal static ISavedState SaveAndResetState() return savedState; } + /// + /// After restores the global registry via + /// RestoreSavedState(), the per-map back-references on each + /// (m_State, m_MapIndexInState) and the per-action index + /// (InputAction.m_ActionIndexInState) may be stale because they were cleared by + /// Destroy() during StaticDisposeCurrentState(). This method re-links those + /// references from the restored and recomputes + /// m_EnabledActionsCount from the action phase memory so that maps and actions + /// correctly reflect their pre-test enabled state. See IN-107889. + /// + internal static void RelinkRestoredStates() + { + var count = s_GlobalState.globalList.length; + for (var i = 0; i < count; ++i) + { + var handle = s_GlobalState.globalList[i]; + if (!handle.IsAllocated) + continue; + if (handle.Target is InputActionState state) + state.RelinkMapsAndRecomputeEnabledCount(); + } + } + + private unsafe void RelinkMapsAndRecomputeEnabledCount() + { + for (var mapIndex = 0; mapIndex < totalMapCount; ++mapIndex) + { + var map = maps[mapIndex]; + if (map == null) + continue; + + map.m_State = this; + map.m_MapIndexInState = mapIndex; + + var indices = mapIndices[mapIndex]; + var mapActions = map.m_Actions; + if (mapActions != null) + { + for (var k = 0; k < indices.actionCount; ++k) + mapActions[k].m_ActionIndexInState = indices.actionStartIndex + k; + } + + // Recompute m_EnabledActionsCount from the restored action phase memory. + // This correctly reflects enabled/disabled state without any explicit Disable() + // call having been made (see TestHook_DisableActions changes for IN-107889). + var enabledCount = 0; + for (var k = 0; k < indices.actionCount; ++k) + { + if (!actionStates[indices.actionStartIndex + k].isDisabled) + ++enabledCount; + } + map.m_EnabledActionsCount = enabledCount; + } + } + private void AddToGlobalList() { CompactGlobalList(); diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputSystem.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputSystem.cs index a2cd47bf74..7e56f3e24a 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputSystem.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputSystem.cs @@ -3518,9 +3518,32 @@ internal static void InitializeInPlayer(IInputRuntime runtime = null, bool loadS #if UNITY_INCLUDE_TESTS internal static void TestHook_DisableActions() { - DisableActions(triggerSetupChanged: true); - if (s_Manager != null) + // Disconnect the project-wide action maps from their InputActionState without + // calling Disable(), which would corrupt the saved state snapshot. + // + // The normal DisableActions() path calls Disable() then OnSetupChanged() on the + // project-wide asset. Disable() modifies action phase memory on the *same* managed + // InputActionState objects that were already captured in the SaveAndResetState() + // snapshot (GCHandles point to the live objects; no deep copy is made). This means + // the restored snapshot has disabled phases even though the maps were enabled when + // the snapshot was taken, causing Restore() to leave maps incorrectly disabled. + // + // Instead, we just null out the map back-references and let Restore() re-link them. + // Control monitors registered through the old runtime don't need unsubscribing here + // because the test manager installs a new runtime; the old runtime is never polled + // during the test. See IN-107889. + var projectWideActions = s_Manager?.actions; + if (projectWideActions != null) + { + foreach (var map in projectWideActions.actionMaps) + { + map.m_State = null; + map.m_MapIndexInState = InputActionState.kInvalidIndex; + map.m_EnabledActionsCount = 0; + } + projectWideActions.m_SharedStateForAllMaps = null; s_Manager.actions = null; + } } internal static void TestHook_EnableActions() diff --git a/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestStateManager.cs b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestStateManager.cs index 438dab1d3f..184aebf074 100644 --- a/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestStateManager.cs +++ b/Packages/com.unity.inputsystem/Tests/TestFixture/InputTestStateManager.cs @@ -121,6 +121,10 @@ public void Restore() state.inputUserState.RestoreSavedState(); state.touchState.RestoreSavedState(); state.inputActionState.RestoreSavedState(); + // Re-link per-map/per-action back-references that were cleared during + // StaticDisposeCurrentState(). Also recomputes m_EnabledActionsCount + // from the restored action phase memory. See IN-107889. + InputActionState.RelinkRestoredStates(); InputSystemTestHooks.TestHook_RestoreFromSavedState(state.manager, state.remote, state.remoteConnection); InputUpdate.Restore(state.managerState.updateState); From 0caac9fe4c2e84b8c7134ffee70144f676aa56ce Mon Sep 17 00:00:00 2001 From: Darren Kelly Date: Mon, 15 Jun 2026 17:56:44 +0200 Subject: [PATCH 4/5] Add fix suggested by u-pr. --- .../InputSystem/Runtime/Actions/InputActionState.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs index 2f4a940129..33b81db331 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs @@ -4565,6 +4565,9 @@ private unsafe void RelinkMapsAndRecomputeEnabledCount() map.m_State = this; map.m_MapIndexInState = mapIndex; + + if (map.m_Asset != null && map.m_Asset.m_SharedStateForAllMaps == null) + map.m_Asset.m_SharedStateForAllMaps = this; var indices = mapIndices[mapIndex]; var mapActions = map.m_Actions; From 7fa3703d531fd4bd89f79df971e7418992faf180 Mon Sep 17 00:00:00 2001 From: Darren Kelly Date: Wed, 17 Jun 2026 17:09:47 +0200 Subject: [PATCH 5/5] Add fix for test issues. --- .../Runtime/Actions/InputActionState.cs | 2 +- .../InputSystem/Runtime/InputSystem.cs | 30 ++++++++++++++----- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs index 33b81db331..d442f8197c 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/Actions/InputActionState.cs @@ -4565,7 +4565,7 @@ private unsafe void RelinkMapsAndRecomputeEnabledCount() map.m_State = this; map.m_MapIndexInState = mapIndex; - + if (map.m_Asset != null && map.m_Asset.m_SharedStateForAllMaps == null) map.m_Asset.m_SharedStateForAllMaps = this; diff --git a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputSystem.cs b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputSystem.cs index 7e56f3e24a..200bb336b7 100644 --- a/Packages/com.unity.inputsystem/InputSystem/Runtime/InputSystem.cs +++ b/Packages/com.unity.inputsystem/InputSystem/Runtime/InputSystem.cs @@ -3535,15 +3535,31 @@ internal static void TestHook_DisableActions() var projectWideActions = s_Manager?.actions; if (projectWideActions != null) { - foreach (var map in projectWideActions.actionMaps) - { - map.m_State = null; - map.m_MapIndexInState = InputActionState.kInvalidIndex; - map.m_EnabledActionsCount = 0; - } - projectWideActions.m_SharedStateForAllMaps = null; + DisconnectActionMaps(projectWideActions); s_Manager.actions = null; } + + // Also disconnect the configured project-wide asset even if manager.actions is null. + // RelinkRestoredStates() restores m_EnabledActionsCount on maps after Restore(). If a + // subsequent test's manager.actions is null (e.g. a previous OneTimeTearDown cleared it), + // TestHook_DisableActions would be a no-op and those maps would keep m_State set and + // m_EnabledActionsCount == m_Actions.Length. InputActionMap.Enable() would then + // early-return thinking all actions are already enabled, but the state is not in + // s_GlobalState (which was cleared by SaveAndResetState()). See IN-107889. + var configuredActions = InputManager.s_GetProjectWideActions?.Invoke(); + if (configuredActions != null && configuredActions != projectWideActions) + DisconnectActionMaps(configuredActions); + } + + private static void DisconnectActionMaps(InputActionAsset asset) + { + foreach (var map in asset.actionMaps) + { + map.m_State = null; + map.m_MapIndexInState = InputActionState.kInvalidIndex; + map.m_EnabledActionsCount = 0; + } + asset.m_SharedStateForAllMaps = null; } internal static void TestHook_EnableActions()