Skip to content

Commit df96d09

Browse files
committed
docs: Document condition-triggered pause via dynamic-code watcher
Timing-sensitive PlayMode verification (short motions, one-frame effects, state transitions) cannot be captured reliably by sleeping and taking a screenshot. Document the enable -> register watcher -> await -> inspect -> resume pattern: execute-dynamic-code registers an EditorApplication.update watcher that fires UloopPausePoint.Pause(id) on the first frame a runtime condition holds, while await-pause-point does the waiting on the CLI side. - Add the pattern with a full watcher example to the pause-point skill - Add hit-then-Step guidance for pausing right after simulated input and for frame-offset positioning, and warn against race-prone Time.frameCount arithmetic in watchers - Cross-reference the pattern from the execute-dynamic-code skill - Regenerate .claude/.agents copies via `uloop skills install`
1 parent d8e88b5 commit df96d09

6 files changed

Lines changed: 135 additions & 3 deletions

File tree

  • .agents/skills
  • .claude/skills
  • Packages/src/Editor
    • CliOnlyTools~/PausePoint/Skill
    • FirstPartyTools/ExecuteDynamicCode/Skill

.agents/skills/uloop-execute-dynamic-code/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ For basic selected GameObject discovery or property inspection, use `find-game-o
1313

1414
This tool can inspect reachable Unity state, such as GameObjects, components, public properties, static values, and method results. It cannot directly read local variables or intermediate calculations inside an already-running method. When those values matter, enable a source pause point on that line instead (`uloop enable-pause-point --file <file> --line <line>`, see the `uloop-pause-point` skill): the hit response's `CapturedVariables` already contains the locals, parameters, and instance fields at that line, with no code edit or recompile. `CapturedVariables` is a pre-line snapshot; this tool during the pause sees the interrupted method's post-interrupt state instead. While Unity stays paused on the hit, use `UloopPausePoint.TryGetCapturedValue(name)` (plus `GetCapturedNames()` / `GetCapturedPausePointId()`) for live captured references such as collections. Do not try to reconstruct pre-line locals with execute-dynamic-code alone.
1515

16+
The reverse combination also works: to freeze Unity on the first frame where a runtime condition holds (an animation peak, HP reaching zero, an enemy spawning), enable an id-only pause point, then use this tool to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause(id)` when the condition is met, and return immediately. Wait with `uloop await-pause-point` on the CLI side — never poll or sleep inside the snippet itself, because the body runs synchronously on the main thread and frames stop advancing. See "Catching a Runtime Condition with a Dynamic-Code Trigger" in the `uloop-pause-point` skill.
17+
1618
## Parameters
1719

1820
- `--code '<code>'`: Inline C# statements to execute. Use direct statements only; `return` is optional, and `using` directives may appear at the top of the snippet.

.agents/skills/uloop-pause-point/SKILL.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,51 @@ Watch expressions are in-memory Editor state. A domain reload clears them, so re
9494
## Marker Types
9595

9696
- `uloop enable-pause-point --file --line` patches the already-compiled method at a source line. No code edit or recompile is required.
97-
- `UloopPausePoint.Pause(id)` is a hand-written marker call for code paths that file:line patching cannot reach. Pair it with `uloop enable-pause-point --id <id>` (no `--file`/`--line`).
97+
- `UloopPausePoint.Pause(id)` is a hand-written marker call for code paths that file:line patching cannot reach. Pair it with `uloop enable-pause-point --id <id>` (no `--file`/`--line`). The call does not need to live in committed source — a dynamic-code watcher can fire it (see the next section).
9898
- For ordinary file:line debugging you do not need `UloopPausePoint.Pause` in source. Prefer CLI enable when the target line can be patched.
9999

