From 9704ca00606c58aad8d79347f5e8ac4320f31d89 Mon Sep 17 00:00:00 2001 From: Masamichi Hatayama Date: Wed, 2 Sep 2026 22:16:47 +0900 Subject: [PATCH] fix: simulate-keyboard press-edge diagnostics no longer report a missing gameplay update on Fixed/Manual input projects (#2508) (cherry picked from commit 9fa01a119be31e64e2c1464f3adb7177c064159d) --- .../references/output.md | 6 ++-- .../references/output.md | 6 ++-- .../Editor/InputUpdateTypeResolverTests.cs | 33 +++++++++++++++++++ .../InputUpdateTypeResolverTests.cs.meta | 11 +++++++ ...essEdgeDiagnosticsMessageFormatterTests.cs | 18 +++++----- .../SimulateKeyboardResponseContractTests.cs | 4 +-- .../InputSystem/InputUpdateTypeResolver.cs | 12 +++++++ .../PressEdgeDiagnosticsMessageFormatter.cs | 8 ++--- .../DeferredPlayerLatchSyncDecision.cs | 7 ++-- .../KeyboardInputActionExecutor.cs | 25 +++++++------- .../SimulateKeyboardResponse.cs | 5 +-- .../Skill/references/output.md | 6 ++-- 12 files changed, 99 insertions(+), 42 deletions(-) create mode 100644 Assets/Tests/Editor/InputUpdateTypeResolverTests.cs create mode 100644 Assets/Tests/Editor/InputUpdateTypeResolverTests.cs.meta diff --git a/.agents/skills/uloop-simulate-keyboard/references/output.md b/.agents/skills/uloop-simulate-keyboard/references/output.md index 1bfa85cd3c..352a17c07c 100644 --- a/.agents/skills/uloop-simulate-keyboard/references/output.md +++ b/.agents/skills/uloop-simulate-keyboard/references/output.md @@ -17,10 +17,10 @@ Returns JSON with: - `RejectedByActivePausePointId` (string, nullable): Set when an active pause point rejected this call before any input was injected — distinct from `PausePointId`, which reports a marker hit during the call. When set, the action never happened, so do not read `Success` alone - `PressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge — verify with a focused log instead of trusting `Success` alone. `null` for every other action (`KeyUp`, `ReleaseAll`) and for timed-out responses; pause-point interruptions still report the observed value. When a single-shot pause point is armed, do not blindly retry on `false`: the input may still have registered late, so check `pause-point-status` for a hit first — a blind retry can consume a re-enabled marker or double-fire the scenario - `PressHoldExtendedFrames` (integer, nullable): Extra observation frames the key stayed held beyond the normal duration window while waiting for `wasPressedThisFrame`; `null` when the release was not delayed -- `PressEdgeConsumedByUpdateType` / `PressEdgeAnyDynamicUpdateObserved` / `PressEdgeKeyAlreadyPressedBeforeQueue` (nullable): Diagnostics populated only when `PressEdgeObserved` is `false` (all `null` when the edge was observed). `Message` carries the same diagnosis as text. Read them before retrying: +- `PressEdgeConsumedByUpdateType` / `PressEdgeAnyGameplayUpdateObserved` / `PressEdgeKeyAlreadyPressedBeforeQueue` (nullable): Diagnostics populated only when `PressEdgeObserved` is `false` (all `null` when the edge was observed). `Message` carries the same diagnosis as text. Read them before retrying: | Diagnostic | Meaning | Next action | |---|---|---| | `PressEdgeKeyAlreadyPressedBeforeQueue=true` | The key was already held; no press transition could occur | Release with `KeyUp`, then press again | -| `PressEdgeConsumedByUpdateType` names a non-`Dynamic` update type (e.g. `Editor`) | An editor-side update consumed the edge before gameplay polling saw it | Retry: rerun `Press` directly; for `KeyDown` the key is now held, so `KeyUp` first (a held key rejects a second `KeyDown`) | -| `PressEdgeAnyDynamicUpdateObserved=false` | No `Dynamic`-type input update ran during the press window (this flag does not track `Fixed`/`Manual` gameplay updates) | Check that PlayMode is running and unpaused; do not retry blindly | +| `PressEdgeConsumedByUpdateType` is `Editor` | An editor-side update consumed the edge before gameplay polling saw it | Retry: rerun `Press` directly; for `KeyDown` the key is now held, so `KeyUp` first (a held key rejects a second `KeyDown`) | +| `PressEdgeAnyGameplayUpdateObserved=false` | No gameplay input update (`Dynamic`, `Fixed`, or `Manual`) ran during the press window; `Editor` ticks do not count | Check that PlayMode is running and unpaused; do not retry blindly | diff --git a/.claude/skills/uloop-simulate-keyboard/references/output.md b/.claude/skills/uloop-simulate-keyboard/references/output.md index 1bfa85cd3c..352a17c07c 100644 --- a/.claude/skills/uloop-simulate-keyboard/references/output.md +++ b/.claude/skills/uloop-simulate-keyboard/references/output.md @@ -17,10 +17,10 @@ Returns JSON with: - `RejectedByActivePausePointId` (string, nullable): Set when an active pause point rejected this call before any input was injected — distinct from `PausePointId`, which reports a marker hit during the call. When set, the action never happened, so do not read `Success` alone - `PressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge — verify with a focused log instead of trusting `Success` alone. `null` for every other action (`KeyUp`, `ReleaseAll`) and for timed-out responses; pause-point interruptions still report the observed value. When a single-shot pause point is armed, do not blindly retry on `false`: the input may still have registered late, so check `pause-point-status` for a hit first — a blind retry can consume a re-enabled marker or double-fire the scenario - `PressHoldExtendedFrames` (integer, nullable): Extra observation frames the key stayed held beyond the normal duration window while waiting for `wasPressedThisFrame`; `null` when the release was not delayed -- `PressEdgeConsumedByUpdateType` / `PressEdgeAnyDynamicUpdateObserved` / `PressEdgeKeyAlreadyPressedBeforeQueue` (nullable): Diagnostics populated only when `PressEdgeObserved` is `false` (all `null` when the edge was observed). `Message` carries the same diagnosis as text. Read them before retrying: +- `PressEdgeConsumedByUpdateType` / `PressEdgeAnyGameplayUpdateObserved` / `PressEdgeKeyAlreadyPressedBeforeQueue` (nullable): Diagnostics populated only when `PressEdgeObserved` is `false` (all `null` when the edge was observed). `Message` carries the same diagnosis as text. Read them before retrying: | Diagnostic | Meaning | Next action | |---|---|---| | `PressEdgeKeyAlreadyPressedBeforeQueue=true` | The key was already held; no press transition could occur | Release with `KeyUp`, then press again | -| `PressEdgeConsumedByUpdateType` names a non-`Dynamic` update type (e.g. `Editor`) | An editor-side update consumed the edge before gameplay polling saw it | Retry: rerun `Press` directly; for `KeyDown` the key is now held, so `KeyUp` first (a held key rejects a second `KeyDown`) | -| `PressEdgeAnyDynamicUpdateObserved=false` | No `Dynamic`-type input update ran during the press window (this flag does not track `Fixed`/`Manual` gameplay updates) | Check that PlayMode is running and unpaused; do not retry blindly | +| `PressEdgeConsumedByUpdateType` is `Editor` | An editor-side update consumed the edge before gameplay polling saw it | Retry: rerun `Press` directly; for `KeyDown` the key is now held, so `KeyUp` first (a held key rejects a second `KeyDown`) | +| `PressEdgeAnyGameplayUpdateObserved=false` | No gameplay input update (`Dynamic`, `Fixed`, or `Manual`) ran during the press window; `Editor` ticks do not count | Check that PlayMode is running and unpaused; do not retry blindly | diff --git a/Assets/Tests/Editor/InputUpdateTypeResolverTests.cs b/Assets/Tests/Editor/InputUpdateTypeResolverTests.cs new file mode 100644 index 0000000000..f8e0717f6f --- /dev/null +++ b/Assets/Tests/Editor/InputUpdateTypeResolverTests.cs @@ -0,0 +1,33 @@ +#nullable enable +#if ULOOP_HAS_INPUT_SYSTEM +using NUnit.Framework; +using UnityEngine.InputSystem.LowLevel; + +using io.github.hatayama.UnityCliLoop.FirstPartyTools; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Pure unit tests for the gameplay update-type classification shared by press-edge observation and diagnostics. + /// + public sealed class InputUpdateTypeResolverTests + { + [TestCase(InputUpdateType.Dynamic)] + [TestCase(InputUpdateType.Fixed)] + [TestCase(InputUpdateType.Manual)] + public void IsGameplayUpdate_WhenGameplayInputUpdateType_ReturnsTrue(InputUpdateType updateType) + { + // Verifies every update type gameplay polling can observe a press edge in counts as a gameplay update. + Assert.That(InputUpdateTypeResolver.IsGameplayUpdate(updateType), Is.True); + } + + [TestCase(InputUpdateType.Editor)] + [TestCase(InputUpdateType.None)] + public void IsGameplayUpdate_WhenNotGameplayInputUpdateType_ReturnsFalse(InputUpdateType updateType) + { + // Verifies Editor ticks and the absence of an update are not mistaken for gameplay input updates. + Assert.That(InputUpdateTypeResolver.IsGameplayUpdate(updateType), Is.False); + } + } +} +#endif diff --git a/Assets/Tests/Editor/InputUpdateTypeResolverTests.cs.meta b/Assets/Tests/Editor/InputUpdateTypeResolverTests.cs.meta new file mode 100644 index 0000000000..de44aad7e6 --- /dev/null +++ b/Assets/Tests/Editor/InputUpdateTypeResolverTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9f14fa0cbe405449f849316350a9bcd7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Tests/Editor/PressEdgeDiagnosticsMessageFormatterTests.cs b/Assets/Tests/Editor/PressEdgeDiagnosticsMessageFormatterTests.cs index e7d09495b1..db1b7eb4c7 100644 --- a/Assets/Tests/Editor/PressEdgeDiagnosticsMessageFormatterTests.cs +++ b/Assets/Tests/Editor/PressEdgeDiagnosticsMessageFormatterTests.cs @@ -16,31 +16,31 @@ public void BuildSuffix_WhenKeyAlreadyPressedBeforeQueue_ReportsNoTransition() // Verifies a pre-existing down state (no edge possible) is reported over other causes. string suffix = PressEdgeDiagnosticsMessageFormatter.BuildSuffix( consumedByUpdateType: "Dynamic", - anyDynamicUpdateObserved: true, + anyGameplayUpdateObserved: true, keyAlreadyPressedBeforeQueue: true); Assert.That(suffix, Does.Contain("already down")); } [Test] - public void BuildSuffix_WhenNotConsumedAndNoDynamicUpdateRan_ReportsNoDynamicUpdate() + public void BuildSuffix_WhenNotConsumedAndNoGameplayUpdateRan_ReportsNoGameplayUpdate() { - // Verifies a stalled Dynamic update loop is distinguished from a dropped event. + // Verifies a stalled gameplay update loop is distinguished from a dropped event. string suffix = PressEdgeDiagnosticsMessageFormatter.BuildSuffix( consumedByUpdateType: null, - anyDynamicUpdateObserved: false, + anyGameplayUpdateObserved: false, keyAlreadyPressedBeforeQueue: false); - Assert.That(suffix, Does.Contain("no Dynamic update ran")); + Assert.That(suffix, Does.Contain("no gameplay input update")); } [Test] - public void BuildSuffix_WhenNotConsumedButDynamicUpdatesRan_ReportsDroppedEvent() + public void BuildSuffix_WhenNotConsumedButGameplayUpdatesRan_ReportsDroppedEvent() { // Verifies the event-never-consumed case is distinguished from a stalled update loop. string suffix = PressEdgeDiagnosticsMessageFormatter.BuildSuffix( consumedByUpdateType: null, - anyDynamicUpdateObserved: true, + anyGameplayUpdateObserved: true, keyAlreadyPressedBeforeQueue: false); Assert.That(suffix, Does.Contain("not consumed by any recorded")); @@ -52,7 +52,7 @@ public void BuildSuffix_WhenConsumedByEditorUpdate_ReportsEditorInvisibility() // Verifies the Editor-update-consumed case names the reason gameplay could not see it. string suffix = PressEdgeDiagnosticsMessageFormatter.BuildSuffix( consumedByUpdateType: "Editor", - anyDynamicUpdateObserved: false, + anyGameplayUpdateObserved: false, keyAlreadyPressedBeforeQueue: false); Assert.That(suffix, Does.Contain("Editor update")); @@ -64,7 +64,7 @@ public void BuildSuffix_WhenConsumedByOtherUpdateType_NamesThatUpdateType() // Verifies an unexpected consuming update type (e.g. Fixed) is surfaced by name. string suffix = PressEdgeDiagnosticsMessageFormatter.BuildSuffix( consumedByUpdateType: "Fixed", - anyDynamicUpdateObserved: true, + anyGameplayUpdateObserved: true, keyAlreadyPressedBeforeQueue: false); Assert.That(suffix, Does.Contain("Fixed update")); diff --git a/Assets/Tests/Editor/SimulateKeyboardResponseContractTests.cs b/Assets/Tests/Editor/SimulateKeyboardResponseContractTests.cs index 8086df8a77..e03496c7da 100644 --- a/Assets/Tests/Editor/SimulateKeyboardResponseContractTests.cs +++ b/Assets/Tests/Editor/SimulateKeyboardResponseContractTests.cs @@ -22,7 +22,7 @@ public sealed class SimulateKeyboardResponseContractTests + "\"PausePointHits\":[{\"Id\":\"hit\",\"HitCount\":1}]," + "\"PressEdgeObserved\":true,\"PressHoldExtendedFrames\":2," + "\"PressEdgeConsumedByUpdateType\":\"Dynamic\"," - + "\"PressEdgeAnyDynamicUpdateObserved\":true," + + "\"PressEdgeAnyGameplayUpdateObserved\":true," + "\"PressEdgeKeyAlreadyPressedBeforeQueue\":false," + "\"KeyStateTrackedHeld\":true,\"KeyStateDeviceIsPressed\":false," + "\"ReleasedKeys\":[\"Space\"],\"Success\":true}"; @@ -66,7 +66,7 @@ public void SimulateKeyboardResponse_WhenOptionalFieldsAreSet_IncludesThoseKeysI PressEdgeObserved = true, PressHoldExtendedFrames = 2, PressEdgeConsumedByUpdateType = "Dynamic", - PressEdgeAnyDynamicUpdateObserved = true, + PressEdgeAnyGameplayUpdateObserved = true, PressEdgeKeyAlreadyPressedBeforeQueue = false, KeyStateTrackedHeld = true, KeyStateDeviceIsPressed = false, diff --git a/Packages/src/Editor/FirstPartyTools/Common/InputSystem/InputUpdateTypeResolver.cs b/Packages/src/Editor/FirstPartyTools/Common/InputSystem/InputUpdateTypeResolver.cs index 70335d60b7..c875e20f62 100644 --- a/Packages/src/Editor/FirstPartyTools/Common/InputSystem/InputUpdateTypeResolver.cs +++ b/Packages/src/Editor/FirstPartyTools/Common/InputSystem/InputUpdateTypeResolver.cs @@ -42,6 +42,18 @@ public static bool IsMatch(InputUpdateType current, InputUpdateType expected) return (current & expected) == expected; } + // A press edge is visible to gameplay polling only when the Input System processed it + // in one of the player-loop update types; the Editor tick never surfaces + // wasPressedThisFrame to gameplay Update, and None means no update ran at all. Press-edge + // observation and its miss diagnostics must agree on this set, otherwise a Fixed or Manual + // project reads "no gameplay update ran" while its gameplay updates did run. + public static bool IsGameplayUpdate(InputUpdateType updateType) + { + return updateType == InputUpdateType.Dynamic + || updateType == InputUpdateType.Fixed + || updateType == InputUpdateType.Manual; + } + public static bool RequiresExplicitUpdate() { InputSettings? settings = InputSystem.settings; diff --git a/Packages/src/Editor/FirstPartyTools/Common/InputSystem/PressEdgeDiagnosticsMessageFormatter.cs b/Packages/src/Editor/FirstPartyTools/Common/InputSystem/PressEdgeDiagnosticsMessageFormatter.cs index 3af3798ff1..c4ba76d0b0 100644 --- a/Packages/src/Editor/FirstPartyTools/Common/InputSystem/PressEdgeDiagnosticsMessageFormatter.cs +++ b/Packages/src/Editor/FirstPartyTools/Common/InputSystem/PressEdgeDiagnosticsMessageFormatter.cs @@ -13,7 +13,7 @@ internal static class PressEdgeDiagnosticsMessageFormatter { public static string BuildSuffix( string? consumedByUpdateType, - bool anyDynamicUpdateObserved, + bool anyGameplayUpdateObserved, bool keyAlreadyPressedBeforeQueue) { if (keyAlreadyPressedBeforeQueue) @@ -23,9 +23,9 @@ public static string BuildSuffix( if (consumedByUpdateType == null) { - return anyDynamicUpdateObserved - ? " (the key-down event was not consumed by any recorded Input System update, even though Dynamic updates ran during the wait)" - : " (no Dynamic update ran during the wait, so gameplay Update polling never had a chance to observe it)"; + return anyGameplayUpdateObserved + ? " (the key-down event was not consumed by any recorded Input System update, even though gameplay input updates ran during the wait)" + : " (no gameplay input update (Dynamic, Fixed, or Manual) ran during the wait, so gameplay polling never had a chance to observe it)"; } if (consumedByUpdateType == "Editor") diff --git a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/DeferredPlayerLatchSyncDecision.cs b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/DeferredPlayerLatchSyncDecision.cs index 49348acea8..15afc9a802 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/DeferredPlayerLatchSyncDecision.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/DeferredPlayerLatchSyncDecision.cs @@ -13,13 +13,12 @@ internal static class DeferredPlayerLatchSyncDecision /// Decides whether this Input System update should sync stale player latches and /// then drop the callback. Why Editor is kept: pause only runs Editor updates, so /// consuming the one-shot there would skip the first player update after resume. - /// Why not HasFlag: Default includes Editor and would look like a player update. + /// Why shared predicate: the latch sync and the press-edge visibility check must agree + /// on which update types are player updates, so both read InputUpdateTypeResolver. /// internal static DeferredLatchSyncTickDecision Decide(InputUpdateType currentUpdateType) { - bool isPlayerUpdate = currentUpdateType == InputUpdateType.Dynamic - || currentUpdateType == InputUpdateType.Fixed - || currentUpdateType == InputUpdateType.Manual; + bool isPlayerUpdate = InputUpdateTypeResolver.IsGameplayUpdate(currentUpdateType); return new DeferredLatchSyncTickDecision(isPlayerUpdate, isPlayerUpdate); } } diff --git a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/KeyboardInputActionExecutor.cs b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/KeyboardInputActionExecutor.cs index 4b211ebcca..fa0ffa8a0f 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/KeyboardInputActionExecutor.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/KeyboardInputActionExecutor.cs @@ -215,7 +215,7 @@ private static SimulateKeyboardResponse BuildPressSuccessResponse( PressEdgeObserved = pressEdgeObserved, PressHoldExtendedFrames = pressHoldExtendedFrames > 0 ? pressHoldExtendedFrames : null, PressEdgeConsumedByUpdateType = pressEdgeObserved ? null : edgeMissDiagnostics.ConsumedByUpdateType, - PressEdgeAnyDynamicUpdateObserved = pressEdgeObserved ? null : edgeMissDiagnostics.AnyDynamicUpdateObserved, + PressEdgeAnyGameplayUpdateObserved = pressEdgeObserved ? null : edgeMissDiagnostics.AnyGameplayUpdateObserved, PressEdgeKeyAlreadyPressedBeforeQueue = pressEdgeObserved ? null : edgeMissDiagnostics.KeyAlreadyPressedBeforeQueue }; } @@ -240,7 +240,7 @@ private static string BuildPressEdgeText( " (press edge was not observed via wasPressedThisFrame; gameplay polling may have missed it, so retry or verify with a focused log)" + PressEdgeDiagnosticsMessageFormatter.BuildSuffix( edgeMissDiagnostics.ConsumedByUpdateType, - edgeMissDiagnostics.AnyDynamicUpdateObserved, + edgeMissDiagnostics.AnyGameplayUpdateObserved, edgeMissDiagnostics.KeyAlreadyPressedBeforeQueue); } @@ -352,7 +352,7 @@ await KeyboardInputMainThreadCleanup.RollbackHeldKey( : " (press edge was not observed via wasPressedThisFrame; gameplay polling may have missed it)" + PressEdgeDiagnosticsMessageFormatter.BuildSuffix( edgeMissDiagnostics.ConsumedByUpdateType, - edgeMissDiagnostics.AnyDynamicUpdateObserved, + edgeMissDiagnostics.AnyGameplayUpdateObserved, edgeMissDiagnostics.KeyAlreadyPressedBeforeQueue); return new SimulateKeyboardResponse { @@ -362,7 +362,7 @@ await KeyboardInputMainThreadCleanup.RollbackHeldKey( KeyName = keyName, PressEdgeObserved = pressEdgeObserved, PressEdgeConsumedByUpdateType = pressEdgeObserved ? null : edgeMissDiagnostics.ConsumedByUpdateType, - PressEdgeAnyDynamicUpdateObserved = pressEdgeObserved ? null : edgeMissDiagnostics.AnyDynamicUpdateObserved, + PressEdgeAnyGameplayUpdateObserved = pressEdgeObserved ? null : edgeMissDiagnostics.AnyGameplayUpdateObserved, PressEdgeKeyAlreadyPressedBeforeQueue = pressEdgeObserved ? null : edgeMissDiagnostics.KeyAlreadyPressedBeforeQueue }; } @@ -429,11 +429,11 @@ await KeyboardInputMainThreadCleanup.ReleaseKeyStateIfPossible( }; } - // Runs inside InputSystem.onAfterUpdate. Editor updates are excluded because a press - // consumed there never surfaces as wasPressedThisFrame to gameplay Update polling. + // Runs inside InputSystem.onAfterUpdate. Only gameplay update types count because a press + // consumed in the Editor tick never surfaces as wasPressedThisFrame to gameplay Update polling. private static bool IsGameplayPressEdgeVisible(Keyboard keyboard, Key key) { - if (InputState.currentUpdateType == InputUpdateType.Editor) + if (!InputUpdateTypeResolver.IsGameplayUpdate(InputState.currentUpdateType)) { return false; } @@ -443,17 +443,18 @@ private static bool IsGameplayPressEdgeVisible(Keyboard keyboard, Key key) // Runs inside InputSystem.onAfterUpdate alongside the edge visibility check above. // Why: the root cause of an unobserved edge could not be reproduced (see Round-6 // investigation), so this records which update type (if any) actually saw - // wasPressedThisFrame become true, and whether a Dynamic update ran at all, to diagnose - // the next real occurrence directly from the response instead of guessing. + // wasPressedThisFrame become true, and whether any gameplay update ran at all, to diagnose + // the next real occurrence directly from the response instead of guessing. The gameplay + // predicate is shared with IsGameplayPressEdgeVisible so the flag can never contradict it. private static void RecordPressEdgeMissDiagnostics( Keyboard keyboard, Key key, PressEdgeMissDiagnostics diagnostics) { InputUpdateType currentUpdateType = InputState.currentUpdateType; - if (currentUpdateType == InputUpdateType.Dynamic) + if (InputUpdateTypeResolver.IsGameplayUpdate(currentUpdateType)) { - diagnostics.AnyDynamicUpdateObserved = true; + diagnostics.AnyGameplayUpdateObserved = true; } if (diagnostics.ConsumedByUpdateType == null && keyboard[key].wasPressedThisFrame) @@ -468,7 +469,7 @@ private static void RecordPressEdgeMissDiagnostics( private sealed class PressEdgeMissDiagnostics { public string? ConsumedByUpdateType; - public bool AnyDynamicUpdateObserved; + public bool AnyGameplayUpdateObserved; public bool KeyAlreadyPressedBeforeQueue; } } diff --git a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardResponse.cs b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardResponse.cs index 58dfa7a840..eb9359a8fb 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardResponse.cs +++ b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/SimulateKeyboardResponse.cs @@ -58,10 +58,11 @@ public class SimulateKeyboardResponse : UnityCliLoopToolResponse public string? PressEdgeConsumedByUpdateType { get; set; } /// - /// Whether any Dynamic update ran while waiting for the press edge to be observed. + /// Whether any gameplay input update (Dynamic, Fixed, or Manual) ran while waiting for + /// the press edge to be observed. Editor updates do not count. /// [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public bool? PressEdgeAnyDynamicUpdateObserved { get; set; } + public bool? PressEdgeAnyGameplayUpdateObserved { get; set; } /// /// Whether the key was already pressed before this action was queued, meaning no press diff --git a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/references/output.md b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/references/output.md index 1bfa85cd3c..352a17c07c 100644 --- a/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/references/output.md +++ b/Packages/src/Editor/FirstPartyTools/SimulateKeyboard/Skill/references/output.md @@ -17,10 +17,10 @@ Returns JSON with: - `RejectedByActivePausePointId` (string, nullable): Set when an active pause point rejected this call before any input was injected — distinct from `PausePointId`, which reports a marker hit during the call. When set, the action never happened, so do not read `Success` alone - `PressEdgeObserved` (boolean, nullable): For `Press` and `KeyDown`, whether the press edge (`wasPressedThisFrame`) was visible inside a gameplay input update. `false` means the CLI succeeded but gameplay polling most likely missed the edge — verify with a focused log instead of trusting `Success` alone. `null` for every other action (`KeyUp`, `ReleaseAll`) and for timed-out responses; pause-point interruptions still report the observed value. When a single-shot pause point is armed, do not blindly retry on `false`: the input may still have registered late, so check `pause-point-status` for a hit first — a blind retry can consume a re-enabled marker or double-fire the scenario - `PressHoldExtendedFrames` (integer, nullable): Extra observation frames the key stayed held beyond the normal duration window while waiting for `wasPressedThisFrame`; `null` when the release was not delayed -- `PressEdgeConsumedByUpdateType` / `PressEdgeAnyDynamicUpdateObserved` / `PressEdgeKeyAlreadyPressedBeforeQueue` (nullable): Diagnostics populated only when `PressEdgeObserved` is `false` (all `null` when the edge was observed). `Message` carries the same diagnosis as text. Read them before retrying: +- `PressEdgeConsumedByUpdateType` / `PressEdgeAnyGameplayUpdateObserved` / `PressEdgeKeyAlreadyPressedBeforeQueue` (nullable): Diagnostics populated only when `PressEdgeObserved` is `false` (all `null` when the edge was observed). `Message` carries the same diagnosis as text. Read them before retrying: | Diagnostic | Meaning | Next action | |---|---|---| | `PressEdgeKeyAlreadyPressedBeforeQueue=true` | The key was already held; no press transition could occur | Release with `KeyUp`, then press again | -| `PressEdgeConsumedByUpdateType` names a non-`Dynamic` update type (e.g. `Editor`) | An editor-side update consumed the edge before gameplay polling saw it | Retry: rerun `Press` directly; for `KeyDown` the key is now held, so `KeyUp` first (a held key rejects a second `KeyDown`) | -| `PressEdgeAnyDynamicUpdateObserved=false` | No `Dynamic`-type input update ran during the press window (this flag does not track `Fixed`/`Manual` gameplay updates) | Check that PlayMode is running and unpaused; do not retry blindly | +| `PressEdgeConsumedByUpdateType` is `Editor` | An editor-side update consumed the edge before gameplay polling saw it | Retry: rerun `Press` directly; for `KeyDown` the key is now held, so `KeyUp` first (a held key rejects a second `KeyDown`) | +| `PressEdgeAnyGameplayUpdateObserved=false` | No gameplay input update (`Dynamic`, `Fixed`, or `Manual`) ran during the press window; `Editor` ticks do not count | Check that PlayMode is running and unpaused; do not retry blindly |