From dfb44d0abeb4bd6125ff1307cc43f99e81cf3da6 Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 4 Jun 2026 22:35:41 +0900 Subject: [PATCH 1/6] Fix overlay prefab script resolution Keep the runtime assembly attachable without implicit auto-references so package prefabs can resolve their overlay components in consumer projects. Also remove the demo-only mouse overlay tester from the package prefab and add prefab contract tests to catch editor-only asmdef regressions and package-only missing script references. --- .../InputVisualizationCanvasPrefabTests.cs | 103 ++++++++++++++++++ ...nputVisualizationCanvasPrefabTests.cs.meta | 11 ++ .../Tests/Editor/uLoopMCP.Tests.Editor.asmdef | 1 + .../Common/InputVisualizationCanvas.cs | 2 + .../RecordInputOverlayPresenter.cs | 2 + .../RecordInput/RecordInputOverlayView.cs | 2 + .../Runtime/ReplayInput/ReplayInputOverlay.cs | 2 + .../SimulateKeyboardOverlay.cs | 2 + .../SimulateMouseInputOverlay.cs | 4 +- .../SimulateMouseInputOverlay.prefab | 13 --- .../SimulateMouseUi/SimulateMouseUiOverlay.cs | 2 + Packages/src/Runtime/uLoopMCP.Runtime.asmdef | 6 +- 12 files changed, 132 insertions(+), 18 deletions(-) create mode 100644 Assets/Tests/Editor/InputVisualizationCanvasPrefabTests.cs create mode 100644 Assets/Tests/Editor/InputVisualizationCanvasPrefabTests.cs.meta diff --git a/Assets/Tests/Editor/InputVisualizationCanvasPrefabTests.cs b/Assets/Tests/Editor/InputVisualizationCanvasPrefabTests.cs new file mode 100644 index 0000000000..5b16d846d7 --- /dev/null +++ b/Assets/Tests/Editor/InputVisualizationCanvasPrefabTests.cs @@ -0,0 +1,103 @@ +#nullable enable +using System; +using System.IO; +using Newtonsoft.Json.Linq; +using NUnit.Framework; +using UnityEditor; +using UnityEngine; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Verifies that the shared input visualization prefab keeps attachable overlay components. + /// + public sealed class InputVisualizationCanvasPrefabTests + { + private const string PrefabPath = + "Packages/io.github.hatayama.uloopmcp/Runtime/Common/InputVisualizationCanvas.prefab"; + private const string RuntimeAssemblyDefinitionPath = + "Packages/src/Runtime/uLoopMCP.Runtime.asmdef"; + private const string SimulateMouseInputOverlayPrefabPath = + "Packages/src/Runtime/SimulateMouseInput/SimulateMouseInputOverlay.prefab"; + private const string DemoMouseInputOverlayTesterMetaPath = + "Assets/Tests/Demo/DemoMouseInputOverlayTester.cs.meta"; + + [Test] + public void RuntimeAssemblyDefinition_WhenScanned_IsPrefabAttachableAndNotAutoReferenced() + { + // The runtime assembly must stay attachable for prefab scripts while avoiding implicit user assembly references. + JObject asmdef = JObject.Parse(ReadProjectText(RuntimeAssemblyDefinitionPath)); + JToken? includePlatformsToken = asmdef["includePlatforms"]; + + Assert.That(includePlatformsToken, Is.Not.Null); + Assert.That(includePlatformsToken!.Type, Is.EqualTo(JTokenType.Array)); + Assert.That(includePlatformsToken.HasValues, Is.False); + Assert.That(asmdef["autoReferenced"]?.Value(), Is.False); + } + + [Test] + public void InputVisualizationCanvasPrefab_WhenInstantiated_HasOverlayReferences() + { + // Instantiation proves Unity can resolve the prefab scripts after import, not only in raw YAML. + GameObject prefab = AssetDatabase.LoadAssetAtPath(PrefabPath); + + Assert.That(prefab, Is.Not.Null); + + UnityEngine.Object instantiatedObject = PrefabUtility.InstantiatePrefab(prefab); + Assert.That(instantiatedObject, Is.TypeOf()); + + GameObject instance = (GameObject)instantiatedObject; + try + { + InputVisualizationCanvas canvas = instance.GetComponent(); + + Assert.That(canvas, Is.Not.Null); + Assert.That(canvas.KeyboardOverlay, Is.Not.Null); + Assert.That(canvas.MouseUiOverlay, Is.Not.Null); + Assert.That(canvas.MouseInputOverlay, Is.Not.Null); + Assert.That(canvas.RecordInputOverlayPresenter, Is.Not.Null); + Assert.That(canvas.ReplayInputOverlay, Is.Not.Null); + } + finally + { + UnityEngine.Object.DestroyImmediate(instance); + } + } + + [Test] + public void SimulateMouseInputOverlayPrefab_WhenSerialized_DoesNotReferenceDemoTester() + { + // Package prefabs cannot reference demo/test scripts because consumers import only Packages/src. + string prefabText = ReadProjectText(SimulateMouseInputOverlayPrefabPath); + string demoTesterGuid = ReadMetaGuid(DemoMouseInputOverlayTesterMetaPath); + + Assert.That(prefabText, Does.Not.Contain(demoTesterGuid)); + } + + private static string ReadProjectText(string relativePath) + { + string projectRoot = Path.GetFullPath(Path.Combine(Application.dataPath, "..")); + string absolutePath = Path.Combine(projectRoot, relativePath); + + return File.ReadAllText(absolutePath); + } + + private static string ReadMetaGuid(string relativePath) + { + string[] lines = ReadProjectText(relativePath).Split('\n'); + const string Prefix = "guid: "; + + for (int i = 0; i < lines.Length; i++) + { + string line = lines[i].Trim(); + if (line.StartsWith(Prefix, StringComparison.Ordinal)) + { + return line.Substring(Prefix.Length).Trim(); + } + } + + Assert.Fail("Meta file must contain a guid line"); + return string.Empty; + } + } +} diff --git a/Assets/Tests/Editor/InputVisualizationCanvasPrefabTests.cs.meta b/Assets/Tests/Editor/InputVisualizationCanvasPrefabTests.cs.meta new file mode 100644 index 0000000000..34547642a6 --- /dev/null +++ b/Assets/Tests/Editor/InputVisualizationCanvasPrefabTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eff3e2ca33fd44eaa80d8954b7cec566 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/uLoopMCP.Tests.Editor.asmdef b/Assets/Tests/Editor/uLoopMCP.Tests.Editor.asmdef index 08ab1ee979..c9c589dc94 100644 --- a/Assets/Tests/Editor/uLoopMCP.Tests.Editor.asmdef +++ b/Assets/Tests/Editor/uLoopMCP.Tests.Editor.asmdef @@ -7,6 +7,7 @@ "GUID:27619889b8ba8c24980f49ee34dbb44a", "GUID:332048ead1ebf4658b9961338e5d68c3", "GUID:290394860909340b7835eb7cc215ee75", + "GUID:c956a21f824994ef087b6de566690b3d", "uLoopMCP.Editor.MetadataValidation" ], "includePlatforms": [ diff --git a/Packages/src/Runtime/Common/InputVisualizationCanvas.cs b/Packages/src/Runtime/Common/InputVisualizationCanvas.cs index 3d45ee2638..01ac58ce1a 100644 --- a/Packages/src/Runtime/Common/InputVisualizationCanvas.cs +++ b/Packages/src/Runtime/Common/InputVisualizationCanvas.cs @@ -1,3 +1,4 @@ +#if UNITY_EDITOR using UnityEngine; namespace io.github.hatayama.uLoopMCP @@ -27,3 +28,4 @@ private void Awake() } } } +#endif diff --git a/Packages/src/Runtime/RecordInput/RecordInputOverlayPresenter.cs b/Packages/src/Runtime/RecordInput/RecordInputOverlayPresenter.cs index 4789151f8b..89e076adb1 100644 --- a/Packages/src/Runtime/RecordInput/RecordInputOverlayPresenter.cs +++ b/Packages/src/Runtime/RecordInput/RecordInputOverlayPresenter.cs @@ -1,3 +1,4 @@ +#if UNITY_EDITOR using System.Globalization; using UnityEngine; @@ -87,3 +88,4 @@ private string FormatStatusText(float seconds) } } } +#endif diff --git a/Packages/src/Runtime/RecordInput/RecordInputOverlayView.cs b/Packages/src/Runtime/RecordInput/RecordInputOverlayView.cs index 3c2e06aeca..9033601266 100644 --- a/Packages/src/Runtime/RecordInput/RecordInputOverlayView.cs +++ b/Packages/src/Runtime/RecordInput/RecordInputOverlayView.cs @@ -1,3 +1,4 @@ +#if UNITY_EDITOR using UnityEngine; using UnityEngine.UI; @@ -71,3 +72,4 @@ public void Hide() } } } +#endif diff --git a/Packages/src/Runtime/ReplayInput/ReplayInputOverlay.cs b/Packages/src/Runtime/ReplayInput/ReplayInputOverlay.cs index 6106ee869a..2271542c71 100644 --- a/Packages/src/Runtime/ReplayInput/ReplayInputOverlay.cs +++ b/Packages/src/Runtime/ReplayInput/ReplayInputOverlay.cs @@ -1,3 +1,4 @@ +#if UNITY_EDITOR #nullable enable using UnityEngine; using UnityEngine.UI; @@ -105,3 +106,4 @@ private void SetVisible(bool visible) } } } +#endif diff --git a/Packages/src/Runtime/SimulateKeyboard/SimulateKeyboardOverlay.cs b/Packages/src/Runtime/SimulateKeyboard/SimulateKeyboardOverlay.cs index b5aeea4ccc..ccb6d436b8 100644 --- a/Packages/src/Runtime/SimulateKeyboard/SimulateKeyboardOverlay.cs +++ b/Packages/src/Runtime/SimulateKeyboard/SimulateKeyboardOverlay.cs @@ -1,3 +1,4 @@ +#if UNITY_EDITOR #nullable enable using System.Collections.Generic; using UnityEngine; @@ -200,3 +201,4 @@ public BadgeEntry(GameObject root, Image background, Text text, LayoutElement la } } } +#endif diff --git a/Packages/src/Runtime/SimulateMouseInput/SimulateMouseInputOverlay.cs b/Packages/src/Runtime/SimulateMouseInput/SimulateMouseInputOverlay.cs index 6f9b2dfa64..8fc65d860f 100644 --- a/Packages/src/Runtime/SimulateMouseInput/SimulateMouseInputOverlay.cs +++ b/Packages/src/Runtime/SimulateMouseInput/SimulateMouseInputOverlay.cs @@ -1,3 +1,4 @@ +#if UNITY_EDITOR #nullable enable using UnityEngine; using UnityEngine.UI; @@ -144,4 +145,5 @@ private void UpdateMoveDirection() _moveDirectionGroup.localRotation = Quaternion.Euler(0f, 0f, angle); } } -} \ No newline at end of file +} +#endif diff --git a/Packages/src/Runtime/SimulateMouseInput/SimulateMouseInputOverlay.prefab b/Packages/src/Runtime/SimulateMouseInput/SimulateMouseInputOverlay.prefab index a259f3e674..592afb0d76 100644 --- a/Packages/src/Runtime/SimulateMouseInput/SimulateMouseInputOverlay.prefab +++ b/Packages/src/Runtime/SimulateMouseInput/SimulateMouseInputOverlay.prefab @@ -257,7 +257,6 @@ GameObject: m_Component: - component: {fileID: 8124307820913732345} - component: {fileID: 8863844378423265032} - - component: {fileID: 5623037964506870047} - component: {fileID: 8237013949146870878} m_Layer: 0 m_Name: SimulateMouseInputOverlay @@ -304,18 +303,6 @@ MonoBehaviour: _scrollArrowTop: {fileID: 4893873975114215188} _scrollArrowBottom: {fileID: 6545894353452666225} _moveDirectionGroup: {fileID: 5622481146988834445} ---- !u!114 &5623037964506870047 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 2122203629555508201} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 3661bc54aaacf4bc09ae641533d2a78e, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!225 &8237013949146870878 CanvasGroup: m_ObjectHideFlags: 0 diff --git a/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiOverlay.cs b/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiOverlay.cs index 53439b3286..87254e6f6a 100644 --- a/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiOverlay.cs +++ b/Packages/src/Runtime/SimulateMouseUi/SimulateMouseUiOverlay.cs @@ -1,3 +1,4 @@ +#if UNITY_EDITOR #nullable enable using System.Collections.Generic; using UnityEngine; @@ -314,3 +315,4 @@ private static Image CreateImage(string name, Transform parent) } } } +#endif diff --git a/Packages/src/Runtime/uLoopMCP.Runtime.asmdef b/Packages/src/Runtime/uLoopMCP.Runtime.asmdef index 4357a3eba3..63d7770187 100644 --- a/Packages/src/Runtime/uLoopMCP.Runtime.asmdef +++ b/Packages/src/Runtime/uLoopMCP.Runtime.asmdef @@ -2,14 +2,12 @@ "name": "uLoopMCP.Runtime", "rootNamespace": "", "references": [], - "includePlatforms": [ - "Editor" - ], + "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], - "autoReferenced": true, + "autoReferenced": false, "defineConstraints": [], "versionDefines": [], "noEngineReferences": false From 91c154500ba57b1e20167ea45e860579f268a43c Mon Sep 17 00:00:00 2001 From: hatayama Date: Thu, 4 Jun 2026 23:27:04 +0900 Subject: [PATCH 2/6] Fix mouse replay overlay E2E Ensure the mouse replay E2E runs against the same Full HD Game View size as its fixture and enables replay visualization so the pointer overlay is exercised. Wire the overlay prefab references through Unity serialization, and avoid duplicate UI dispatch when InputSystemUIInputModule already consumes injected mouse state. --- Assets/Tests/Demo/uLoopMCP.Tests.Demo.asmdef | 10 +- .../PlayMode/SimulateMouseDemoE2ETests.cs | 35 ++- .../Api/McpTools/ReplayInput/InputReplayer.cs | 172 ++++++++++--- .../McpTools/ReplayInput/ReplayInputTool.cs | 2 - .../Common/InputVisualizationCanvas.prefab | 238 ++++++++++++++++++ 5 files changed, 408 insertions(+), 49 deletions(-) diff --git a/Assets/Tests/Demo/uLoopMCP.Tests.Demo.asmdef b/Assets/Tests/Demo/uLoopMCP.Tests.Demo.asmdef index b16dcfa74a..fcf34e787c 100644 --- a/Assets/Tests/Demo/uLoopMCP.Tests.Demo.asmdef +++ b/Assets/Tests/Demo/uLoopMCP.Tests.Demo.asmdef @@ -6,15 +6,15 @@ "GUID:c956a21f824994ef087b6de566690b3d", "GUID:4307f53044263cf4b835bd812fc161a4" ], - "includePlatforms": [ - "Editor" - ], + "includePlatforms": [], "excludePlatforms": [], "allowUnsafeCode": false, "overrideReferences": false, "precompiledReferences": [], - "autoReferenced": true, - "defineConstraints": [], + "autoReferenced": false, + "defineConstraints": [ + "UNITY_INCLUDE_TESTS" + ], "versionDefines": [ { "name": "com.unity.inputsystem", diff --git a/Assets/Tests/PlayMode/SimulateMouseDemoE2ETests.cs b/Assets/Tests/PlayMode/SimulateMouseDemoE2ETests.cs index 6a8db3feee..67570b7bc6 100644 --- a/Assets/Tests/PlayMode/SimulateMouseDemoE2ETests.cs +++ b/Assets/Tests/PlayMode/SimulateMouseDemoE2ETests.cs @@ -1,8 +1,10 @@ #if ULOOPMCP_HAS_INPUT_SYSTEM using System.Collections; using System.IO; +using System.Reflection; using io.github.hatayama.uLoopMCP; using NUnit.Framework; +using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; @@ -15,6 +17,9 @@ public class SimulateMouseDemoE2ETests private const string SCENE_PATH = "Assets/Scenes/SimulateMouseDemoScene.unity"; private const string FIXTURE_DIR = "Assets/Tests/PlayMode/Fixtures/SimulateMouseDemoScene"; private const float REPLAY_TIMEOUT_SECONDS = 30f; + private const int FULL_HD_WIDTH = 1920; + private const int FULL_HD_HEIGHT = 1080; + private const string FULL_HD_LABEL = "uLoop E2E Full HD"; private bool _replayCompleted; @@ -23,6 +28,7 @@ public IEnumerator SetUp() { _replayCompleted = false; InputReplayer.ReplayCompleted += OnReplayCompleted; + SetGameViewResolution(); AsyncOperation loadOp = EditorSceneManager.LoadSceneAsyncInPlayMode( SCENE_PATH, @@ -33,6 +39,8 @@ public IEnumerator SetUp() yield return null; } + AssertGameViewResolution(); + // EditorBridge [InitializeOnLoad] subscribes on the first frame after load; // second yield ensures its event hooks are active before replay starts. yield return null; @@ -80,7 +88,7 @@ public IEnumerator Replay_Should_ProduceIdenticalEventLog() InputRecordingData recordingData = InputRecordingFileHelper.Load(fixtureRecordingJson); Debug.Assert(recordingData != null, $"Failed to load fixture: {fixtureRecordingJson}"); - InputReplayer.StartReplay(recordingData, loop: false, showOverlay: false); + InputReplayer.StartReplay(recordingData, loop: false, showOverlay: true); float timeoutAt = Time.realtimeSinceStartup + REPLAY_TIMEOUT_SECONDS; yield return new WaitUntil(() => @@ -105,6 +113,31 @@ private void OnReplayCompleted() _replayCompleted = true; } + private static void SetGameViewResolution() + { + System.Type gameViewType = typeof(Editor).Assembly.GetType("UnityEditor.GameView"); + Debug.Assert(gameViewType != null, "GameView type must exist"); + + EditorWindow gameView = EditorWindow.GetWindow(gameViewType); + MethodInfo setCustomResolution = gameViewType.GetMethod( + "SetCustomResolution", + BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); + Debug.Assert(setCustomResolution != null, "GameView.SetCustomResolution must exist"); + + // Unity exposes no public API for Game View target resolution, but this E2E + // fixture must replay against the same pixel size as the recorded mouse positions. + setCustomResolution.Invoke( + gameView, + new object[] { new Vector2(FULL_HD_WIDTH, FULL_HD_HEIGHT), FULL_HD_LABEL }); + } + + private static void AssertGameViewResolution() + { + Vector2 size = Handles.GetMainGameViewSize(); + Assert.AreEqual(FULL_HD_WIDTH, Mathf.RoundToInt(size.x), "Game View width must match fixture recording"); + Assert.AreEqual(FULL_HD_HEIGHT, Mathf.RoundToInt(size.y), "Game View height must match fixture recording"); + } + private static void CleanupLogFile(string path) { if (File.Exists(path)) diff --git a/Packages/src/Editor/Api/McpTools/ReplayInput/InputReplayer.cs b/Packages/src/Editor/Api/McpTools/ReplayInput/InputReplayer.cs index 6aa4622536..e0fe8dd2df 100644 --- a/Packages/src/Editor/Api/McpTools/ReplayInput/InputReplayer.cs +++ b/Packages/src/Editor/Api/McpTools/ReplayInput/InputReplayer.cs @@ -9,6 +9,7 @@ using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.InputSystem.LowLevel; +using UnityEngine.InputSystem.UI; namespace io.github.hatayama.uLoopMCP { @@ -36,8 +37,8 @@ internal static class InputReplayer private static readonly HashSet _replayHeldButtons = new(); private static Vector2? _replayMousePosition; - // InputModule (StandaloneInputModule / InputSystemUIInputModule) ignores injected - // Mouse.current state, so UI interactions must go through ExecuteEvents directly. + // Some UI modules do not consume injected Mouse.current state, so replay keeps + // manual ExecuteEvents support for those modules. private static bool _hasMousePosition; private static bool _prevLeftButtonHeld; private static Vector2? _previousReplayMousePosition; @@ -86,10 +87,8 @@ public static void StartReplay(InputRecordingData data, bool loop, bool showOver _replayHeldButtons.Clear(); _hasMousePosition = DetectMousePositionEvents(data!); ResetUiReplayState(); - if (_hasMousePosition) - { - SimulateMouseInputOverlayState.Clear(); - } + EnsureOverlayForReplay(showOverlay); + ClearOverlayState(); _isReplaying = true; InputSystem.onAfterUpdate -= OnAfterUpdate; @@ -117,11 +116,7 @@ public static void StopReplay() _replayHeldButtons.Clear(); ResetUiReplayState(); - ReplayInputOverlayState.Clear(); - if (_hasMousePosition) - { - SimulateMouseUiOverlayState.Clear(); - } + ClearOverlayState(); } private static void OnAfterUpdate() @@ -245,7 +240,10 @@ private static void ProcessKeyDown(string keyName) } _replayHeldKeys.Add(key); - SimulateKeyboardOverlayState.AddHeldKey(keyName); + if (_showOverlay) + { + SimulateKeyboardOverlayState.AddHeldKey(keyName); + } } private static void ProcessKeyUp(string keyName) @@ -256,7 +254,10 @@ private static void ProcessKeyUp(string keyName) } _replayHeldKeys.Remove(key); - SimulateKeyboardOverlayState.RemoveHeldKey(keyName); + if (_showOverlay) + { + SimulateKeyboardOverlayState.RemoveHeldKey(keyName); + } } private static void ProcessMouseClick(string buttonName) @@ -267,7 +268,7 @@ private static void ProcessMouseClick(string buttonName) } _replayHeldButtons.Add(button); - if (!_hasMousePosition) + if (_showOverlay && !_hasMousePosition) { SimulateMouseInputOverlayState.SetButtonHeld(button, true); } @@ -281,7 +282,7 @@ private static void ProcessMouseRelease(string buttonName) } _replayHeldButtons.Remove(button); - if (!_hasMousePosition) + if (_showOverlay && !_hasMousePosition) { SimulateMouseInputOverlayState.SetButtonHeld(button, false); } @@ -290,7 +291,7 @@ private static void ProcessMouseRelease(string buttonName) private static void ProcessMouseDelta(string data, ref Vector2 frameDelta) { frameDelta = InputRecorder.ParseVector2(data); - if (!_hasMousePosition) + if (_showOverlay && !_hasMousePosition) { SimulateMouseInputOverlayState.SetMoveDelta(frameDelta); } @@ -309,7 +310,7 @@ private static void ProcessMouseScroll(string data, ref Vector2 frameScroll) } frameScroll = new Vector2(0f, scrollY); - if (!_hasMousePosition) + if (_showOverlay && !_hasMousePosition) { int direction = scrollY > 0f ? 1 : scrollY < 0f ? -1 : 0; SimulateMouseInputOverlayState.SetScrollDirection(direction); @@ -389,18 +390,21 @@ private static void ReleaseAllHeldInputs() ApplyMouseSnapshot(mouse, _emptyButtons, Vector2.zero, Vector2.zero, null); } - foreach (Key key in _replayHeldKeys) + if (_showOverlay) { - SimulateKeyboardOverlayState.RemoveHeldKey(key.ToString()); - } + foreach (Key key in _replayHeldKeys) + { + SimulateKeyboardOverlayState.RemoveHeldKey(key.ToString()); + } - foreach (MouseButton button in _replayHeldButtons) - { - SimulateMouseInputOverlayState.SetButtonHeld(button, false); - } + foreach (MouseButton button in _replayHeldButtons) + { + SimulateMouseInputOverlayState.SetButtonHeld(button, false); + } - SimulateMouseInputOverlayState.SetMoveDelta(Vector2.zero); - SimulateMouseInputOverlayState.SetScrollDirection(0); + SimulateMouseInputOverlayState.SetMoveDelta(Vector2.zero); + SimulateMouseInputOverlayState.SetScrollDirection(0); + } _replayHeldKeys.Clear(); _replayHeldButtons.Clear(); } @@ -466,30 +470,49 @@ private static void ApplyUiEvents() Vector2 gameViewSize = Handles.GetMainGameViewSize(); Vector2 inputPos = new Vector2(screenPos.x, gameViewSize.y - screenPos.y); + bool dispatchUiEventsManually = ShouldDispatchUiEventsManually(eventSystem); if (justPressed) { _suppressIdleUiOverlay = false; _pressTime = Time.realtimeSinceStartup; - OnUiPointerDown(screenPos, eventSystem); - SimulateMouseUiOverlayState.Update( - MouseAction.Click, inputPos, null, _currentPressTarget?.name, gameViewSize); - SimulateMouseUiOverlayState.RequestExpandAnimation(); + _pressScreenPosition = screenPos; + _isDragging = false; + if (dispatchUiEventsManually) + { + OnUiPointerDown(screenPos, eventSystem); + } + if (_showOverlay) + { + SimulateMouseUiOverlayState.Update( + MouseAction.Click, inputPos, null, _currentPressTarget?.name, gameViewSize); + SimulateMouseUiOverlayState.RequestExpandAnimation(); + } } - else if (leftHeld && (_currentPressTarget != null || _currentDragTarget != null)) + else if (leftHeld && ShouldTrackHeldPointer(dispatchUiEventsManually)) { - OnUiDrag(screenPos); + if (dispatchUiEventsManually) + { + OnUiDrag(screenPos); + } + else + { + UpdateOverlayDragState(screenPos, eventSystem); + } if (_isDragging) { - Vector2 pressInputPos = new Vector2(_pressScreenPosition.x, gameViewSize.y - _pressScreenPosition.y); - SimulateMouseUiOverlayState.Update( - MouseAction.Drag, inputPos, pressInputPos, null, gameViewSize); + if (_showOverlay) + { + Vector2 pressInputPos = new Vector2(_pressScreenPosition.x, gameViewSize.y - _pressScreenPosition.y); + SimulateMouseUiOverlayState.Update( + MouseAction.Drag, inputPos, pressInputPos, null, gameViewSize); + } } else { float elapsed = Time.realtimeSinceStartup - _pressTime; - if (elapsed >= 0.5f) + if (_showOverlay && elapsed >= 0.5f) { SimulateMouseUiOverlayState.Update( MouseAction.LongPress, inputPos, null, _currentPressTarget?.name, gameViewSize); @@ -502,16 +525,78 @@ private static void ApplyUiEvents() // Keeping the overlay hidden until the pointer actually moves prevents release fade-out // from being cancelled by the next idle frame at the same position. _suppressIdleUiOverlay = false; - SimulateMouseUiOverlayState.Update( - MouseAction.Click, inputPos, null, null, gameViewSize); + if (_showOverlay) + { + SimulateMouseUiOverlayState.Update( + MouseAction.Click, inputPos, null, null, gameViewSize); + } } if (justReleased) { - OnUiPointerUp(screenPos, eventSystem); + if (dispatchUiEventsManually) + { + OnUiPointerUp(screenPos, eventSystem); + } + else + { + ClearPointerState(); + } _suppressIdleUiOverlay = true; - SimulateMouseUiOverlayState.RequestDissipateAnimation(); - SimulateMouseUiOverlayState.Clear(); + if (_showOverlay) + { + SimulateMouseUiOverlayState.RequestDissipateAnimation(); + SimulateMouseUiOverlayState.Clear(); + } + } + } + + private static void EnsureOverlayForReplay(bool showOverlay) + { + if (!showOverlay) + { + return; + } + + OverlayCanvasFactory.EnsureExists(); + RecordReplayOverlayFactory.EnsureReplayOverlay(); + } + + private static void ClearOverlayState() + { + ReplayInputOverlayState.Clear(); + SimulateMouseUiOverlayState.Clear(); + SimulateMouseInputOverlayState.Clear(); + SimulateKeyboardOverlayState.Clear(); + } + + private static bool ShouldDispatchUiEventsManually(EventSystem eventSystem) + { + BaseInputModule? inputModule = eventSystem.currentInputModule; + return !(inputModule is InputSystemUIInputModule); + } + + private static bool ShouldTrackHeldPointer(bool dispatchUiEventsManually) + { + if (!dispatchUiEventsManually) + { + return true; + } + + return _currentPressTarget != null || _currentDragTarget != null; + } + + private static void UpdateOverlayDragState(Vector2 screenPos, EventSystem eventSystem) + { + if (_isDragging) + { + return; + } + + float distance = (screenPos - _pressScreenPosition).magnitude; + if (distance > eventSystem.pixelDragThreshold) + { + _isDragging = true; } } @@ -631,6 +716,11 @@ private static void OnUiPointerUp(Vector2 screenPos, EventSystem eventSystem) } } + ClearPointerState(); + } + + private static void ClearPointerState() + { _currentPressTarget = null; _currentDragTarget = null; _isDragging = false; diff --git a/Packages/src/Editor/Api/McpTools/ReplayInput/ReplayInputTool.cs b/Packages/src/Editor/Api/McpTools/ReplayInput/ReplayInputTool.cs index fc813b29b9..3df27ced03 100644 --- a/Packages/src/Editor/Api/McpTools/ReplayInput/ReplayInputTool.cs +++ b/Packages/src/Editor/Api/McpTools/ReplayInput/ReplayInputTool.cs @@ -157,8 +157,6 @@ private static ReplayInputResponse ExecuteStart(ReplayInputSchema parameters) }; } - OverlayCanvasFactory.EnsureExists(); - RecordReplayOverlayFactory.EnsureReplayOverlay(); InputReplayer.StartReplay(data, parameters.Loop, parameters.ShowOverlay); int eventCount = data.GetTotalEventCount(); diff --git a/Packages/src/Runtime/Common/InputVisualizationCanvas.prefab b/Packages/src/Runtime/Common/InputVisualizationCanvas.prefab index 1a8d84bba4..48f80a9791 100644 --- a/Packages/src/Runtime/Common/InputVisualizationCanvas.prefab +++ b/Packages/src/Runtime/Common/InputVisualizationCanvas.prefab @@ -211,11 +211,71 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} + - target: {fileID: 7304014939787943379, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + propertyPath: _view + value: + objectReference: {fileID: 7500804184470111482} + - target: {fileID: 7672979299455549903, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + propertyPath: _recDotText + value: + objectReference: {fileID: 8650917651823086408} + - target: {fileID: 7672979299455549903, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + propertyPath: _statusText + value: + objectReference: {fileID: 799087218663616109} + - target: {fileID: 7672979299455549903, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + propertyPath: _canvasGroup + value: + objectReference: {fileID: 1512996111237769590} + - target: {fileID: 7672979299455549903, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + propertyPath: _countdownText + value: + objectReference: {fileID: 5622076999123470954} + - target: {fileID: 7672979299455549903, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + propertyPath: _countdownGroup + value: + objectReference: {fileID: 8864626840232069285} + - target: {fileID: 7672979299455549903, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + propertyPath: _recordingGroup + value: + objectReference: {fileID: 156814871501579555} m_RemovedComponents: [] m_RemovedGameObjects: [] m_AddedGameObjects: [] m_AddedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} +--- !u!1 &156814871501579555 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 22181428640489494, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + m_PrefabInstance: {fileID: 172213960314160437} + m_PrefabAsset: {fileID: 0} +--- !u!114 &799087218663616109 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 681519330149296984, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + m_PrefabInstance: {fileID: 172213960314160437} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!225 &1512996111237769590 stripped +CanvasGroup: + m_CorrespondingSourceObject: {fileID: 1629439249830669379, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + m_PrefabInstance: {fileID: 172213960314160437} + m_PrefabAsset: {fileID: 0} +--- !u!114 &5622076999123470954 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 5505213563100565343, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + m_PrefabInstance: {fileID: 172213960314160437} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!224 &6349903084446132074 stripped RectTransform: m_CorrespondingSourceObject: {fileID: 6520280533638980191, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} @@ -232,6 +292,33 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 55b9e485125e14c979260f3aa7816f9c, type: 3} m_Name: m_EditorClassIdentifier: +--- !u!114 &7500804184470111482 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 7672979299455549903, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + m_PrefabInstance: {fileID: 172213960314160437} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 626f611c789dd4febbff8920eeb25837, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!114 &8650917651823086408 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 8821962787790114429, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + m_PrefabInstance: {fileID: 172213960314160437} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!1 &8864626840232069285 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 8747864644649913744, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + m_PrefabInstance: {fileID: 172213960314160437} + m_PrefabAsset: {fileID: 0} --- !u!1001 &828123329405598390 PrefabInstance: m_ObjectHideFlags: 0 @@ -320,6 +407,38 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} + - target: {fileID: 7252727134433425355, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + propertyPath: _crosshairH + value: + objectReference: {fileID: 3067451695685099051} + - target: {fileID: 7252727134433425355, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + propertyPath: _crosshairV + value: + objectReference: {fileID: 2321804306982394422} + - target: {fileID: 7252727134433425355, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + propertyPath: _canvasGroup + value: + objectReference: {fileID: 6408876800999454565} + - target: {fileID: 7252727134433425355, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + propertyPath: _circleImage + value: + objectReference: {fileID: 2042927172406633204} + - target: {fileID: 7252727134433425355, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + propertyPath: _cursorGroup + value: + objectReference: {fileID: 1411858458224765341} + - target: {fileID: 7252727134433425355, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + propertyPath: _circleSprite + value: + objectReference: {fileID: 21300000, guid: 0294a084c436048db9ed81eac798a709, type: 3} + - target: {fileID: 7252727134433425355, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + propertyPath: _longPressText + value: + objectReference: {fileID: 7219080529419826848} + - target: {fileID: 7252727134433425355, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + propertyPath: _dragStartMarker + value: + objectReference: {fileID: 1578214708066381370} - target: {fileID: 9106729417482781781, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} propertyPath: m_Name value: SimulateMouseUiOverlay @@ -329,11 +448,76 @@ PrefabInstance: m_AddedGameObjects: [] m_AddedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} +--- !u!224 &1411858458224765341 stripped +RectTransform: + m_CorrespondingSourceObject: {fileID: 1795238667056386859, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + m_PrefabInstance: {fileID: 828123329405598390} + m_PrefabAsset: {fileID: 0} +--- !u!114 &1578214708066381370 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 2204762901719864460, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + m_PrefabInstance: {fileID: 828123329405598390} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 0 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!114 &2042927172406633204 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 1668577853838809154, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + m_PrefabInstance: {fileID: 828123329405598390} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!114 &2321804306982394422 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 3118357298515763328, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + m_PrefabInstance: {fileID: 828123329405598390} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!114 &3067451695685099051 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 2445404945182483613, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + m_PrefabInstance: {fileID: 828123329405598390} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!224 &4617186797138212377 stripped RectTransform: m_CorrespondingSourceObject: {fileID: 5435175328419204271, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} m_PrefabInstance: {fileID: 828123329405598390} m_PrefabAsset: {fileID: 0} +--- !u!225 &6408876800999454565 stripped +CanvasGroup: + m_CorrespondingSourceObject: {fileID: 6021016629897285075, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + m_PrefabInstance: {fileID: 828123329405598390} + m_PrefabAsset: {fileID: 0} +--- !u!114 &7219080529419826848 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 8021270174096083990, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + m_PrefabInstance: {fileID: 828123329405598390} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 0 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!114 &8059412677754260861 stripped MonoBehaviour: m_CorrespondingSourceObject: {fileID: 7252727134433425355, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} @@ -478,6 +662,14 @@ PrefabInstance: serializedVersion: 3 m_TransformParent: {fileID: 855430283409054088} m_Modifications: + - target: {fileID: 750881311793061267, guid: 6ddbcdb56af1c46719cf4fa1b302f1b4, type: 3} + propertyPath: _statusText + value: + objectReference: {fileID: 2893399217642933393} + - target: {fileID: 750881311793061267, guid: 6ddbcdb56af1c46719cf4fa1b302f1b4, type: 3} + propertyPath: _progressBarFill + value: + objectReference: {fileID: 2064571181623911462} - target: {fileID: 1866048660558847936, guid: 6ddbcdb56af1c46719cf4fa1b302f1b4, type: 3} propertyPath: m_Pivot.x value: 0.5 @@ -583,6 +775,28 @@ PrefabInstance: m_AddedGameObjects: [] m_AddedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: 6ddbcdb56af1c46719cf4fa1b302f1b4, type: 3} +--- !u!114 &2064571181623911462 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 5900655728717313438, guid: 6ddbcdb56af1c46719cf4fa1b302f1b4, type: 3} + m_PrefabInstance: {fileID: 5568001178914132408} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!114 &2893399217642933393 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 7305657527396605225, guid: 6ddbcdb56af1c46719cf4fa1b302f1b4, type: 3} + m_PrefabInstance: {fileID: 5568001178914132408} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 5f7201a12d95ffc409449d95f23cf332, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!114 &5129082111969823787 stripped MonoBehaviour: m_CorrespondingSourceObject: {fileID: 750881311793061267, guid: 6ddbcdb56af1c46719cf4fa1b302f1b4, type: 3} @@ -759,6 +973,14 @@ PrefabInstance: propertyPath: m_AnchoredPosition.y value: 0 objectReference: {fileID: 0} + - target: {fileID: 6082752830946661477, guid: 13a5dea128bba49df91807161c323487, type: 3} + propertyPath: _container + value: + objectReference: {fileID: 6364288575518178073} + - target: {fileID: 6082752830946661477, guid: 13a5dea128bba49df91807161c323487, type: 3} + propertyPath: _containerImage + value: + objectReference: {fileID: 1286791144272469249} - target: {fileID: 8394911915651975566, guid: 13a5dea128bba49df91807161c323487, type: 3} propertyPath: m_SizeDelta.x value: 0 @@ -780,6 +1002,17 @@ PrefabInstance: m_AddedGameObjects: [] m_AddedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: 13a5dea128bba49df91807161c323487, type: 3} +--- !u!114 &1286791144272469249 stripped +MonoBehaviour: + m_CorrespondingSourceObject: {fileID: 8787032785713595466, guid: 13a5dea128bba49df91807161c323487, type: 3} + m_PrefabInstance: {fileID: 7505906806931134795} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 6364288575518178073} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: --- !u!114 &4341488740020178222 stripped MonoBehaviour: m_CorrespondingSourceObject: {fileID: 6082752830946661477, guid: 13a5dea128bba49df91807161c323487, type: 3} @@ -791,6 +1024,11 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 22d59970393a54b4cb41f956e0eb3f41, type: 3} m_Name: m_EditorClassIdentifier: +--- !u!1 &6364288575518178073 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 3492586335444099666, guid: 13a5dea128bba49df91807161c323487, type: 3} + m_PrefabInstance: {fileID: 7505906806931134795} + m_PrefabAsset: {fileID: 0} --- !u!224 &8952917233715870054 stripped RectTransform: m_CorrespondingSourceObject: {fileID: 1447186769560363053, guid: 13a5dea128bba49df91807161c323487, type: 3} From 554af19954243bdf02270761a30d8fea7968e185 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 5 Jun 2026 00:02:53 +0900 Subject: [PATCH 3/6] Mark input overlay prefab as EditorOnly Tag every GameObject in the shared input visualization prefab as EditorOnly so Unity can exclude the instantiated overlay hierarchy from player scenes while keeping prefab script references resolvable in the editor. --- .../Common/InputVisualizationCanvas.prefab | 138 +++++++++++++++++- 1 file changed, 137 insertions(+), 1 deletion(-) diff --git a/Packages/src/Runtime/Common/InputVisualizationCanvas.prefab b/Packages/src/Runtime/Common/InputVisualizationCanvas.prefab index 48f80a9791..c49292221e 100644 --- a/Packages/src/Runtime/Common/InputVisualizationCanvas.prefab +++ b/Packages/src/Runtime/Common/InputVisualizationCanvas.prefab @@ -15,7 +15,7 @@ GameObject: - component: {fileID: 5473869302179759802} m_Layer: 0 m_Name: InputVisualizationCanvas - m_TagString: Untagged + m_TagString: EditorOnly m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 @@ -127,10 +127,18 @@ PrefabInstance: serializedVersion: 3 m_TransformParent: {fileID: 855430283409054088} m_Modifications: + - target: {fileID: 22181428640489494, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} - target: {fileID: 5910659660082971454, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} propertyPath: m_Name value: RecordInputOverlay objectReference: {fileID: 0} + - target: {fileID: 5910659660082971454, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} - target: {fileID: 6520280533638980191, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} propertyPath: m_Pivot.x value: 0.5 @@ -211,6 +219,10 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} + - target: {fileID: 7302841544564334857, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} - target: {fileID: 7304014939787943379, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} propertyPath: _view value: @@ -239,6 +251,18 @@ PrefabInstance: propertyPath: _recordingGroup value: objectReference: {fileID: 156814871501579555} + - target: {fileID: 8311708416284801602, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} + - target: {fileID: 8406836639261979753, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} + - target: {fileID: 8747864644649913744, guid: 62ec688be40ba465a8653a8437b9a739, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} m_RemovedComponents: [] m_RemovedGameObjects: [] m_AddedGameObjects: [] @@ -327,6 +351,18 @@ PrefabInstance: serializedVersion: 3 m_TransformParent: {fileID: 855430283409054088} m_Modifications: + - target: {fileID: 3579496036604184255, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} + - target: {fileID: 3645456351603840414, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} + - target: {fileID: 4096853404940709834, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} - target: {fileID: 5435175328419204271, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} propertyPath: m_Pivot.x value: 0 @@ -407,6 +443,10 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} + - target: {fileID: 6086391879608123167, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} - target: {fileID: 7252727134433425355, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} propertyPath: _crosshairH value: @@ -439,10 +479,22 @@ PrefabInstance: propertyPath: _dragStartMarker value: objectReference: {fileID: 1578214708066381370} + - target: {fileID: 7486829258772114075, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} + - target: {fileID: 8398465818153543159, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} - target: {fileID: 9106729417482781781, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} propertyPath: m_Name value: SimulateMouseUiOverlay objectReference: {fileID: 0} + - target: {fileID: 9106729417482781781, guid: f47ccc1f9351044799d3c9da246db73a, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} m_RemovedComponents: [] m_RemovedGameObjects: [] m_AddedGameObjects: [] @@ -537,10 +589,30 @@ PrefabInstance: serializedVersion: 3 m_TransformParent: {fileID: 855430283409054088} m_Modifications: + - target: {fileID: 253023116830826680, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} + - target: {fileID: 641186257110384445, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} + - target: {fileID: 1893870842996581666, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} - target: {fileID: 2122203629555508201, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} propertyPath: m_Name value: SimulateMouseInputOverlay objectReference: {fileID: 0} + - target: {fileID: 2122203629555508201, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} + - target: {fileID: 2131268326784498726, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} - target: {fileID: 5622481146988834445, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} propertyPath: m_LocalRotation.x value: 0 @@ -553,6 +625,26 @@ PrefabInstance: propertyPath: m_LocalRotation.z value: 0 objectReference: {fileID: 0} + - target: {fileID: 6254897849895741874, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} + - target: {fileID: 7009724595179871094, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} + - target: {fileID: 7345968847908351245, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} + - target: {fileID: 7553317276571807195, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} + - target: {fileID: 8009591172217868134, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} - target: {fileID: 8124307820913732345, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} propertyPath: m_Pivot.x value: 0 @@ -633,6 +725,18 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} + - target: {fileID: 8293422726571725699, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} + - target: {fileID: 8702196187181304333, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} + - target: {fileID: 8719267255120921363, guid: 242afb3abe6ac48f99512e35951c9978, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} m_RemovedComponents: [] m_RemovedGameObjects: [] m_AddedGameObjects: [] @@ -754,10 +858,18 @@ PrefabInstance: propertyPath: m_IsActive value: 1 objectReference: {fileID: 0} + - target: {fileID: 3143306149169442488, guid: 6ddbcdb56af1c46719cf4fa1b302f1b4, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} - target: {fileID: 4291754732258876827, guid: 6ddbcdb56af1c46719cf4fa1b302f1b4, type: 3} propertyPath: m_Name value: ReplayInputOverlay objectReference: {fileID: 0} + - target: {fileID: 4291754732258876827, guid: 6ddbcdb56af1c46719cf4fa1b302f1b4, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} - target: {fileID: 4495645932648773490, guid: 6ddbcdb56af1c46719cf4fa1b302f1b4, type: 3} propertyPath: m_SizeDelta.y value: -3 @@ -770,6 +882,14 @@ PrefabInstance: propertyPath: m_IsActive value: 1 objectReference: {fileID: 0} + - target: {fileID: 5842103157894414940, guid: 6ddbcdb56af1c46719cf4fa1b302f1b4, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} + - target: {fileID: 8320914726685114482, guid: 6ddbcdb56af1c46719cf4fa1b302f1b4, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} m_RemovedComponents: [] m_RemovedGameObjects: [] m_AddedGameObjects: [] @@ -821,6 +941,10 @@ PrefabInstance: serializedVersion: 3 m_TransformParent: {fileID: 855430283409054088} m_Modifications: + - target: {fileID: 1163102534174049270, guid: 13a5dea128bba49df91807161c323487, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} - target: {fileID: 1254632121800322779, guid: 13a5dea128bba49df91807161c323487, type: 3} propertyPath: m_AnchorMax.x value: 0 @@ -949,6 +1073,10 @@ PrefabInstance: propertyPath: m_LocalEulerAnglesHint.z value: 0 objectReference: {fileID: 0} + - target: {fileID: 3492586335444099666, guid: 13a5dea128bba49df91807161c323487, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} - target: {fileID: 5371017374443542522, guid: 13a5dea128bba49df91807161c323487, type: 3} propertyPath: m_AnchorMax.x value: 0 @@ -981,6 +1109,10 @@ PrefabInstance: propertyPath: _containerImage value: objectReference: {fileID: 1286791144272469249} + - target: {fileID: 6281231881990964606, guid: 13a5dea128bba49df91807161c323487, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} - target: {fileID: 8394911915651975566, guid: 13a5dea128bba49df91807161c323487, type: 3} propertyPath: m_SizeDelta.x value: 0 @@ -997,6 +1129,10 @@ PrefabInstance: propertyPath: m_Name value: SimulateKeyboardOverlay objectReference: {fileID: 0} + - target: {fileID: 9003946234088771061, guid: 13a5dea128bba49df91807161c323487, type: 3} + propertyPath: m_TagString + value: EditorOnly + objectReference: {fileID: 0} m_RemovedComponents: [] m_RemovedGameObjects: [] m_AddedGameObjects: [] From 1a2e3c99ccd7c44cf7c69dac40e8dd8b05966e9d Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 5 Jun 2026 00:15:17 +0900 Subject: [PATCH 4/6] Filter prefab-only runtime assembly from player builds Keep the runtime asmdef player-compatible so overlay prefabs resolve in the Editor, then remove uLoopMCP.Runtime.dll from Player build assembly lists through Unity's build assembly filter callback. --- .../Editor/RuntimeAssemblyBuildFilterTests.cs | 31 ++++++++++++++ .../RuntimeAssemblyBuildFilterTests.cs.meta | 11 +++++ .../Build/RuntimeAssemblyBuildFilter.cs | 42 +++++++++++++++++++ .../Build/RuntimeAssemblyBuildFilter.cs.meta | 11 +++++ .../src/Editor/Shared/Config/McpConstants.cs | 1 + 5 files changed, 96 insertions(+) create mode 100644 Assets/Tests/Editor/RuntimeAssemblyBuildFilterTests.cs create mode 100644 Assets/Tests/Editor/RuntimeAssemblyBuildFilterTests.cs.meta create mode 100644 Packages/src/Editor/Build/RuntimeAssemblyBuildFilter.cs create mode 100644 Packages/src/Editor/Build/RuntimeAssemblyBuildFilter.cs.meta diff --git a/Assets/Tests/Editor/RuntimeAssemblyBuildFilterTests.cs b/Assets/Tests/Editor/RuntimeAssemblyBuildFilterTests.cs new file mode 100644 index 0000000000..0b6d9bd0bb --- /dev/null +++ b/Assets/Tests/Editor/RuntimeAssemblyBuildFilterTests.cs @@ -0,0 +1,31 @@ +using NUnit.Framework; +using UnityEditor; + +namespace io.github.hatayama.uLoopMCP +{ + /// + /// Verifies that the Player build callback removes only the prefab-only runtime assembly. + /// + public sealed class RuntimeAssemblyBuildFilterTests + { + [Test] + public void OnFilterAssemblies_WhenRuntimeAssemblyIsPresent_RemovesOnlyRuntimeAssembly() + { + RuntimeAssemblyBuildFilter filter = new RuntimeAssemblyBuildFilter(); + string[] assemblies = + { + "/build/Managed/Assembly-CSharp.dll", + "/build/Managed/uLoopMCP.Runtime.dll", + "/build/Managed/UnityEngine.CoreModule.dll" + }; + + string[] filteredAssemblies = filter.OnFilterAssemblies(BuildOptions.None, assemblies); + + Assert.That(filteredAssemblies, Is.EqualTo(new[] + { + "/build/Managed/Assembly-CSharp.dll", + "/build/Managed/UnityEngine.CoreModule.dll" + })); + } + } +} diff --git a/Assets/Tests/Editor/RuntimeAssemblyBuildFilterTests.cs.meta b/Assets/Tests/Editor/RuntimeAssemblyBuildFilterTests.cs.meta new file mode 100644 index 0000000000..0b6f9c367a --- /dev/null +++ b/Assets/Tests/Editor/RuntimeAssemblyBuildFilterTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 25812280c766b4d0f8aa505c53df1439 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Build/RuntimeAssemblyBuildFilter.cs b/Packages/src/Editor/Build/RuntimeAssemblyBuildFilter.cs new file mode 100644 index 0000000000..a9ec3395d7 --- /dev/null +++ b/Packages/src/Editor/Build/RuntimeAssemblyBuildFilter.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.IO; +using UnityEditor; +using UnityEditor.Build; +using UnityEngine; + +namespace io.github.hatayama.uLoopMCP +{ + // Removes uLoop's prefab-only runtime assembly from Player builds after Editor compilation keeps prefab scripts resolvable. + internal sealed class RuntimeAssemblyBuildFilter : IFilterBuildAssemblies + { + public int callbackOrder => 0; + + public string[] OnFilterAssemblies(BuildOptions buildOptions, string[] assemblies) + { + Debug.Assert(assemblies != null, "assemblies must be provided by Unity build pipeline"); + + List filteredAssemblies = new List(assemblies.Length); + for (int i = 0; i < assemblies.Length; i++) + { + string assemblyPath = assemblies[i]; + if (IsRuntimeAssembly(assemblyPath)) + { + continue; + } + + filteredAssemblies.Add(assemblyPath); + } + + return filteredAssemblies.ToArray(); + } + + private static bool IsRuntimeAssembly(string assemblyPath) + { + Debug.Assert(!string.IsNullOrEmpty(assemblyPath), "assemblyPath must be provided by Unity build pipeline"); + + string fileName = Path.GetFileName(assemblyPath); + return string.Equals(fileName, McpConstants.RUNTIME_ASSEMBLY_FILE_NAME, StringComparison.Ordinal); + } + } +} diff --git a/Packages/src/Editor/Build/RuntimeAssemblyBuildFilter.cs.meta b/Packages/src/Editor/Build/RuntimeAssemblyBuildFilter.cs.meta new file mode 100644 index 0000000000..57c12e404a --- /dev/null +++ b/Packages/src/Editor/Build/RuntimeAssemblyBuildFilter.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 042727ba0224740ea88a475b774caf52 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Editor/Shared/Config/McpConstants.cs b/Packages/src/Editor/Shared/Config/McpConstants.cs index 9c17c6660b..b3fd3173cc 100644 --- a/Packages/src/Editor/Shared/Config/McpConstants.cs +++ b/Packages/src/Editor/Shared/Config/McpConstants.cs @@ -129,6 +129,7 @@ public static UnityEditor.PackageManager.PackageInfo PackageInfo public const string ULOOPMCP_DIR = "uLoopMCP"; public const string COMPILE_RESULTS_DIR = "compile-results"; public const string JSON_FILE_EXTENSION = ".json"; + public const string RUNTIME_ASSEMBLY_FILE_NAME = "uLoopMCP.Runtime.dll"; // .uloop directory public const string ULOOP_DIR = ".uloop"; From 16699fcae3fc7b2c281a5bb31bc0fee26021c114 Mon Sep 17 00:00:00 2001 From: hatayama Date: Fri, 5 Jun 2026 01:54:25 +0900 Subject: [PATCH 5/6] Fix replay completion timing Delay non-loop replay completion until InputSystemUIInputModule can consume the terminal release, keep loop cadence gapless, and preserve pointer dispatch mode across press/release. Keep the runtime assembly in player test builds so PlayMode test players can resolve package test references. --- .../Editor/RuntimeAssemblyBuildFilterTests.cs | 16 ++ .../PlayMode/InputReplayerCompletionTests.cs | 269 ++++++++++++++++++ .../InputReplayerCompletionTests.cs.meta | 11 + .../PlayMode/SimulateMouseDemoE2ETests.cs | 70 ++++- .../Api/McpTools/ReplayInput/InputReplayer.cs | 62 +++- .../Build/RuntimeAssemblyBuildFilter.cs | 6 + 6 files changed, 429 insertions(+), 5 deletions(-) create mode 100644 Assets/Tests/PlayMode/InputReplayerCompletionTests.cs create mode 100644 Assets/Tests/PlayMode/InputReplayerCompletionTests.cs.meta diff --git a/Assets/Tests/Editor/RuntimeAssemblyBuildFilterTests.cs b/Assets/Tests/Editor/RuntimeAssemblyBuildFilterTests.cs index 0b6d9bd0bb..f9f9520e8c 100644 --- a/Assets/Tests/Editor/RuntimeAssemblyBuildFilterTests.cs +++ b/Assets/Tests/Editor/RuntimeAssemblyBuildFilterTests.cs @@ -27,5 +27,21 @@ public void OnFilterAssemblies_WhenRuntimeAssemblyIsPresent_RemovesOnlyRuntimeAs "/build/Managed/UnityEngine.CoreModule.dll" })); } + + [Test] + public void OnFilterAssemblies_WhenTestAssembliesAreIncluded_KeepsRuntimeAssembly() + { + RuntimeAssemblyBuildFilter filter = new RuntimeAssemblyBuildFilter(); + string[] assemblies = + { + "/build/Managed/Assembly-CSharp.dll", + "/build/Managed/uLoopMCP.Runtime.dll", + "/build/Managed/uLoopMCP.Tests.PlayMode.dll" + }; + + string[] filteredAssemblies = filter.OnFilterAssemblies(BuildOptions.IncludeTestAssemblies, assemblies); + + Assert.That(filteredAssemblies, Is.EqualTo(assemblies)); + } } } diff --git a/Assets/Tests/PlayMode/InputReplayerCompletionTests.cs b/Assets/Tests/PlayMode/InputReplayerCompletionTests.cs new file mode 100644 index 0000000000..d0ea5bd582 --- /dev/null +++ b/Assets/Tests/PlayMode/InputReplayerCompletionTests.cs @@ -0,0 +1,269 @@ +#if ULOOPMCP_HAS_INPUT_SYSTEM +#nullable enable +using System.Collections; +using System.Collections.Generic; +using io.github.hatayama.uLoopMCP; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.EventSystems; +using UnityEngine.InputSystem; +using UnityEngine.InputSystem.UI; +using UnityEngine.TestTools; +using UnityEngine.UI; + +namespace Tests.PlayMode +{ + /// + /// Verifies that replay completion waits for UI modules to consume the final injected input frame. + /// + public sealed class InputReplayerCompletionTests + { + private const float ReplayTimeoutSeconds = 5f; + private const float ButtonWidth = 240f; + private const float ButtonHeight = 120f; + + private GameObject _canvasGo = null!; + private GameObject _eventSystemGo = null!; + private GameObject _buttonGo = null!; + private Mouse? _createdMouse; + private bool _replayCompleted; + private int _clickCount; + private int _clickCountAtCompletion; + private bool _observedLoopFrameBeyondTotal; + private bool _observedLoopReset; + private int _loopObservationTicks; + private PointerLifecycleTracker _pointerLifecycleTracker = null!; + + [UnitySetUp] + public IEnumerator SetUp() + { + _replayCompleted = false; + _clickCount = 0; + _clickCountAtCompletion = 0; + _observedLoopFrameBeyondTotal = false; + _observedLoopReset = false; + _loopObservationTicks = 0; + + EnsureMouseDeviceExists(); + CreateScene(); + + yield return null; + } + + [UnityTearDown] + public IEnumerator TearDown() + { + InputReplayer.ReplayCompleted -= OnReplayCompleted; + InputSystem.onAfterUpdate -= ObserveLoopReplayCadence; + + if (InputReplayer.IsReplaying) + { + InputReplayer.StopReplay(); + } + + if (_createdMouse != null) + { + InputSystem.RemoveDevice(_createdMouse); + _createdMouse = null; + } + + Object.Destroy(_buttonGo); + Object.Destroy(_eventSystemGo); + Object.Destroy(_canvasGo); + + yield return null; + } + + [UnityTest] + public IEnumerator ReplayCompleted_WhenInputSystemUiConsumesTerminalRelease_FiresAfterClick() + { + Vector2 screenPosition = RectTransformUtility.WorldToScreenPoint(null, _buttonGo.transform.position); + InputRecordingData recording = CreateTerminalClickRecording(screenPosition); + + InputReplayer.ReplayCompleted += OnReplayCompleted; + InputReplayer.StartReplay(recording, loop: false, showOverlay: false); + + float timeoutAt = Time.realtimeSinceStartup + ReplayTimeoutSeconds; + yield return new WaitUntil(() => + _replayCompleted || Time.realtimeSinceStartup >= timeoutAt); + + Assert.IsTrue(_replayCompleted, $"Replay did not complete within {ReplayTimeoutSeconds}s"); + Assert.AreEqual(1, _clickCountAtCompletion, + "ReplayCompleted must fire after InputSystemUIInputModule processes the terminal release"); + Assert.AreEqual(1, _clickCount, "Button click must be processed exactly once"); + } + + [UnityTest] + public IEnumerator LoopReplay_WhenInputSystemUiConsumesTerminalRelease_DoesNotInsertEmptyFrame() + { + Vector2 screenPosition = RectTransformUtility.WorldToScreenPoint(null, _buttonGo.transform.position); + InputRecordingData recording = CreateTerminalClickRecording(screenPosition); + + InputReplayer.StartReplay(recording, loop: true, showOverlay: false); + InputSystem.onAfterUpdate += ObserveLoopReplayCadence; + + float timeoutAt = Time.realtimeSinceStartup + ReplayTimeoutSeconds; + yield return new WaitUntil(() => + _observedLoopReset || Time.realtimeSinceStartup >= timeoutAt); + + Assert.IsTrue(_observedLoopReset, $"Looped replay did not reset within {ReplayTimeoutSeconds}s"); + Assert.IsFalse( + _observedLoopFrameBeyondTotal, + "Looped replay must reset on the terminal release without exposing an unrecorded empty frame"); + } + + [UnityTest] + public IEnumerator Replay_WhenInputSystemUiModuleIsSelectedAfterManualPress_FiresMatchingPointerUp() + { + RecreateEventSystemWithoutSelectedInputModule(); + EventSystem? eventSystem = EventSystem.current; + Assert.IsNotNull(eventSystem, "EventSystem.current must be assigned"); + Assert.IsNull(eventSystem!.currentInputModule, "Fresh EventSystem should not select an input module until update"); + + Vector2 screenPosition = RectTransformUtility.WorldToScreenPoint(null, _buttonGo.transform.position); + InputRecordingData recording = CreateTerminalClickRecording(screenPosition); + + InputReplayer.ReplayCompleted += OnReplayCompleted; + InputReplayer.StartReplay(recording, loop: false, showOverlay: false); + + float timeoutAt = Time.realtimeSinceStartup + ReplayTimeoutSeconds; + yield return new WaitUntil(() => + _replayCompleted || Time.realtimeSinceStartup >= timeoutAt); + + Assert.IsTrue(_replayCompleted, $"Replay did not complete within {ReplayTimeoutSeconds}s"); + Assert.Greater(_pointerLifecycleTracker.PointerDownCount, 0, + "The replay should dispatch at least one pointer down to the target"); + Assert.AreEqual(_pointerLifecycleTracker.PointerDownCount, _pointerLifecycleTracker.PointerUpCount, + "Every pointer down must receive a matching pointer up even if the input module is selected later"); + } + + private void OnReplayCompleted() + { + _clickCountAtCompletion = _clickCount; + _replayCompleted = true; + } + + private void ObserveLoopReplayCadence() + { + if (!InputReplayer.IsReplaying) + { + return; + } + + if (InputReplayer.CurrentFrame > InputReplayer.TotalFrames) + { + _observedLoopFrameBeyondTotal = true; + } + + if (_loopObservationTicks > 0 && InputReplayer.CurrentFrame == 0) + { + _observedLoopReset = true; + } + + _loopObservationTicks++; + } + + private void OnButtonClicked() + { + _clickCount++; + } + + private void EnsureMouseDeviceExists() + { + if (Mouse.current != null) + { + return; + } + + _createdMouse = InputSystem.AddDevice(); + } + + private void CreateScene() + { + _canvasGo = new GameObject("InputReplayerCompletionCanvas"); + Canvas canvas = _canvasGo.AddComponent(); + canvas.renderMode = RenderMode.ScreenSpaceOverlay; + _canvasGo.AddComponent(); + + _eventSystemGo = new GameObject("InputReplayerCompletionEventSystem"); + _eventSystemGo.AddComponent(); + _eventSystemGo.AddComponent(); + + _buttonGo = new GameObject("TerminalReleaseButton"); + _buttonGo.transform.SetParent(_canvasGo.transform, false); + RectTransform buttonRect = _buttonGo.AddComponent(); + buttonRect.sizeDelta = new Vector2(ButtonWidth, ButtonHeight); + Image buttonImage = _buttonGo.AddComponent(); + Button button = _buttonGo.AddComponent