100+
## Catching a Runtime Condition with a Dynamic-Code Trigger
101+
102+
A file:line pause point freezes a specific source line. When the moment you need is defined by a runtime condition instead — an animation passing a normalized time, HP reaching zero, an enemy spawning — combine an id-only marker with `execute-dynamic-code`. Timing-sensitive verification such as short motions or one-frame effects cannot be captured by sleeping and then taking a screenshot; this pattern freezes the first frame where the condition holds, without writing any .cs file.
103+
104+
1. Enable an id-only marker: `uloop enable-pause-point --id hit-peak --timeout-seconds 120` (single-shot by default).
105+
2. Run `uloop execute-dynamic-code` to trigger the action and register a watcher on `EditorApplication.update`, then return immediately. The watcher evaluates the condition every frame; on the first frame it holds, it removes itself and calls `UloopPausePoint.Pause("hit-peak")`.
106+
3. Wait on the CLI side: `uloop await-pause-point --id hit-peak --timeout-seconds 120`.
107+
4. While Unity is paused, collect evidence: `uloop screenshot`, state reads with `execute-dynamic-code`, or `control-play-mode --action Step` frame stepping.
108+
5. Resume with `uloop control-play-mode --action Play`.
109+
110+
Example watcher (freeze when the Hit animation passes 30% of the motion):
111+
112+
```csharp
113+
using UnityEngine;
114+
using UnityEditor;
115+
using io.github.hatayama.UnityCliLoop.Runtime;
116+
Animator animator = GameObject.Find("Zombie").GetComponent<Animator>();
117+
EditorApplication.CallbackFunction watcher = null;
118+
watcher = () =>
119+
{
120+
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0);
121+
if (!state.IsName("Hit") || state.normalizedTime < 0.3f) return;
122+
EditorApplication.update -= watcher;
123+
UloopPausePoint.Pause("hit-peak");
124+
};
125+
EditorApplication.update += watcher;
126+
return "watcher registered";
127+
```
128+
129+
Rules for this pattern:
130+
131+
- The dynamic-code body runs synchronously on the main thread. Never poll or sleep inside the snippet — frames stop advancing and the animation freezes with them. Register the watcher and return; the waiting belongs to `await-pause-point`.
132+
- The watcher must unsubscribe itself from `EditorApplication.update` when it fires. A leaked delegate keeps running until the next domain reload.
133+
- `UloopPausePoint.Pause(id)` is a public static Runtime API, and dynamic code compiles against the project's assemblies, so the watcher can call it exactly like game code. It fires only while the same id is enabled; otherwise it is a no-op, so a stray watcher cannot pause Unity unexpectedly.
134+
- A single-shot marker disarms after the first hit. To catch repeated occurrences, enable with `--mode continuous` and run `await-pause-point` again after each resume.
135+
136+
## Pausing Right After Simulated Input, Plus N Frames
137+
138+
To freeze the frame where a `simulate-mouse-ui` click or a `simulate-keyboard` key press lands, you do not need a watcher: enable a file:line pause point on the input-consuming line before sending the input. When the pause lands mid-command, the `simulate-*` command returns promptly with `InterruptedByPausePoint=true` (see Line Placement).
139+
140+
For "N frames after the input" (for example, three frames after a key press), advance from that hit with `control-play-mode --action Step` exactly N times — `Step` works right after a hit. Do not compute frame offsets in a dynamic-code watcher (recording `Time.frameCount` and pausing at `recorded + N`): frames keep advancing between CLI commands, so the recorded baseline is race-prone and the pause lands on an unpredictable frame. Reserve the watcher pattern for condition-defined moments; use hit-then-Step for frame-offset positioning.
141+
100142
## Hit Preconditions
101143

102144
A pause point hits only when control flow reaches the patched line (or the `Pause(id)` call). `simulate-keyboard` returning `PressEdgeObserved=true` means the input edge was observed, not that your target game logic has reached the pause line yet.

