Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions .agents/skills/uloop-pause-point/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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:
Expand Down
11 changes: 5 additions & 6 deletions .claude/skills/uloop-pause-point/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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));
}

Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand Down
11 changes: 5 additions & 6 deletions Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading