diff --git a/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs
index 4526b8987e..1056bb1290 100644
--- a/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs
+++ b/Assets/Tests/Editor/StaticFacadeStateGuardTests.cs
@@ -62,6 +62,7 @@ public sealed class StaticFacadeStateGuardTests
"Packages/src/Editor/Infrastructure/SkillSetup/ToolSkillSynchronizer.cs",
"Packages/src/Editor/Presentation/Setup/SetupWizardWindow.cs",
"Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs",
+ "Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputMainThreadCleanup.cs",
"Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardUseCase.cs"
};
diff --git a/Assets/Tests/PlayMode/MouseInputSimulationResponseFactoryTests.cs b/Assets/Tests/PlayMode/MouseInputSimulationResponseFactoryTests.cs
new file mode 100644
index 0000000000..16f7102ec7
--- /dev/null
+++ b/Assets/Tests/PlayMode/MouseInputSimulationResponseFactoryTests.cs
@@ -0,0 +1,110 @@
+#if ULOOP_HAS_INPUT_SYSTEM
+#nullable enable
+using System;
+using NUnit.Framework;
+using UnityEngine;
+
+using io.github.hatayama.UnityCliLoop.FirstPartyTools;
+using io.github.hatayama.UnityCliLoop.Runtime;
+using io.github.hatayama.UnityCliLoop.ToolContracts;
+
+namespace io.github.hatayama.UnityCliLoop.Tests.PlayMode
+{
+ ///
+ /// Characterizes wire-visible mouse input response construction before factory extraction.
+ ///
+ [TestFixture]
+ public sealed class MouseInputSimulationResponseFactoryTests
+ {
+ private readonly DateTime _nowUtc = new(2026, 7, 9, 0, 0, 0, DateTimeKind.Utc);
+
+ [SetUp]
+ public void SetUp()
+ {
+ UloopPausePointRegistry.ConfigureForTests(new FakePauseController(), () => _nowUtc);
+ }
+
+ [TearDown]
+ public void TearDown()
+ {
+ UloopPausePointRegistry.ResetForTests();
+ }
+
+ ///
+ /// Verifies interrupted button responses project every Pause Point hit and preserve button coordinates.
+ ///
+ [Test]
+ public void InterruptedButtonResult_WithMultiplePausePointHits_MapsPauseEvidenceAndButtonPosition()
+ {
+ UloopPausePointRegistry.Enable("first-hit", 30);
+ UloopPausePointRegistry.Enable("latest-hit", 30);
+ UloopPausePoint.Pause("first-hit");
+ UloopPausePoint.Pause("latest-hit");
+ Vector2 inputPosition = new(10.25f, 20.75f);
+
+ SimulateMouseInputResponse response = MouseInputSimulationResponseFactory.InterruptedButtonResult(
+ UnityCliLoopMouseInputAction.Click,
+ "Left",
+ inputPosition);
+
+ Assert.That(response.Success, Is.True);
+ Assert.That(
+ response.Message,
+ Is.EqualTo(
+ "Mouse input stopped because Unity paused during Pause Point inspection. Unity CLI Loop released its held input bookkeeping."));
+ Assert.That(response.Action, Is.EqualTo(UnityCliLoopMouseInputAction.Click.ToString()));
+ Assert.That(response.Button, Is.EqualTo("Left"));
+ Assert.That(response.PositionX, Is.EqualTo(inputPosition.x));
+ Assert.That(response.PositionY, Is.EqualTo(inputPosition.y));
+ Assert.That(response.InterruptedByPausePoint, Is.True);
+ Assert.That(response.PausePointId, Is.EqualTo("latest-hit"));
+ Assert.That(response.PausePointHitCount, Is.EqualTo(1));
+ Assert.That(response.PausePointHits, Has.Count.EqualTo(2));
+ Assert.That(response.PausePointHits![0].Id, Is.EqualTo("first-hit"));
+ Assert.That(response.PausePointHits[1].Id, Is.EqualTo("latest-hit"));
+ }
+
+ ///
+ /// Verifies timed-out button responses preserve the action, button, coordinates, and timeout message.
+ ///
+ [Test]
+ public void TimedOutButtonResult_WithButtonAndPosition_MapsFailureResponse()
+ {
+ Vector2 inputPosition = new(30.5f, 40.25f);
+
+ SimulateMouseInputResponse response = MouseInputSimulationResponseFactory.TimedOutButtonResult(
+ UnityCliLoopMouseInputAction.LongPress,
+ "Right",
+ inputPosition);
+
+ Assert.That(response.Success, Is.False);
+ Assert.That(
+ response.Message,
+ Is.EqualTo(
+ "Mouse input timed out while waiting for Unity Editor update. Cleanup is queued for the next Editor tick."));
+ Assert.That(response.Action, Is.EqualTo(UnityCliLoopMouseInputAction.LongPress.ToString()));
+ Assert.That(response.Button, Is.EqualTo("Right"));
+ Assert.That(response.PositionX, Is.EqualTo(inputPosition.x));
+ Assert.That(response.PositionY, Is.EqualTo(inputPosition.y));
+ Assert.That(response.InterruptedByPausePoint, Is.False);
+ Assert.That(response.PausePointId, Is.Null);
+ Assert.That(response.PausePointHitCount, Is.Null);
+ Assert.That(response.PausePointHits, Is.Null);
+ }
+
+ ///
+ /// Test double that records Pause Point state without pausing the Unity Editor.
+ ///
+ private sealed class FakePauseController : IUloopPausePointPauseController
+ {
+ public bool IsPlaying => true;
+ public bool IsPaused { get; private set; }
+
+ public void Pause()
+ {
+ IsPaused = true;
+ }
+ }
+ }
+}
+#endif
diff --git a/Assets/Tests/PlayMode/MouseInputSimulationResponseFactoryTests.cs.meta b/Assets/Tests/PlayMode/MouseInputSimulationResponseFactoryTests.cs.meta
new file mode 100644
index 0000000000..63aa900b74
--- /dev/null
+++ b/Assets/Tests/PlayMode/MouseInputSimulationResponseFactoryTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 7ab44557d0cc64ea1877f70eb1e036c1
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputMainThreadCleanup.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputMainThreadCleanup.cs
new file mode 100644
index 0000000000..7280380e2f
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputMainThreadCleanup.cs
@@ -0,0 +1,141 @@
+#if ULOOP_HAS_INPUT_SYSTEM
+#nullable enable
+using System.Threading;
+using System.Threading.Tasks;
+using UnityEditor;
+using UnityEngine;
+using UnityEngine.InputSystem;
+
+using RuntimeMouseButton = io.github.hatayama.UnityCliLoop.Runtime.MouseButton;
+using io.github.hatayama.UnityCliLoop.Runtime;
+using io.github.hatayama.UnityCliLoop.ToolContracts;
+
+namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
+{
+ ///
+ /// Restores mouse device and overlay state on the Unity main thread after simulation ends.
+ ///
+ internal static class MouseInputMainThreadCleanup
+ {
+ internal static async Task ReleaseButtonIfPossible(
+ Mouse mouse,
+ RuntimeMouseButton button,
+ CancellationToken ct)
+ {
+ await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
+ if (!CanInjectMouseState(mouse))
+ {
+ return InputSimulationWaitOutcome.Completed;
+ }
+
+ if (EditorApplication.isPaused)
+ {
+ ReleaseButtonImmediately(mouse, button);
+ return InputSimulationWaitOutcome.Completed;
+ }
+
+ InputSimulationWaitOutcome releaseOutcome = await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(
+ () => MouseInputState.SetButtonState(mouse, button, false),
+ ct).ConfigureAwait(false);
+ if (releaseOutcome == InputSimulationWaitOutcome.TimedOut)
+ {
+ ScheduleReleaseButtonImmediately(mouse, button);
+ }
+
+ return releaseOutcome;
+ }
+
+ private static void ScheduleReleaseButtonImmediately(Mouse mouse, RuntimeMouseButton button)
+ {
+ ReleaseButtonImmediatelyOnMainThreadAsync(mouse, button, CancellationToken.None).Forget();
+ }
+
+ private static async Task ReleaseButtonImmediatelyOnMainThreadAsync(
+ Mouse mouse,
+ RuntimeMouseButton button,
+ CancellationToken ct)
+ {
+ await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
+ ReleaseButtonImmediately(mouse, button);
+ }
+
+ private static void ReleaseButtonImmediately(Mouse mouse, RuntimeMouseButton button)
+ {
+ Debug.Assert(CanInjectMouseState(mouse), "mouse button can only be released while PlayMode has a mouse");
+ if (!CanInjectMouseState(mouse))
+ {
+ return;
+ }
+
+ MouseInputState.SetButtonState(mouse, button, false);
+ InputSystemUpdateHelper.RunExplicitUpdate(InputUpdateTypeResolver.Resolve());
+ }
+
+ internal static void ResetDeltaIfPossible(Mouse mouse)
+ {
+ if (!CanInjectMouseState(mouse))
+ {
+ return;
+ }
+
+ MouseInputState.InjectDelta(mouse, Vector2.zero);
+ if (EditorApplication.isPaused)
+ {
+ InputSystemUpdateHelper.RunExplicitUpdate(InputUpdateTypeResolver.Resolve());
+ }
+ }
+
+ internal static void ScheduleTimedOutButtonCleanup(
+ Mouse mouse,
+ RuntimeMouseButton button,
+ bool pressWasApplied)
+ {
+ CleanupTimedOutButtonAsync(mouse, button, pressWasApplied, CancellationToken.None).Forget();
+ }
+
+ private static async Task CleanupTimedOutButtonAsync(
+ Mouse mouse,
+ RuntimeMouseButton button,
+ bool pressWasApplied,
+ CancellationToken ct)
+ {
+ await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
+ if (pressWasApplied)
+ {
+ await ReleaseButtonIfPossible(mouse, button, ct).ConfigureAwait(false);
+ }
+
+ MouseInputState.SetButtonUp(button);
+ SimulateMouseInputOverlayState.SetButtonHeld(button, false);
+ }
+
+ internal static void ScheduleTimedOutMouseOverlayCleanup()
+ {
+ CleanupTimedOutMouseOverlayAsync(CancellationToken.None).Forget();
+ }
+
+ private static async Task CleanupTimedOutMouseOverlayAsync(CancellationToken ct)
+ {
+ await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
+ SimulateMouseInputOverlayState.Clear();
+ }
+
+ internal static void ScheduleTimedOutDeltaCleanup(Mouse mouse)
+ {
+ CleanupTimedOutDeltaAsync(mouse, CancellationToken.None).Forget();
+ }
+
+ private static async Task CleanupTimedOutDeltaAsync(Mouse mouse, CancellationToken ct)
+ {
+ await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
+ ResetDeltaIfPossible(mouse);
+ SimulateMouseInputOverlayState.Clear();
+ }
+
+ private static bool CanInjectMouseState(Mouse mouse)
+ {
+ return EditorApplication.isPlaying && mouse != null;
+ }
+ }
+}
+#endif
diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputMainThreadCleanup.cs.meta b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputMainThreadCleanup.cs.meta
new file mode 100644
index 0000000000..88c30e05b4
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputMainThreadCleanup.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: eea8ef017defc4c7dbc892f72ddc6d8d
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputSimulationResponseFactory.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputSimulationResponseFactory.cs
new file mode 100644
index 0000000000..19b514aa3b
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputSimulationResponseFactory.cs
@@ -0,0 +1,116 @@
+#if ULOOP_HAS_INPUT_SYSTEM
+#nullable enable
+using System.Collections.Generic;
+using UnityEngine;
+
+using io.github.hatayama.UnityCliLoop.Runtime;
+using io.github.hatayama.UnityCliLoop.ToolContracts;
+
+namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
+{
+ ///
+ /// Creates wire-visible responses for mouse input simulation outcomes.
+ ///
+ internal static class MouseInputSimulationResponseFactory
+ {
+ internal static SimulateMouseInputResponse InterruptedButtonResult(
+ UnityCliLoopMouseInputAction action,
+ string buttonName,
+ Vector2 inputPos)
+ {
+ SimulateMouseInputResponse result = InterruptedActionResult(action);
+ result.Button = buttonName;
+ result.PositionX = inputPos.x;
+ result.PositionY = inputPos.y;
+ return result;
+ }
+
+ internal static SimulateMouseInputResponse InterruptedActionResult(
+ UnityCliLoopMouseInputAction action)
+ {
+ SimulateMouseInputResponse result = new()
+ {
+ Success = true,
+ Message = "Mouse input stopped because Unity paused during Pause Point inspection. Unity CLI Loop released its held input bookkeeping.",
+ Action = action.ToString(),
+ InterruptedByPausePoint = true
+ };
+ AttachPausePointHit(result);
+ return result;
+ }
+
+ internal static SimulateMouseInputResponse TimedOutButtonResult(
+ UnityCliLoopMouseInputAction action,
+ string buttonName,
+ Vector2 inputPos)
+ {
+ SimulateMouseInputResponse result = TimedOutActionResult(action);
+ result.Button = buttonName;
+ result.PositionX = inputPos.x;
+ result.PositionY = inputPos.y;
+ return result;
+ }
+
+ internal static SimulateMouseInputResponse TimedOutActionResult(
+ UnityCliLoopMouseInputAction action)
+ {
+ return new SimulateMouseInputResponse
+ {
+ Success = false,
+ Message = "Mouse input timed out while waiting for Unity Editor update. Cleanup is queued for the next Editor tick.",
+ Action = action.ToString()
+ };
+ }
+
+ private static void AttachPausePointHit(SimulateMouseInputResponse result)
+ {
+ if (result == null)
+ {
+ Debug.Assert(false, "result must not be null");
+ return;
+ }
+
+ UloopPausePointSnapshot? snapshot = UloopPausePointRegistry.GetLatestHitSnapshot();
+ if (snapshot == null)
+ {
+ return;
+ }
+
+ if (!snapshot.IsHit)
+ {
+ return;
+ }
+
+ string? snapshotId = snapshot.Id;
+ if (string.IsNullOrEmpty(snapshotId))
+ {
+ return;
+ }
+
+ result.PausePointId = snapshotId;
+ result.PausePointHitCount = snapshot.HitCount;
+ result.PausePointHits = CollectPausePointHits();
+ }
+
+ // One input can hit several markers in the same frame; the representative
+ // PausePointId alone forced agents into extra status calls to find the others.
+ private static List CollectPausePointHits()
+ {
+ List hits = new();
+ foreach (UloopPausePointSnapshot snapshot in UloopPausePointRegistry.GetHitSnapshots())
+ {
+ if (!snapshot.IsHit || string.IsNullOrEmpty(snapshot.Id))
+ {
+ continue;
+ }
+ hits.Add(new UnityCliLoopPausePointHit
+ {
+ Id = snapshot.Id,
+ HitCount = snapshot.HitCount
+ });
+ }
+ return hits;
+ }
+ }
+}
+#endif
diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputSimulationResponseFactory.cs.meta b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputSimulationResponseFactory.cs.meta
new file mode 100644
index 0000000000..f3ec6c849f
--- /dev/null
+++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputSimulationResponseFactory.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 455a57fc2aef44c599c2e546689be3d7
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs
index f19ea8a3dc..95c34b207f 100644
--- a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs
+++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputUseCase.cs
@@ -1,9 +1,7 @@
#nullable enable
using System;
-using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
-using UnityEditor;
using UnityEngine;
#if ULOOP_HAS_INPUT_SYSTEM
using UnityEngine.InputSystem;
@@ -181,7 +179,10 @@ private async Task ExecuteClick(
() => MouseInputState.SetPositionState(mouse, screenPos), ct).ConfigureAwait(false);
if (positionOutcome == InputSimulationWaitOutcome.TimedOut)
{
- return TimedOutButtonResult(UnityCliLoopMouseInputAction.Click, buttonName, inputPos);
+ return MouseInputSimulationResponseFactory.TimedOutButtonResult(
+ UnityCliLoopMouseInputAction.Click,
+ buttonName,
+ inputPos);
}
// Press button
@@ -207,16 +208,19 @@ private async Task ExecuteClick(
{
if (waitOutcome == InputSimulationWaitOutcome.TimedOut)
{
- ScheduleTimedOutButtonCleanup(mouse, button, pressWasApplied);
+ MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, pressWasApplied);
}
else if (pressWasApplied)
{
InputSimulationWaitOutcome releaseOutcome =
- await ReleaseButtonIfPossible(mouse, button, CancellationToken.None).ConfigureAwait(false);
+ await MouseInputMainThreadCleanup.ReleaseButtonIfPossible(
+ mouse,
+ button,
+ CancellationToken.None).ConfigureAwait(false);
if (releaseOutcome == InputSimulationWaitOutcome.TimedOut)
{
waitOutcome = InputSimulationWaitOutcome.TimedOut;
- ScheduleTimedOutButtonCleanup(mouse, button, false);
+ MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, false);
}
else
{
@@ -245,7 +249,7 @@ private async Task ExecuteClick(
if (waitOutcome == InputSimulationWaitOutcome.Paused)
{
- return InterruptedButtonResult(
+ return MouseInputSimulationResponseFactory.InterruptedButtonResult(
UnityCliLoopMouseInputAction.Click,
buttonName,
inputPos);
@@ -253,7 +257,10 @@ private async Task ExecuteClick(
if (waitOutcome == InputSimulationWaitOutcome.TimedOut)
{
- return TimedOutButtonResult(UnityCliLoopMouseInputAction.Click, buttonName, inputPos);
+ return MouseInputSimulationResponseFactory.TimedOutButtonResult(
+ UnityCliLoopMouseInputAction.Click,
+ buttonName,
+ inputPos);
}
string durationText = request.Duration > 0f ? $" for {InputSimulationDurationFormatter.FormatSeconds(request.Duration)}s" : "";
@@ -291,7 +298,10 @@ private async Task ExecuteLongPress(
() => MouseInputState.SetPositionState(mouse, screenPos), ct).ConfigureAwait(false);
if (positionOutcome == InputSimulationWaitOutcome.TimedOut)
{
- return TimedOutButtonResult(UnityCliLoopMouseInputAction.LongPress, buttonName, inputPos);
+ return MouseInputSimulationResponseFactory.TimedOutButtonResult(
+ UnityCliLoopMouseInputAction.LongPress,
+ buttonName,
+ inputPos);
}
// Press button
@@ -320,16 +330,19 @@ private async Task ExecuteLongPress(
{
if (waitOutcome == InputSimulationWaitOutcome.TimedOut)
{
- ScheduleTimedOutButtonCleanup(mouse, button, pressWasApplied);
+ MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, pressWasApplied);
}
else if (pressWasApplied)
{
InputSimulationWaitOutcome releaseOutcome =
- await ReleaseButtonIfPossible(mouse, button, CancellationToken.None).ConfigureAwait(false);
+ await MouseInputMainThreadCleanup.ReleaseButtonIfPossible(
+ mouse,
+ button,
+ CancellationToken.None).ConfigureAwait(false);
if (releaseOutcome == InputSimulationWaitOutcome.TimedOut)
{
waitOutcome = InputSimulationWaitOutcome.TimedOut;
- ScheduleTimedOutButtonCleanup(mouse, button, false);
+ MouseInputMainThreadCleanup.ScheduleTimedOutButtonCleanup(mouse, button, false);
}
else
{
@@ -358,7 +371,7 @@ private async Task ExecuteLongPress(
if (waitOutcome == InputSimulationWaitOutcome.Paused)
{
- return InterruptedButtonResult(
+ return MouseInputSimulationResponseFactory.InterruptedButtonResult(
UnityCliLoopMouseInputAction.LongPress,
buttonName,
inputPos);
@@ -366,7 +379,10 @@ private async Task ExecuteLongPress(
if (waitOutcome == InputSimulationWaitOutcome.TimedOut)
{
- return TimedOutButtonResult(UnityCliLoopMouseInputAction.LongPress, buttonName, inputPos);
+ return MouseInputSimulationResponseFactory.TimedOutButtonResult(
+ UnityCliLoopMouseInputAction.LongPress,
+ buttonName,
+ inputPos);
}
return new SimulateMouseInputResponse
@@ -390,8 +406,9 @@ private async Task ExecuteMoveDelta(
() => MouseInputState.SetDeltaState(mouse, delta), ct).ConfigureAwait(false);
if (applyOutcome == InputSimulationWaitOutcome.TimedOut)
{
- ScheduleTimedOutMouseOverlayCleanup();
- return TimedOutActionResult(UnityCliLoopMouseInputAction.MoveDelta);
+ MouseInputMainThreadCleanup.ScheduleTimedOutMouseOverlayCleanup();
+ return MouseInputSimulationResponseFactory.TimedOutActionResult(
+ UnityCliLoopMouseInputAction.MoveDelta);
}
InputSimulationWaitOutcome waitOutcome = await InputSystemUpdateHelper.WaitForObservationFrames(ct)
@@ -400,13 +417,15 @@ private async Task ExecuteMoveDelta(
{
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
SimulateMouseInputOverlayState.Clear();
- return InterruptedActionResult(UnityCliLoopMouseInputAction.MoveDelta);
+ return MouseInputSimulationResponseFactory.InterruptedActionResult(
+ UnityCliLoopMouseInputAction.MoveDelta);
}
if (waitOutcome == InputSimulationWaitOutcome.TimedOut)
{
- ScheduleTimedOutMouseOverlayCleanup();
- return TimedOutActionResult(UnityCliLoopMouseInputAction.MoveDelta);
+ MouseInputMainThreadCleanup.ScheduleTimedOutMouseOverlayCleanup();
+ return MouseInputSimulationResponseFactory.TimedOutActionResult(
+ UnityCliLoopMouseInputAction.MoveDelta);
}
return new SimulateMouseInputResponse
@@ -429,8 +448,9 @@ private async Task ExecuteScroll(
() => MouseInputState.SetScrollState(mouse, scroll), ct).ConfigureAwait(false);
if (applyOutcome == InputSimulationWaitOutcome.TimedOut)
{
- ScheduleTimedOutMouseOverlayCleanup();
- return TimedOutActionResult(UnityCliLoopMouseInputAction.Scroll);
+ MouseInputMainThreadCleanup.ScheduleTimedOutMouseOverlayCleanup();
+ return MouseInputSimulationResponseFactory.TimedOutActionResult(
+ UnityCliLoopMouseInputAction.Scroll);
}
InputSimulationWaitOutcome waitOutcome = await InputSystemUpdateHelper.WaitForObservationFrames(ct)
@@ -439,13 +459,15 @@ private async Task ExecuteScroll(
{
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
SimulateMouseInputOverlayState.Clear();
- return InterruptedActionResult(UnityCliLoopMouseInputAction.Scroll);
+ return MouseInputSimulationResponseFactory.InterruptedActionResult(
+ UnityCliLoopMouseInputAction.Scroll);
}
if (waitOutcome == InputSimulationWaitOutcome.TimedOut)
{
- ScheduleTimedOutMouseOverlayCleanup();
- return TimedOutActionResult(UnityCliLoopMouseInputAction.Scroll);
+ MouseInputMainThreadCleanup.ScheduleTimedOutMouseOverlayCleanup();
+ return MouseInputSimulationResponseFactory.TimedOutActionResult(
+ UnityCliLoopMouseInputAction.Scroll);
}
return new SimulateMouseInputResponse
@@ -492,8 +514,9 @@ private async Task ExecuteSmoothDelta(
() => MouseInputState.InjectDelta(mouse, frameDelta), ct).ConfigureAwait(false);
if (applyOutcome == InputSimulationWaitOutcome.TimedOut)
{
- ScheduleTimedOutDeltaCleanup(mouse);
- return TimedOutActionResult(UnityCliLoopMouseInputAction.SmoothDelta);
+ MouseInputMainThreadCleanup.ScheduleTimedOutDeltaCleanup(mouse);
+ return MouseInputSimulationResponseFactory.TimedOutActionResult(
+ UnityCliLoopMouseInputAction.SmoothDelta);
}
previousT = t;
@@ -502,15 +525,17 @@ private async Task ExecuteSmoothDelta(
if (waitOutcome == InputSimulationWaitOutcome.Paused)
{
await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
- ResetDeltaIfPossible(mouse);
+ MouseInputMainThreadCleanup.ResetDeltaIfPossible(mouse);
SimulateMouseInputOverlayState.Clear();
- return InterruptedActionResult(UnityCliLoopMouseInputAction.SmoothDelta);
+ return MouseInputSimulationResponseFactory.InterruptedActionResult(
+ UnityCliLoopMouseInputAction.SmoothDelta);
}
if (waitOutcome == InputSimulationWaitOutcome.TimedOut)
{
- ScheduleTimedOutDeltaCleanup(mouse);
- return TimedOutActionResult(UnityCliLoopMouseInputAction.SmoothDelta);
+ MouseInputMainThreadCleanup.ScheduleTimedOutDeltaCleanup(mouse);
+ return MouseInputSimulationResponseFactory.TimedOutActionResult(
+ UnityCliLoopMouseInputAction.SmoothDelta);
}
if (t >= 1f)
@@ -524,8 +549,9 @@ private async Task ExecuteSmoothDelta(
() => MouseInputState.InjectDelta(mouse, Vector2.zero), ct).ConfigureAwait(false);
if (resetOutcome == InputSimulationWaitOutcome.TimedOut)
{
- ScheduleTimedOutDeltaCleanup(mouse);
- return TimedOutActionResult(UnityCliLoopMouseInputAction.SmoothDelta);
+ MouseInputMainThreadCleanup.ScheduleTimedOutDeltaCleanup(mouse);
+ return MouseInputSimulationResponseFactory.TimedOutActionResult(
+ UnityCliLoopMouseInputAction.SmoothDelta);
}
return new SimulateMouseInputResponse
@@ -536,222 +562,6 @@ private async Task ExecuteSmoothDelta(
};
}
- private static SimulateMouseInputResponse InterruptedButtonResult(
- UnityCliLoopMouseInputAction action,
- string buttonName,
- Vector2 inputPos)
- {
- SimulateMouseInputResponse result = InterruptedActionResult(action);
- result.Button = buttonName;
- result.PositionX = inputPos.x;
- result.PositionY = inputPos.y;
- return result;
- }
-
- private static SimulateMouseInputResponse InterruptedActionResult(
- UnityCliLoopMouseInputAction action)
- {
- SimulateMouseInputResponse result = new()
- {
- Success = true,
- Message = "Mouse input stopped because Unity paused during Pause Point inspection. Unity CLI Loop released its held input bookkeeping.",
- Action = action.ToString(),
- InterruptedByPausePoint = true
- };
- AttachPausePointHit(result);
- return result;
- }
-
- private static SimulateMouseInputResponse TimedOutButtonResult(
- UnityCliLoopMouseInputAction action,
- string buttonName,
- Vector2 inputPos)
- {
- SimulateMouseInputResponse result = TimedOutActionResult(action);
- result.Button = buttonName;
- result.PositionX = inputPos.x;
- result.PositionY = inputPos.y;
- return result;
- }
-
- private static SimulateMouseInputResponse TimedOutActionResult(
- UnityCliLoopMouseInputAction action)
- {
- return new SimulateMouseInputResponse
- {
- Success = false,
- Message = "Mouse input timed out while waiting for Unity Editor update. Cleanup is queued for the next Editor tick.",
- Action = action.ToString()
- };
- }
-
- private static void AttachPausePointHit(SimulateMouseInputResponse result)
- {
- if (result == null)
- {
- Debug.Assert(false, "result must not be null");
- return;
- }
-
- UloopPausePointSnapshot? snapshot = UloopPausePointRegistry.GetLatestHitSnapshot();
- if (snapshot == null)
- {
- return;
- }
-
- if (!snapshot.IsHit)
- {
- return;
- }
-
- string? snapshotId = snapshot.Id;
- if (string.IsNullOrEmpty(snapshotId))
- {
- return;
- }
-
- result.PausePointId = snapshotId;
- result.PausePointHitCount = snapshot.HitCount;
- result.PausePointHits = CollectPausePointHits();
- }
-
- // One input can hit several markers in the same frame; the representative
- // PausePointId alone forced agents into extra status calls to find the others.
- private static List CollectPausePointHits()
- {
- List hits = new();
- foreach (UloopPausePointSnapshot snapshot in UloopPausePointRegistry.GetHitSnapshots())
- {
- if (!snapshot.IsHit || string.IsNullOrEmpty(snapshot.Id))
- {
- continue;
- }
- hits.Add(new UnityCliLoopPausePointHit
- {
- Id = snapshot.Id,
- HitCount = snapshot.HitCount
- });
- }
- return hits;
- }
-
- private static async Task ReleaseButtonIfPossible(
- Mouse mouse,
- RuntimeMouseButton button,
- CancellationToken ct)
- {
- await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
- if (!CanInjectMouseState(mouse))
- {
- return InputSimulationWaitOutcome.Completed;
- }
-
- if (EditorApplication.isPaused)
- {
- ReleaseButtonImmediately(mouse, button);
- return InputSimulationWaitOutcome.Completed;
- }
-
- InputSimulationWaitOutcome releaseOutcome = await InputSystemUpdateHelper.ApplyOnNextConfiguredUpdate(
- () => MouseInputState.SetButtonState(mouse, button, false),
- ct).ConfigureAwait(false);
- if (releaseOutcome == InputSimulationWaitOutcome.TimedOut)
- {
- ScheduleReleaseButtonImmediately(mouse, button);
- }
-
- return releaseOutcome;
- }
-
- private static void ScheduleReleaseButtonImmediately(Mouse mouse, RuntimeMouseButton button)
- {
- ReleaseButtonImmediatelyOnMainThreadAsync(mouse, button, CancellationToken.None).Forget();
- }
-
- private static async Task ReleaseButtonImmediatelyOnMainThreadAsync(
- Mouse mouse,
- RuntimeMouseButton button,
- CancellationToken ct)
- {
- await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
- ReleaseButtonImmediately(mouse, button);
- }
-
- private static void ReleaseButtonImmediately(Mouse mouse, RuntimeMouseButton button)
- {
- Debug.Assert(CanInjectMouseState(mouse), "mouse button can only be released while PlayMode has a mouse");
- if (!CanInjectMouseState(mouse))
- {
- return;
- }
-
- MouseInputState.SetButtonState(mouse, button, false);
- InputSystemUpdateHelper.RunExplicitUpdate(InputUpdateTypeResolver.Resolve());
- }
-
- private static void ResetDeltaIfPossible(Mouse mouse)
- {
- if (!CanInjectMouseState(mouse))
- {
- return;
- }
-
- MouseInputState.InjectDelta(mouse, Vector2.zero);
- if (EditorApplication.isPaused)
- {
- InputSystemUpdateHelper.RunExplicitUpdate(InputUpdateTypeResolver.Resolve());
- }
- }
-
- private static void ScheduleTimedOutButtonCleanup(Mouse mouse, RuntimeMouseButton button, bool pressWasApplied)
- {
- CleanupTimedOutButtonAsync(mouse, button, pressWasApplied, CancellationToken.None).Forget();
- }
-
- private static async Task CleanupTimedOutButtonAsync(
- Mouse mouse,
- RuntimeMouseButton button,
- bool pressWasApplied,
- CancellationToken ct)
- {
- await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
- if (pressWasApplied)
- {
- await ReleaseButtonIfPossible(mouse, button, ct).ConfigureAwait(false);
- }
-
- MouseInputState.SetButtonUp(button);
- SimulateMouseInputOverlayState.SetButtonHeld(button, false);
- }
-
- private static void ScheduleTimedOutMouseOverlayCleanup()
- {
- CleanupTimedOutMouseOverlayAsync(CancellationToken.None).Forget();
- }
-
- private static async Task CleanupTimedOutMouseOverlayAsync(CancellationToken ct)
- {
- await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
- SimulateMouseInputOverlayState.Clear();
- }
-
- private static void ScheduleTimedOutDeltaCleanup(Mouse mouse)
- {
- CleanupTimedOutDeltaAsync(mouse, CancellationToken.None).Forget();
- }
-
- private static async Task CleanupTimedOutDeltaAsync(Mouse mouse, CancellationToken ct)
- {
- await InputSystemUpdateHelper.SwitchToMainThreadIfNeeded(ct);
- ResetDeltaIfPossible(mouse);
- SimulateMouseInputOverlayState.Clear();
- }
-
- private static bool CanInjectMouseState(Mouse mouse)
- {
- return EditorApplication.isPlaying && mouse != null;
- }
-
private static RuntimeMouseButton ToRuntimeMouseButton(UnityCliLoopMouseButton button)
{
switch (button)