.claude/skills/uloop-execute-dynamic-code/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ For basic selected GameObject discovery or property inspection, use `find-game-o
1313

1414
This tool can inspect reachable Unity state, such as GameObjects, components, public properties, static values, and method results. It cannot directly read local variables or intermediate calculations inside an already-running method. When those values matter, enable a source pause point on that line instead (`uloop enable-pause-point --file <file> --line <line>`, see the `uloop-pause-point` skill): the hit response's `CapturedVariables` already contains the locals, parameters, and instance fields at that line, with no code edit or recompile. `CapturedVariables` is a pre-line snapshot; this tool during the pause sees the interrupted method's post-interrupt state instead. While Unity stays paused on the hit, use `UloopPausePoint.TryGetCapturedValue(name)` (plus `GetCapturedNames()` / `GetCapturedPausePointId()`) for live captured references such as collections. Do not try to reconstruct pre-line locals with execute-dynamic-code alone.
1515

16+
The reverse combination also works: to freeze Unity on the first frame where a runtime condition holds (an animation peak, HP reaching zero, an enemy spawning), enable an id-only pause point, then use this tool to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause(id)` when the condition is met, and return immediately. Wait with `uloop await-pause-point` on the CLI side — never poll or sleep inside the snippet itself, because the body runs synchronously on the main thread and frames stop advancing. See "Catching a Runtime Condition with a Dynamic-Code Trigger" in the `uloop-pause-point` skill.
17+
1618
## Parameters
1719

1820
- `--code '<code>'`: Inline C# statements to execute. Use direct statements only; `return` is optional, and `using` directives may appear at the top of the snippet.

.claude/skills/uloop-pause-point/SKILL.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,51 @@ Watch expressions are in-memory Editor state. A domain reload clears them, so re
9494
## Marker Types
9595

9696
- `uloop enable-pause-point --file --line` patches the already-compiled method at a source line. No code edit or recompile is required.
97-
- `UloopPausePoint.Pause(id)` is a hand-written marker call for code paths that file:line patching cannot reach. Pair it with `uloop enable-pause-point --id <id>` (no `--file`/`--line`).
97+
- `UloopPausePoint.Pause(id)` is a hand-written marker call for code paths that file:line patching cannot reach. Pair it with `uloop enable-pause-point --id <id>` (no `--file`/`--line`). The call does not need to live in committed source — a dynamic-code watcher can fire it (see the next section).
9898
- For ordinary file:line debugging you do not need `UloopPausePoint.Pause` in source. Prefer CLI enable when the target line can be patched.
9999

100+
## Catching a Runtime Condition with a Dynamic-Code Trigger
101+
102+
A file:line pause point freezes a specific source line. When the moment you need is defined by a runtime condition instead — an animation passing a normalized time, HP reaching zero, an enemy spawning — combine an id-only marker with `execute-dynamic-code`. Timing-sensitive verification such as short motions or one-frame effects cannot be captured by sleeping and then taking a screenshot; this pattern freezes the first frame where the condition holds, without writing any .cs file.
103+
104+
1. Enable an id-only marker: `uloop enable-pause-point --id hit-peak --timeout-seconds 120` (single-shot by default).
105+
2. Run `uloop execute-dynamic-code` to trigger the action and register a watcher on `EditorApplication.update`, then return immediately. The watcher evaluates the condition every frame; on the first frame it holds, it removes itself and calls `UloopPausePoint.Pause("hit-peak")`.
106+
3. Wait on the CLI side: `uloop await-pause-point --id hit-peak --timeout-seconds 120`.
107+
4. While Unity is paused, collect evidence: `uloop screenshot`, state reads with `execute-dynamic-code`, or `control-play-mode --action Step` frame stepping.
108+
5. Resume with `uloop control-play-mode --action Play`.
109+
110+
Example watcher (freeze when the Hit animation passes 30% of the motion):
111+
112+
```csharp
113+
using UnityEngine;
114+
using UnityEditor;
115+
using io.github.hatayama.UnityCliLoop.Runtime;
116+
Animator animator = GameObject.Find("Zombie").GetComponent<Animator>();
117+
EditorApplication.CallbackFunction watcher = null;
118+
watcher = () =>
119+
{
120+
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0);
121+
if (!state.IsName("Hit") || state.normalizedTime < 0.3f) return;
122+
EditorApplication.update -= watcher;
123+
UloopPausePoint.Pause("hit-peak");
124+
};
125+
EditorApplication.update += watcher;
126+
return "watcher registered";
127+
```
128+
129+
Rules for this pattern:
130+
131+
- The dynamic-code body runs synchronously on the main thread. Never poll or sleep inside the snippet — frames stop advancing and the animation freezes with them. Register the watcher and return; the waiting belongs to `await-pause-point`.
132+
- The watcher must unsubscribe itself from `EditorApplication.update` when it fires. A leaked delegate keeps running until the next domain reload.
133+
- `UloopPausePoint.Pause(id)` is a public static Runtime API, and dynamic code compiles against the project's assemblies, so the watcher can call it exactly like game code. It fires only while the same id is enabled; otherwise it is a no-op, so a stray watcher cannot pause Unity unexpectedly.
134+
- A single-shot marker disarms after the first hit. To catch repeated occurrences, enable with `--mode continuous` and run `await-pause-point` again after each resume.
135+
136+
## Pausing Right After Simulated Input, Plus N Frames
137+
138+
To freeze the frame where a `simulate-mouse-ui` click or a `simulate-keyboard` key press lands, you do not need a watcher: enable a file:line pause point on the input-consuming line before sending the input. When the pause lands mid-command, the `simulate-*` command returns promptly with `InterruptedByPausePoint=true` (see Line Placement).
139+
140+
For "N frames after the input" (for example, three frames after a key press), advance from that hit with `control-play-mode --action Step` exactly N times — `Step` works right after a hit. Do not compute frame offsets in a dynamic-code watcher (recording `Time.frameCount` and pausing at `recorded + N`): frames keep advancing between CLI commands, so the recorded baseline is race-prone and the pause lands on an unpredictable frame. Reserve the watcher pattern for condition-defined moments; use hit-then-Step for frame-offset positioning.
141+
100142
## Hit Preconditions
101143

