From fda5ff75ac47cadfd520325f59f4f2334d6d4ee1 Mon Sep 17 00:00:00 2001 From: Masamichi Hatayama Date: Wed, 2 Sep 2026 20:36:39 +0900 Subject: [PATCH] fix: simulate-mouse-input reports whether an interrupted press reached the game instead of "may have registered" (#2509) (cherry picked from commit aa4c2189863aa136fefb0749991f32d39fc3aab9) --- .../uloop-simulate-mouse-input/SKILL.md | 2 +- .../references/output-and-coordinates.md | 1 + .../uloop-simulate-mouse-input/SKILL.md | 2 +- .../references/output-and-coordinates.md | 1 + ...ouseInputSimulationResponseFactoryTests.cs | 68 +++++++++++++++++++ ...nputSimulationResponseFactoryTests.cs.meta | 11 +++ .../MouseInputSimulationResponseFactory.cs | 10 ++- .../SimulateMouseInputResponse.cs | 8 +++ .../SimulateMouseInput/Skill/SKILL.md | 2 +- .../references/output-and-coordinates.md | 1 + 10 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 Assets/Tests/Editor/MouseInputSimulationResponseFactoryTests.cs create mode 100644 Assets/Tests/Editor/MouseInputSimulationResponseFactoryTests.cs.meta diff --git a/.agents/skills/uloop-simulate-mouse-input/SKILL.md b/.agents/skills/uloop-simulate-mouse-input/SKILL.md index 09b6c8f8f6..b70dfb023c 100644 --- a/.agents/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.agents/skills/uloop-simulate-mouse-input/SKILL.md @@ -59,7 +59,7 @@ uloop simulate-mouse-input --dry-run --x --y [--layer-mask ] [--ma ### Pause Point Inspection (Standard for E2E) -For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill — it covers line placement and interruption semantics. Tool-specific note: if `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was safely released. Clear inspection-only pause points (`uloop clear-pause-point --all`) before final validation. +For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill — it covers line placement and interruption semantics. Tool-specific note: if `InterruptedByPausePoint: true`, Unity is paused; read `PressDeliveredToGame` before retrying. Clear inspection-only pause points (`uloop clear-pause-point --all`) before final validation. ## When to use this vs simulate-mouse-ui diff --git a/.agents/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md b/.agents/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md index cc3d1445fa..aebdf6b330 100644 --- a/.agents/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md +++ b/.agents/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md @@ -21,6 +21,7 @@ Returns JSON with: - `InjectedUnityPositionX` / `InjectedUnityPositionY`: Coordinates injected into `Mouse.current.position` (or used for dry-run ScreenPointToRay) - `CoordinateConversionFormula`: Conversion formula used by the tool - `InterruptedByPausePoint` / `PausePointId` / `PausePointHitCount` / `PausePointHits`: Pause-point interruption info (all nullable except the boolean). `PausePointHits` lists every marker hit during this input in hit order; `PausePointId` only names the latest one. See the Pause Point Inspection section in SKILL.md +- `PressDeliveredToGame` (boolean, nullable): Set only when a `Click` / `LongPress` was interrupted by a pause point. `true`: the Input System processed the press edge in a gameplay update before the pause, so game code polling that frame observed it and the world state may already have changed (for example a block was mined). Do not retry; re-check the affected state and `pause-point-status`. `false`: the queued edge was discarded before any gameplay update, the game never observed a press, and a retry after resume is safe. `null` for other actions and uninterrupted responses Verify visual outcome with a follow-up screenshot. diff --git a/.claude/skills/uloop-simulate-mouse-input/SKILL.md b/.claude/skills/uloop-simulate-mouse-input/SKILL.md index 09b6c8f8f6..b70dfb023c 100644 --- a/.claude/skills/uloop-simulate-mouse-input/SKILL.md +++ b/.claude/skills/uloop-simulate-mouse-input/SKILL.md @@ -59,7 +59,7 @@ uloop simulate-mouse-input --dry-run --x --y [--layer-mask ] [--ma ### Pause Point Inspection (Standard for E2E) -For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill — it covers line placement and interruption semantics. Tool-specific note: if `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was safely released. Clear inspection-only pause points (`uloop clear-pause-point --all`) before final validation. +For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill — it covers line placement and interruption semantics. Tool-specific note: if `InterruptedByPausePoint: true`, Unity is paused; read `PressDeliveredToGame` before retrying. Clear inspection-only pause points (`uloop clear-pause-point --all`) before final validation. ## When to use this vs simulate-mouse-ui diff --git a/.claude/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md b/.claude/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md index cc3d1445fa..aebdf6b330 100644 --- a/.claude/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md +++ b/.claude/skills/uloop-simulate-mouse-input/references/output-and-coordinates.md @@ -21,6 +21,7 @@ Returns JSON with: - `InjectedUnityPositionX` / `InjectedUnityPositionY`: Coordinates injected into `Mouse.current.position` (or used for dry-run ScreenPointToRay) - `CoordinateConversionFormula`: Conversion formula used by the tool - `InterruptedByPausePoint` / `PausePointId` / `PausePointHitCount` / `PausePointHits`: Pause-point interruption info (all nullable except the boolean). `PausePointHits` lists every marker hit during this input in hit order; `PausePointId` only names the latest one. See the Pause Point Inspection section in SKILL.md +- `PressDeliveredToGame` (boolean, nullable): Set only when a `Click` / `LongPress` was interrupted by a pause point. `true`: the Input System processed the press edge in a gameplay update before the pause, so game code polling that frame observed it and the world state may already have changed (for example a block was mined). Do not retry; re-check the affected state and `pause-point-status`. `false`: the queued edge was discarded before any gameplay update, the game never observed a press, and a retry after resume is safe. `null` for other actions and uninterrupted responses Verify visual outcome with a follow-up screenshot. diff --git a/Assets/Tests/Editor/MouseInputSimulationResponseFactoryTests.cs b/Assets/Tests/Editor/MouseInputSimulationResponseFactoryTests.cs new file mode 100644 index 0000000000..9e3db93a4f --- /dev/null +++ b/Assets/Tests/Editor/MouseInputSimulationResponseFactoryTests.cs @@ -0,0 +1,68 @@ +#if ULOOP_HAS_INPUT_SYSTEM +#nullable enable +using NUnit.Framework; +using UnityEngine; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; +using io.github.hatayama.UnityCliLoop.ToolContracts; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Pins the pause-point interruption verdicts of simulate-mouse-input button responses. + /// + [TestFixture] + public sealed class MouseInputSimulationResponseFactoryTests + { + /// + /// Verifies a press applied before the pause reports a definite delivered verdict and re-check guidance instead of "may have". + /// + [Test] + public void InterruptedButtonResult_WhenPressWasApplied_ReportsPressDeliveredToGame() + { + SimulateMouseInputResponse response = MouseInputSimulationResponseFactory.InterruptedButtonResult( + UnityCliLoopMouseInputAction.Click, + "Left", + new Vector2(10f, 20f), + pressWasApplied: true); + + Assert.That(response.Success, Is.True); + Assert.That(response.InterruptedByPausePoint, Is.True); + Assert.That(response.PressDeliveredToGame, Is.True); + Assert.That(response.Message, Does.Contain("was delivered to the game before the pause")); + Assert.That(response.Message, Does.Contain("world state may already have changed")); + Assert.That(response.Message, Does.Not.Contain("may have registered")); + } + + /// + /// Verifies a press discarded before apply reports a definite not-delivered verdict and says a retry is safe. + /// + [Test] + public void InterruptedButtonResult_WhenPressWasDiscarded_ReportsPressNotDeliveredToGame() + { + SimulateMouseInputResponse response = MouseInputSimulationResponseFactory.InterruptedButtonResult( + UnityCliLoopMouseInputAction.LongPress, + "Right", + new Vector2(1f, 2f), + pressWasApplied: false); + + Assert.That(response.PressDeliveredToGame, Is.False); + Assert.That(response.Message, Does.Contain("never observed")); + Assert.That(response.Message, Does.Contain("safe to retry")); + } + + /// + /// Verifies non-button interruptions leave the press verdict absent because no press edge was involved. + /// + [Test] + public void InterruptedActionResult_LeavesPressDeliveredToGameNull() + { + SimulateMouseInputResponse response = MouseInputSimulationResponseFactory.InterruptedActionResult( + UnityCliLoopMouseInputAction.MoveDelta); + + Assert.That(response.InterruptedByPausePoint, Is.True); + Assert.That(response.PressDeliveredToGame, Is.Null); + } + } +} +#endif diff --git a/Assets/Tests/Editor/MouseInputSimulationResponseFactoryTests.cs.meta b/Assets/Tests/Editor/MouseInputSimulationResponseFactoryTests.cs.meta new file mode 100644 index 0000000000..a21b7ac7b0 --- /dev/null +++ b/Assets/Tests/Editor/MouseInputSimulationResponseFactoryTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 0c7498af9981849b688bead4adb13f63 +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 index 112fc6592e..fa93a53e0a 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputSimulationResponseFactory.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/MouseInputSimulationResponseFactory.cs @@ -47,6 +47,9 @@ internal static SimulateMouseInputResponse SuccessButtonResult( // (b) WaitForPressLifetime after a successful apply leaves pressWasApplied=true — the press // already landed (including when that press itself fired the pause point). Claiming // "discarded" in (b) inverts the diagnosis this message exists to prevent. + // The verdict is definite: apply only completes inside an Input System update of the + // configured gameplay type, so the game's polling in that frame observed the press. A + // hedged "may have registered" left callers re-deriving this from world state. internal static SimulateMouseInputResponse InterruptedButtonResult( UnityCliLoopMouseInputAction action, string buttonName, @@ -54,8 +57,8 @@ internal static SimulateMouseInputResponse InterruptedButtonResult( bool pressWasApplied) { string message = pressWasApplied - ? $"Mouse input stopped because Unity paused during Pause Point inspection. Button '{buttonName}' press was already delivered to the game before the pause; Unity CLI Loop released it from bookkeeping, so the game may have registered the press." - : $"Mouse input stopped because Unity paused during Pause Point inspection. Button '{buttonName}' was released from Unity CLI Loop bookkeeping; the queued input edge was discarded."; + ? $"Mouse input stopped because Unity paused during Pause Point inspection. Button '{buttonName}' press was delivered to the game before the pause: the Input System processed the press edge in a gameplay update, so game code polling that frame observed it and the world state may already have changed. Do not retry the press; re-check the affected state (and pause-point-status) before deciding the next step." + : $"Mouse input stopped because Unity paused during Pause Point inspection. Button '{buttonName}' was released from Unity CLI Loop bookkeeping; the queued input edge was discarded before any gameplay update processed it, so the game never observed a press and it is safe to retry after resume."; SimulateMouseInputResponse result = new() { Success = true, @@ -64,7 +67,8 @@ internal static SimulateMouseInputResponse InterruptedButtonResult( Button = buttonName, PositionX = inputPos.x, PositionY = inputPos.y, - InterruptedByPausePoint = true + InterruptedByPausePoint = true, + PressDeliveredToGame = pressWasApplied }; AttachPausePointHit(result); return result; diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputResponse.cs b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputResponse.cs index c6cdd8572e..0b8f26888d 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputResponse.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/SimulateMouseInputResponse.cs @@ -41,6 +41,14 @@ public class SimulateMouseInputResponse : UnityCliLoopToolResponse public string CoordinateConversionFormula { get; set; } = ""; public bool InterruptedByPausePoint { get; set; } /// + /// Set only on pause-point-interrupted Click/LongPress. True when the Input System processed + /// the press edge in a gameplay update before the pause (game code polling that frame + /// observed it, so the world state may already have changed); false when the queued edge + /// was discarded before any gameplay update, so the game never observed a press. Null for + /// non-button actions and for uninterrupted responses. + /// + public bool? PressDeliveredToGame { get; set; } + /// /// Id of the pause point that refused this call before it ran anything, null otherwise. /// Distinct from PausePointId, which reports a marker hit *during* the call: a refusal means /// no input was injected at all, so a caller reading only Success would miss that the action diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md index 09b6c8f8f6..b70dfb023c 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/SKILL.md @@ -59,7 +59,7 @@ uloop simulate-mouse-input --dry-run --x --y [--layer-mask ] [--ma ### Pause Point Inspection (Standard for E2E) -For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill — it covers line placement and interruption semantics. Tool-specific note: if `InterruptedByPausePoint: true`, Unity is paused and input bookkeeping was safely released. Clear inspection-only pause points (`uloop clear-pause-point --all`) before final validation. +For standard frame proof when this input drives a state transition, follow the `uloop-pause-point` skill — it covers line placement and interruption semantics. Tool-specific note: if `InterruptedByPausePoint: true`, Unity is paused; read `PressDeliveredToGame` before retrying. Clear inspection-only pause points (`uloop clear-pause-point --all`) before final validation. ## When to use this vs simulate-mouse-ui diff --git a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/references/output-and-coordinates.md b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/references/output-and-coordinates.md index cc3d1445fa..aebdf6b330 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/references/output-and-coordinates.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateMouseInput/Skill/references/output-and-coordinates.md @@ -21,6 +21,7 @@ Returns JSON with: - `InjectedUnityPositionX` / `InjectedUnityPositionY`: Coordinates injected into `Mouse.current.position` (or used for dry-run ScreenPointToRay) - `CoordinateConversionFormula`: Conversion formula used by the tool - `InterruptedByPausePoint` / `PausePointId` / `PausePointHitCount` / `PausePointHits`: Pause-point interruption info (all nullable except the boolean). `PausePointHits` lists every marker hit during this input in hit order; `PausePointId` only names the latest one. See the Pause Point Inspection section in SKILL.md +- `PressDeliveredToGame` (boolean, nullable): Set only when a `Click` / `LongPress` was interrupted by a pause point. `true`: the Input System processed the press edge in a gameplay update before the pause, so game code polling that frame observed it and the world state may already have changed (for example a block was mined). Do not retry; re-check the affected state and `pause-point-status`. `false`: the queued edge was discarded before any gameplay update, the game never observed a press, and a retry after resume is safe. `null` for other actions and uninterrupted responses Verify visual outcome with a follow-up screenshot.