From 21f07e876529a5d0caa569bf2ceee05049e29921 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 25 Jul 2026 12:15:21 +0900 Subject: [PATCH 1/3] fix: Fold fast-progressing freeze guidance into the pause-point quick check steps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 11 Tetris verification (severity 6/10) found the existing "freeze first" note ineffective: placed as a standalone paragraph after the Quick Check Template, it got read but not recalled at the moment it mattered — the reporter only remembered the pitfall after gravity auto-landed pieces mid-scenario and broke the test. The follow-up interview confirmed a numbered decision step at the top of the template would have prevented this. Restructure the template so the freeze decision is step 1 (before any state is armed or arranged), the one-command arm/fire/wait becomes step 2, later steps renumber, and the now-redundant standalone paragraph is removed — net prose does not grow, keeping the recently dieted skill (#1978) lean. .claude/skills/uloop-pause-point/SKILL.md and .agents/skills/uloop-pause-point/SKILL.md are regenerated copies produced by `uloop skills install --claude --agents`, not hand edits. --- .agents/skills/uloop-pause-point/SKILL.md | 11 +++++------ .claude/skills/uloop-pause-point/SKILL.md | 11 +++++------ .../Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md | 11 +++++------ 3 files changed, 15 insertions(+), 18 deletions(-) diff --git a/.agents/skills/uloop-pause-point/SKILL.md b/.agents/skills/uloop-pause-point/SKILL.md index 22d111c651..48f1eef9c7 100644 --- a/.agents/skills/uloop-pause-point/SKILL.md +++ b/.agents/skills/uloop-pause-point/SKILL.md @@ -9,7 +9,8 @@ description: "Pauses Unity playback at any source file:line without editing code Use this small loop for one representative frame you care about. No source edit and no recompile: the pause point is patched into the already-compiled code and can be enabled mid-PlayMode. -1. Enter PlayMode, then run one foreground command that arms the pause point, fires the input, and waits for the hit: +1. Enter PlayMode, then decide before anything else: does the game progress on its own (timers, gravity, spawners)? If yes, pause it right away with `control-play-mode --action Pause`, arrange any scenario state while paused, and add `--resume-play` to the command in step 2 — see Fast-Progressing Games below. +2. Run one foreground command that arms the pause point, fires the input, and waits for the hit: ```bash uloop enable-pause-point --file Assets/Scripts/Enemy.cs --line 42 --timeout-seconds 30 --await --trigger "simulate-keyboard --action Press --key Space" @@ -23,14 +24,12 @@ When the game reaches the line on its own, omit `--trigger`. Fall back to split The response returns the derived marker `Id` (`Assets/Scripts/Enemy.cs:42`), the `ResolvedLine` that was actually patched, the `ResolvedMethod`, and `ResolvedLineText` — the actual source text at `ResolvedLine`. When the requested line has no executable statement, the pause point rounds forward to the next executable line — check `ResolvedLine`/`ResolvedLineText` when precision matters, and re-check them after every code edit — a rewritten file shifts line numbers. Use the returned `Id` for every follow-up command. On a hit, this same response already carries `CapturedVariables` and every other field `await-pause-point` would have returned — no separate `await-pause-point` call is needed. -2. Read `CapturedVariables` in the hit response first: the locals, parameters, and `this` instance fields at the paused line are already there (see Reading CapturedVariables). -3. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. -4. A `single-shot` marker (the default) disarms itself after the hit, so no clear call is required before moving on. Clearing is still what removes the underlying code patch (a disarmed marker leaves the patch installed), so for `continuous`/`trace` markers, or when the method must run fully untouched again, clear it with `uloop clear-pause-point --id "Assets/Scripts/Enemy.cs:42"` (or `--all` to clear every active marker at once) or stop PlayMode. Clearing resumes Play Mode only when the current pause is owned by a pause-point hit — the clear response then carries a `Warning` saying it resumed Play Mode. A manual pause (`control-play-mode --action Pause` or the Editor pause button) is left untouched by clear. +3. Read `CapturedVariables` in the hit response first: the locals, parameters, and `this` instance fields at the paused line are already there (see Reading CapturedVariables). +4. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. +5. A `single-shot` marker (the default) disarms itself after the hit, so no clear call is required before moving on. Clearing is still what removes the underlying code patch (a disarmed marker leaves the patch installed), so for `continuous`/`trace` markers, or when the method must run fully untouched again, clear it with `uloop clear-pause-point --id "Assets/Scripts/Enemy.cs:42"` (or `--all` to clear every active marker at once) or stop PlayMode. Clearing resumes Play Mode only when the current pause is owned by a pause-point hit — the clear response then carries a `Warning` saying it resumed Play Mode. A manual pause (`control-play-mode --action Pause` or the Editor pause button) is left untouched by clear. A hit pauses Unity at the next frame boundary — the patched method and the rest of that frame still run to completion. Only `CapturedVariables` is evidence of the values at the patched line; state read after the pause (for example via `execute-dynamic-code`) may already have advanced past it. -If the game progresses on its own (timers, gravity, spawners), freeze first and arm while paused — see Fast-Progressing Games below. - ## Capture Modes and History Choose the capture mode when enabling a pause point: diff --git a/.claude/skills/uloop-pause-point/SKILL.md b/.claude/skills/uloop-pause-point/SKILL.md index 22d111c651..48f1eef9c7 100644 --- a/.claude/skills/uloop-pause-point/SKILL.md +++ b/.claude/skills/uloop-pause-point/SKILL.md @@ -9,7 +9,8 @@ description: "Pauses Unity playback at any source file:line without editing code Use this small loop for one representative frame you care about. No source edit and no recompile: the pause point is patched into the already-compiled code and can be enabled mid-PlayMode. -1. Enter PlayMode, then run one foreground command that arms the pause point, fires the input, and waits for the hit: +1. Enter PlayMode, then decide before anything else: does the game progress on its own (timers, gravity, spawners)? If yes, pause it right away with `control-play-mode --action Pause`, arrange any scenario state while paused, and add `--resume-play` to the command in step 2 — see Fast-Progressing Games below. +2. Run one foreground command that arms the pause point, fires the input, and waits for the hit: ```bash uloop enable-pause-point --file Assets/Scripts/Enemy.cs --line 42 --timeout-seconds 30 --await --trigger "simulate-keyboard --action Press --key Space" @@ -23,14 +24,12 @@ When the game reaches the line on its own, omit `--trigger`. Fall back to split The response returns the derived marker `Id` (`Assets/Scripts/Enemy.cs:42`), the `ResolvedLine` that was actually patched, the `ResolvedMethod`, and `ResolvedLineText` — the actual source text at `ResolvedLine`. When the requested line has no executable statement, the pause point rounds forward to the next executable line — check `ResolvedLine`/`ResolvedLineText` when precision matters, and re-check them after every code edit — a rewritten file shifts line numbers. Use the returned `Id` for every follow-up command. On a hit, this same response already carries `CapturedVariables` and every other field `await-pause-point` would have returned — no separate `await-pause-point` call is needed. -2. Read `CapturedVariables` in the hit response first: the locals, parameters, and `this` instance fields at the paused line are already there (see Reading CapturedVariables). -3. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. -4. A `single-shot` marker (the default) disarms itself after the hit, so no clear call is required before moving on. Clearing is still what removes the underlying code patch (a disarmed marker leaves the patch installed), so for `continuous`/`trace` markers, or when the method must run fully untouched again, clear it with `uloop clear-pause-point --id "Assets/Scripts/Enemy.cs:42"` (or `--all` to clear every active marker at once) or stop PlayMode. Clearing resumes Play Mode only when the current pause is owned by a pause-point hit — the clear response then carries a `Warning` saying it resumed Play Mode. A manual pause (`control-play-mode --action Pause` or the Editor pause button) is left untouched by clear. +3. Read `CapturedVariables` in the hit response first: the locals, parameters, and `this` instance fields at the paused line are already there (see Reading CapturedVariables). +4. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. +5. A `single-shot` marker (the default) disarms itself after the hit, so no clear call is required before moving on. Clearing is still what removes the underlying code patch (a disarmed marker leaves the patch installed), so for `continuous`/`trace` markers, or when the method must run fully untouched again, clear it with `uloop clear-pause-point --id "Assets/Scripts/Enemy.cs:42"` (or `--all` to clear every active marker at once) or stop PlayMode. Clearing resumes Play Mode only when the current pause is owned by a pause-point hit — the clear response then carries a `Warning` saying it resumed Play Mode. A manual pause (`control-play-mode --action Pause` or the Editor pause button) is left untouched by clear. A hit pauses Unity at the next frame boundary — the patched method and the rest of that frame still run to completion. Only `CapturedVariables` is evidence of the values at the patched line; state read after the pause (for example via `execute-dynamic-code`) may already have advanced past it. -If the game progresses on its own (timers, gravity, spawners), freeze first and arm while paused — see Fast-Progressing Games below. - ## Capture Modes and History Choose the capture mode when enabling a pause point: diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md index 22d111c651..48f1eef9c7 100644 --- a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md @@ -9,7 +9,8 @@ description: "Pauses Unity playback at any source file:line without editing code Use this small loop for one representative frame you care about. No source edit and no recompile: the pause point is patched into the already-compiled code and can be enabled mid-PlayMode. -1. Enter PlayMode, then run one foreground command that arms the pause point, fires the input, and waits for the hit: +1. Enter PlayMode, then decide before anything else: does the game progress on its own (timers, gravity, spawners)? If yes, pause it right away with `control-play-mode --action Pause`, arrange any scenario state while paused, and add `--resume-play` to the command in step 2 — see Fast-Progressing Games below. +2. Run one foreground command that arms the pause point, fires the input, and waits for the hit: ```bash uloop enable-pause-point --file Assets/Scripts/Enemy.cs --line 42 --timeout-seconds 30 --await --trigger "simulate-keyboard --action Press --key Space" @@ -23,14 +24,12 @@ When the game reaches the line on its own, omit `--trigger`. Fall back to split The response returns the derived marker `Id` (`Assets/Scripts/Enemy.cs:42`), the `ResolvedLine` that was actually patched, the `ResolvedMethod`, and `ResolvedLineText` — the actual source text at `ResolvedLine`. When the requested line has no executable statement, the pause point rounds forward to the next executable line — check `ResolvedLine`/`ResolvedLineText` when precision matters, and re-check them after every code edit — a rewritten file shifts line numbers. Use the returned `Id` for every follow-up command. On a hit, this same response already carries `CapturedVariables` and every other field `await-pause-point` would have returned — no separate `await-pause-point` call is needed. -2. Read `CapturedVariables` in the hit response first: the locals, parameters, and `this` instance fields at the paused line are already there (see Reading CapturedVariables). -3. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. -4. A `single-shot` marker (the default) disarms itself after the hit, so no clear call is required before moving on. Clearing is still what removes the underlying code patch (a disarmed marker leaves the patch installed), so for `continuous`/`trace` markers, or when the method must run fully untouched again, clear it with `uloop clear-pause-point --id "Assets/Scripts/Enemy.cs:42"` (or `--all` to clear every active marker at once) or stop PlayMode. Clearing resumes Play Mode only when the current pause is owned by a pause-point hit — the clear response then carries a `Warning` saying it resumed Play Mode. A manual pause (`control-play-mode --action Pause` or the Editor pause button) is left untouched by clear. +3. Read `CapturedVariables` in the hit response first: the locals, parameters, and `this` instance fields at the paused line are already there (see Reading CapturedVariables). +4. While Unity is still paused, capture any additional evidence with `uloop execute-dynamic-code`, `uloop get-hierarchy`, `uloop find-game-objects`, and one screenshot. +5. A `single-shot` marker (the default) disarms itself after the hit, so no clear call is required before moving on. Clearing is still what removes the underlying code patch (a disarmed marker leaves the patch installed), so for `continuous`/`trace` markers, or when the method must run fully untouched again, clear it with `uloop clear-pause-point --id "Assets/Scripts/Enemy.cs:42"` (or `--all` to clear every active marker at once) or stop PlayMode. Clearing resumes Play Mode only when the current pause is owned by a pause-point hit — the clear response then carries a `Warning` saying it resumed Play Mode. A manual pause (`control-play-mode --action Pause` or the Editor pause button) is left untouched by clear. A hit pauses Unity at the next frame boundary — the patched method and the rest of that frame still run to completion. Only `CapturedVariables` is evidence of the values at the patched line; state read after the pause (for example via `execute-dynamic-code`) may already have advanced past it. -If the game progresses on its own (timers, gravity, spawners), freeze first and arm while paused — see Fast-Progressing Games below. - ## Capture Modes and History Choose the capture mode when enabling a pause point: From f60d10dc1f590ed95f17fa42a12637aae3a5da0e Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 25 Jul 2026 12:15:21 +0900 Subject: [PATCH 2/3] fix: Add early-return branch pattern to pause-point non-firing hint Round 11 Tetris verification hit a rotation pause point that timed out as PAUSE_POINT_EXPIRED because the method exited on its collision- rejection early return (game state had auto-progressed and the board was already blocked). Nothing in the error hinted at another branch, so the reporter had to recompute Board.CanPlace() by hand to find out. The follow-up interview confirmed adding this pattern to the hint would have shortcut that investigation. Add pattern (3) to the shared pausePointNonFiringPatternsHint constant (used by both the timeout and expired hints): the method ran but exited on an earlier branch, so arm a second marker on the early-return line to see which path ran. Update the timeout and expired test expectations to assert the new hint text. --- .../internal/projectrunner/pause_point_errors.go | 12 +++++++----- .../internal/projectrunner/pause_point_wait_test.go | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/cli/project-runner/internal/projectrunner/pause_point_errors.go b/cli/project-runner/internal/projectrunner/pause_point_errors.go index c6bc7e809a..23003500f0 100644 --- a/cli/project-runner/internal/projectrunner/pause_point_errors.go +++ b/cli/project-runner/internal/projectrunner/pause_point_errors.go @@ -67,13 +67,15 @@ const ( pausePointHintPlayModeNotRunning = "PlayMode is not running. Start PlayMode (or trigger the marker code path in Edit Mode), then wait again." pausePointHintEditorAlreadyPaused = "Unity is already paused, so gameplay cannot reach the marker. Resume PlayMode before waiting again." - // Shared by both pausePointTimeoutHint and pausePointExpiredHint: a marker whose method is a - // physics/message callback, or is called from one, can miss a GameObject that already existed - // when the patch was installed even though the method body genuinely ran. Kept as a single - // constant so the two hints stay in sync instead of drifting copies of the same diagnosis. + // Shared by both pausePointTimeoutHint and pausePointExpiredHint: patterns where the method + // body genuinely ran (or was invoked) yet the marker never fired — a physics/message callback + // missing a pre-existing GameObject, a pre-bound delegate bypassing the patch, or control flow + // exiting on an earlier branch before the target line. Kept as a single constant so the two + // hints stay in sync instead of drifting copies of the same diagnosis. pausePointNonFiringPatternsHint = "If the target line never hit despite the trigger firing, check the non-firing patterns: " + "(1) the method is a physics/message callback or is called from one on a GameObject that existed before enable — recreate the GameObject or embed UloopPausePoint.Pause; " + - "(2) the method was already bound into a delegate/event before enable — the pre-bound invocation path bypasses the patch." + "(2) the method was already bound into a delegate/event before enable — the pre-bound invocation path bypasses the patch; " + + "(3) the method ran but exited on an earlier branch (for example a guard rejected the action because game state had already moved on) — arm a second marker on the early-return line to see which path ran." ) // pausePointTimeoutHint maps the final probed status to a deterministic diagnosis, diff --git a/cli/project-runner/internal/projectrunner/pause_point_wait_test.go b/cli/project-runner/internal/projectrunner/pause_point_wait_test.go index 699d87cc64..55eb131e5a 100644 --- a/cli/project-runner/internal/projectrunner/pause_point_wait_test.go +++ b/cli/project-runner/internal/projectrunner/pause_point_wait_test.go @@ -953,7 +953,7 @@ func TestPausePointTimeoutErrorIncludesDiagnosisHint(t *testing.T) { "If the marker targets a Unity message method such as OnCollisionEnter2D/OnTriggerEnter2D, check whether `enable-pause-point`'s response carried a Warning about cached message dispatch: Unity can resolve a GameObject's message dispatch before the marker patch is installed, so a GameObject that already existed at enable time may never reach the marker even though the method body runs. Recreating the GameObject after enabling, or embedding UloopPausePoint.Pause(\"id\") directly in the method body, avoids this. " + "If the target line is inside a very small method, Mono's JIT may have inlined it into callers and the pause point never fires; move the pause point into the calling method. " + "If PlayMode kept progressing on its own while you were arranging state (timers, gravity, spawners), the scenario may have already been consumed before this marker could fire; next time, run `control-play-mode --action Pause` before setup and resume with `control-play-mode --action Play` only after `enable-pause-point` succeeds. " + - "If the target line never hit despite the trigger firing, check the non-firing patterns: (1) the method is a physics/message callback or is called from one on a GameObject that existed before enable — recreate the GameObject or embed UloopPausePoint.Pause; (2) the method was already bound into a delegate/event before enable — the pre-bound invocation path bypasses the patch.", + "If the target line never hit despite the trigger firing, check the non-firing patterns: (1) the method is a physics/message callback or is called from one on a GameObject that existed before enable — recreate the GameObject or embed UloopPausePoint.Pause; (2) the method was already bound into a delegate/event before enable — the pre-bound invocation path bypasses the patch; (3) the method ran but exited on an earlier branch (for example a guard rejected the action because game state had already moved on) — arm a second marker on the early-return line to see which path ran.", }, } @@ -1002,7 +1002,7 @@ func TestPausePointExpiredErrorIncludesDiagnosisHint(t *testing.T) { HitCount: 0, }, wantHint: "Marker expired before it was hit: the enable-pause-point --timeout-seconds window (measured from enable, not from this wait) ran out. Re-enable the marker with a longer --timeout-seconds and trigger the code path again. " + - "If the target line never hit despite the trigger firing, check the non-firing patterns: (1) the method is a physics/message callback or is called from one on a GameObject that existed before enable — recreate the GameObject or embed UloopPausePoint.Pause; (2) the method was already bound into a delegate/event before enable — the pre-bound invocation path bypasses the patch.", + "If the target line never hit despite the trigger firing, check the non-firing patterns: (1) the method is a physics/message callback or is called from one on a GameObject that existed before enable — recreate the GameObject or embed UloopPausePoint.Pause; (2) the method was already bound into a delegate/event before enable — the pre-bound invocation path bypasses the patch; (3) the method ran but exited on an earlier branch (for example a guard rejected the action because game state had already moved on) — arm a second marker on the early-return line to see which path ran.", }, } From 43dc76d0c453ee24c595718e5afa0294d6ab3473 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 25 Jul 2026 12:15:21 +0900 Subject: [PATCH 3/3] fix: Warn about mid-solver Rigidbody values on physics-callback pause points MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 11 block-breaker verification initially misread a captured Rigidbody velocity of (0, 0) inside OnCollisionEnter2D as a physics bug; the value was a mid-solver intermediate (a documented capture constraint) that happened to coexist with a real project bug (missing PhysicsMaterial2D). The follow-up interview showed the existing enable-time physics-callback Warning only covers cached-dispatch hit misses and says nothing about value reliability — what actually helps is stating the discrimination rule in the response itself. Add PhysicalCallbackMidSolverValuesWarning, appended in BuildPatchWarning whenever a physics-callback (direct or one-hop indirect) target is detected: re-read the velocity live after resuming; still zero outside the callback means the game's own physics setup is the suspect. Update existing warning-composition tests for the new ordering (cached-dispatch, mid-solver, inlining) plus presence/absence assertions on the ordinary-method and unrelated-helper cases. --- .../SourcePausePointPatcherTests.cs | 15 ++++++++++----- .../PausePoint/SourcePausePointConstants.cs | 12 ++++++++++++ .../PausePoint/SourcePausePointPatcher.cs | 5 +++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/Assets/Tests/Editor/SourcePausePointPatcher/SourcePausePointPatcherTests.cs b/Assets/Tests/Editor/SourcePausePointPatcher/SourcePausePointPatcherTests.cs index 167c80f357..7ebe320f3f 100644 --- a/Assets/Tests/Editor/SourcePausePointPatcher/SourcePausePointPatcherTests.cs +++ b/Assets/Tests/Editor/SourcePausePointPatcher/SourcePausePointPatcherTests.cs @@ -159,16 +159,17 @@ public void Patch_PhysicsMessageMethodOnMonoBehaviour_ReturnsCachedDispatchWarni Assert.That(patchResult.Success, Is.True); Assert.That(patchResult.Warning, Does.Contain(SourcePausePointConstants.PhysicalCallbackMayMissExistingInstanceWarning)); + Assert.That(patchResult.Warning, Does.Contain(SourcePausePointConstants.PhysicalCallbackMidSolverValuesWarning)); } [Test] public void Patch_PhysicsMessageMethodOnMonoBehaviour_AlsoTriggersInliningWarning() { - // Verifies that when both the physical-callback warning and the small-body - // inlining-risk warning apply to the same method (OnCollisionEnter2D's body is only - // 16 IL bytes, well under SmallMethodInliningRiskThresholdBytes), BuildPatchWarning - // joins them in the same order as the checks appear in its source (physical-callback - // first, then inlining-risk), space-separated. + // Verifies that when the physical-callback warnings and the small-body inlining-risk + // warning all apply to the same method (OnCollisionEnter2D's body is only 16 IL bytes, + // well under SmallMethodInliningRiskThresholdBytes), BuildPatchWarning joins them in + // the same order as the checks appear in its source (cached-dispatch first, then + // mid-solver values, then inlining-risk), space-separated. const string id = "patcher-physical-callback-method-dual-warning"; SourcePausePointResolveResult resolveResult = SourcePausePointResolver.Resolve( FixturesDirectory + "PatcherPhysicalCallbackMethodFixture.cs", 13); @@ -180,6 +181,7 @@ public void Patch_PhysicsMessageMethodOnMonoBehaviour_AlsoTriggersInliningWarnin Assert.That(patchResult.Success, Is.True); Assert.That(patchResult.Warning, Is.EqualTo( SourcePausePointConstants.PhysicalCallbackMayMissExistingInstanceWarning + " " + + SourcePausePointConstants.PhysicalCallbackMidSolverValuesWarning + " " + SourcePausePointConstants.SmallMethodInliningRiskWarning)); } @@ -200,6 +202,7 @@ public void Patch_OrdinaryMessageMethodOnSameMonoBehaviour_DoesNotReturnCachedDi Assert.That(patchResult.Success, Is.True); Assert.That(patchResult.Warning, Does.Not.Contain(SourcePausePointConstants.PhysicalCallbackMayMissExistingInstanceWarning)); + Assert.That(patchResult.Warning, Does.Not.Contain(SourcePausePointConstants.PhysicalCallbackMidSolverValuesWarning)); } [Test] @@ -219,6 +222,7 @@ public void Patch_HelperMethodCalledFromPhysicalCallback_ReturnsIndirectCachedDi Assert.That(patchResult.Success, Is.True); Assert.That(patchResult.Warning, Does.Contain(SourcePausePointConstants.PhysicalCallbackIndirectCallMayMissExistingInstanceWarning)); + Assert.That(patchResult.Warning, Does.Contain(SourcePausePointConstants.PhysicalCallbackMidSolverValuesWarning)); } [Test] @@ -237,6 +241,7 @@ public void Patch_HelperMethodNotCalledFromPhysicalCallback_DoesNotReturnIndirec Assert.That(patchResult.Success, Is.True); Assert.That(patchResult.Warning, Does.Not.Contain(SourcePausePointConstants.PhysicalCallbackIndirectCallMayMissExistingInstanceWarning)); + Assert.That(patchResult.Warning, Does.Not.Contain(SourcePausePointConstants.PhysicalCallbackMidSolverValuesWarning)); } [Test] diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointConstants.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointConstants.cs index a9d91affdd..532bd28bc2 100644 --- a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointConstants.cs +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointConstants.cs @@ -97,6 +97,18 @@ internal static class SourcePausePointConstants + "UloopPausePoint.Pause(\"id\") directly in the method body and arm it with enable-pause-point --id " + "instead."; + // Values captured inside a physics callback can be mid-solver intermediates (a Rigidbody + // velocity may capture as zero even though the body visibly moves). Verification feedback + // showed the existing cached-dispatch warnings say nothing about value reliability, so a + // captured zero gets misread as a physics bug; the response itself must state the + // discrimination rule instead of leaving it documented only in the skill references. + public const string PhysicalCallbackMidSolverValuesWarning = + "Rigidbody velocity values captured inside a physics callback can be mid-solver " + + "intermediates; a captured (0, 0) is not proof the body is stationary. To tell them " + + "apart, re-read the velocity live (execute-dynamic-code) after resuming: if it is " + + "still zero outside the callback, suspect the game's own physics setup rather than " + + "the capture."; + // Surfaces the same JIT-inlining risk documented under Requirements & Safety in the skill, // but at enable time instead of only after a confusing HitCount=0 timeout. public const string SmallMethodInliningRiskWarning = diff --git a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatcher.cs b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatcher.cs index 5c61174f45..f376741578 100644 --- a/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatcher.cs +++ b/Packages/src/Editor/FirstPartyTools/PausePoint/SourcePausePointPatcher.cs @@ -273,6 +273,11 @@ private static (string Warning, bool HasPhysicsCallbackWarning) BuildPatchWarnin hasPhysicsCallbackWarning = true; } + if (hasPhysicsCallbackWarning) + { + warnings.Add(SourcePausePointConstants.PhysicalCallbackMidSolverValuesWarning); + } + if (IsLikelyJitInlined(method)) { warnings.Add(SourcePausePointConstants.SmallMethodInliningRiskWarning);