102144
A pause point hits only when control flow reaches the patched line (or the `Pause(id)` call). `simulate-keyboard` returning `PressEdgeObserved=true` means the input edge was observed, not that your target game logic has reached the pause line yet.

Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,51 @@ Watch expressions are in-memory Editor state. A domain reload clears them, so re
9494
## Marker Types
9595

9696
- `uloop enable-pause-point --file --line` patches the already-compiled method at a source line. No code edit or recompile is required.
97-
- `UloopPausePoint.Pause(id)` is a hand-written marker call for code paths that file:line patching cannot reach. Pair it with `uloop enable-pause-point --id <id>` (no `--file`/`--line`).
97+
- `UloopPausePoint.Pause(id)` is a hand-written marker call for code paths that file:line patching cannot reach. Pair it with `uloop enable-pause-point --id <id>` (no `--file`/`--line`). The call does not need to live in committed source — a dynamic-code watcher can fire it (see the next section).
9898
- For ordinary file:line debugging you do not need `UloopPausePoint.Pause` in source. Prefer CLI enable when the target line can be patched.
9999

100+
## Catching a Runtime Condition with a Dynamic-Code Trigger
101+
102+
A file:line pause point freezes a specific source line. When the moment you need is defined by a runtime condition instead — an animation passing a normalized time, HP reaching zero, an enemy spawning — combine an id-only marker with `execute-dynamic-code`. Timing-sensitive verification such as short motions or one-frame effects cannot be captured by sleeping and then taking a screenshot; this pattern freezes the first frame where the condition holds, without writing any .cs file.
103+
104+
1. Enable an id-only marker: `uloop enable-pause-point --id hit-peak --timeout-seconds 120` (single-shot by default).
105+
2. Run `uloop execute-dynamic-code` to trigger the action and register a watcher on `EditorApplication.update`, then return immediately. The watcher evaluates the condition every frame; on the first frame it holds, it removes itself and calls `UloopPausePoint.Pause("hit-peak")`.
106+
3. Wait on the CLI side: `uloop await-pause-point --id hit-peak --timeout-seconds 120`.
107+
4. While Unity is paused, collect evidence: `uloop screenshot`, state reads with `execute-dynamic-code`, or `control-play-mode --action Step` frame stepping.
108+
5. Resume with `uloop control-play-mode --action Play`.
109+
110+
Example watcher (freeze when the Hit animation passes 30% of the motion):
111+
112+
```csharp
113+
using UnityEngine;
114+
using UnityEditor;
115+
using io.github.hatayama.UnityCliLoop.Runtime;
116+
Animator animator = GameObject.Find("Zombie").GetComponent<Animator>();
117+
EditorApplication.CallbackFunction watcher = null;
118+
watcher = () =>
119+
{
120+
AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0);
121+
if (!state.IsName("Hit") || state.normalizedTime < 0.3f) return;
122+
EditorApplication.update -= watcher;
123+
UloopPausePoint.Pause("hit-peak");
124+
};
125+
EditorApplication.update += watcher;
126+
return "watcher registered";
127+
```
128+
129+
Rules for this pattern:
130+
131+
- The dynamic-code body runs synchronously on the main thread. Never poll or sleep inside the snippet — frames stop advancing and the animation freezes with them. Register the watcher and return; the waiting belongs to `await-pause-point`.
132+
- The watcher must unsubscribe itself from `EditorApplication.update` when it fires. A leaked delegate keeps running until the next domain reload.
133+
- `UloopPausePoint.Pause(id)` is a public static Runtime API, and dynamic code compiles against the project's assemblies, so the watcher can call it exactly like game code. It fires only while the same id is enabled; otherwise it is a no-op, so a stray watcher cannot pause Unity unexpectedly.
134+
- A single-shot marker disarms after the first hit. To catch repeated occurrences, enable with `--mode continuous` and run `await-pause-point` again after each resume.
135+
136+
## Pausing Right After Simulated Input, Plus N Frames
137+
138+
To freeze the frame where a `simulate-mouse-ui` click or a `simulate-keyboard` key press lands, you do not need a watcher: enable a file:line pause point on the input-consuming line before sending the input. When the pause lands mid-command, the `simulate-*` command returns promptly with `InterruptedByPausePoint=true` (see Line Placement).
139+
140+
For "N frames after the input" (for example, three frames after a key press), advance from that hit with `control-play-mode --action Step` exactly N times — `Step` works right after a hit. Do not compute frame offsets in a dynamic-code watcher (recording `Time.frameCount` and pausing at `recorded + N`): frames keep advancing between CLI commands, so the recorded baseline is race-prone and the pause lands on an unpredictable frame. Reserve the watcher pattern for condition-defined moments; use hit-then-Step for frame-offset positioning.
141+
100142
## Hit Preconditions
101143

102144
A pause point hits only when control flow reaches the patched line (or the `Pause(id)` call). `simulate-keyboard` returning `PressEdgeObserved=true` means the input edge was observed, not that your target game logic has reached the pause line yet.

Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ For basic selected GameObject discovery or property inspection, use `find-game-o
1313

1414
This tool can inspect reachable Unity state, such as GameObjects, components, public properties, static values, and method results. It cannot directly read local variables or intermediate calculations inside an already-running method. When those values matter, enable a source pause point on that line instead (`uloop enable-pause-point --file <file> --line <line>`, see the `uloop-pause-point` skill): the hit response's `CapturedVariables` already contains the locals, parameters, and instance fields at that line, with no code edit or recompile. `CapturedVariables` is a pre-line snapshot; this tool during the pause sees the interrupted method's post-interrupt state instead. While Unity stays paused on the hit, use `UloopPausePoint.TryGetCapturedValue(name)` (plus `GetCapturedNames()` / `GetCapturedPausePointId()`) for live captured references such as collections. Do not try to reconstruct pre-line locals with execute-dynamic-code alone.
1515

16+
The reverse combination also works: to freeze Unity on the first frame where a runtime condition holds (an animation peak, HP reaching zero, an enemy spawning), enable an id-only pause point, then use this tool to register an `EditorApplication.update` watcher that calls `UloopPausePoint.Pause(id)` when the condition is met, and return immediately. Wait with `uloop await-pause-point` on the CLI side — never poll or sleep inside the snippet itself, because the body runs synchronously on the main thread and frames stop advancing. See "Catching a Runtime Condition with a Dynamic-Code Trigger" in the `uloop-pause-point` skill.
17+
1618
## Parameters
1719

1820
- `--code '<code>'`: Inline C# statements to execute. Use direct statements only; `return` is optional, and `using` directives may appear at the top of the snippet.

0 commit comments

Comments
 (0)