From d8e88b56e5d8a06a03a8a2651d42947cb4386801 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sat, 18 Jul 2026 23:11:49 +0900 Subject: [PATCH 1/3] docs: Discourage --force-recompile in the compile skill guide AI agents tend to reach for --force-recompile "just in case" after external file edits, but incremental change detection is Unity's job and a plain compile already runs every required recompilation. On large projects the forced full rebuild freezes the Editor for a long time, usually returns COMPILE_RESULT_UNKNOWN across the domain reload, and puts the Editor into the unstable just-after-reload state. Document the few legitimate use cases and make plain `uloop compile` the default guidance. --- .agents/skills/uloop-compile/SKILL.md | 19 ++++++++++++++++++- .claude/skills/uloop-compile/SKILL.md | 19 ++++++++++++++++++- .../FirstPartyTools/Compile/Skill/SKILL.md | 19 ++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/.agents/skills/uloop-compile/SKILL.md b/.agents/skills/uloop-compile/SKILL.md index 9aaba36ce4..108abc5a84 100644 --- a/.agents/skills/uloop-compile/SKILL.md +++ b/.agents/skills/uloop-compile/SKILL.md @@ -18,10 +18,27 @@ uloop compile [--force-recompile] [--no-wait-for-domain-reload] [--stop-on-exter | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `--force-recompile` | flag | - | Use for broader validation, including warnings hidden by other asmdefs; much slower than normal compile | +| `--force-recompile` | flag | - | Full recompile plus domain reload. Rarely needed — see "When to use --force-recompile" below | | `--no-wait-for-domain-reload` | flag | - | Return before Domain Reload completion | | `--stop-on-external-scene-changes` | flag | - | Stop before compilation if open Scene files changed externally instead of auto-reloading them | +## When to use --force-recompile + +`--force-recompile` is almost never needed. Detecting changed files is Unity's job: even when +files were edited outside the Editor, a plain `uloop compile` refreshes assets and runs every +recompilation the changes require. "The files were changed externally, so recompile everything +just in case" is not a valid reason. + +Why to avoid it: + +- On large projects a full recompile plus domain reload can freeze Unity for a long time. +- The result crosses a domain reload, so it often comes back as `COMPILE_RESULT_UNKNOWN` and + does not work as a verification step. +- It puts the Editor into the unstable just-after-reload state for no benefit. + +The one legitimate use case: you need warnings hidden by other asmdefs surfaced by a full +build. Otherwise always run plain `uloop compile`. + ## Output Returns JSON: diff --git a/.claude/skills/uloop-compile/SKILL.md b/.claude/skills/uloop-compile/SKILL.md index 9aaba36ce4..108abc5a84 100644 --- a/.claude/skills/uloop-compile/SKILL.md +++ b/.claude/skills/uloop-compile/SKILL.md @@ -18,10 +18,27 @@ uloop compile [--force-recompile] [--no-wait-for-domain-reload] [--stop-on-exter | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `--force-recompile` | flag | - | Use for broader validation, including warnings hidden by other asmdefs; much slower than normal compile | +| `--force-recompile` | flag | - | Full recompile plus domain reload. Rarely needed — see "When to use --force-recompile" below | | `--no-wait-for-domain-reload` | flag | - | Return before Domain Reload completion | | `--stop-on-external-scene-changes` | flag | - | Stop before compilation if open Scene files changed externally instead of auto-reloading them | +## When to use --force-recompile + +`--force-recompile` is almost never needed. Detecting changed files is Unity's job: even when +files were edited outside the Editor, a plain `uloop compile` refreshes assets and runs every +recompilation the changes require. "The files were changed externally, so recompile everything +just in case" is not a valid reason. + +Why to avoid it: + +- On large projects a full recompile plus domain reload can freeze Unity for a long time. +- The result crosses a domain reload, so it often comes back as `COMPILE_RESULT_UNKNOWN` and + does not work as a verification step. +- It puts the Editor into the unstable just-after-reload state for no benefit. + +The one legitimate use case: you need warnings hidden by other asmdefs surfaced by a full +build. Otherwise always run plain `uloop compile`. + ## Output Returns JSON: diff --git a/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md index 9aaba36ce4..108abc5a84 100644 --- a/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/Compile/Skill/SKILL.md @@ -18,10 +18,27 @@ uloop compile [--force-recompile] [--no-wait-for-domain-reload] [--stop-on-exter | Parameter | Type | Default | Description | |-----------|------|---------|-------------| -| `--force-recompile` | flag | - | Use for broader validation, including warnings hidden by other asmdefs; much slower than normal compile | +| `--force-recompile` | flag | - | Full recompile plus domain reload. Rarely needed — see "When to use --force-recompile" below | | `--no-wait-for-domain-reload` | flag | - | Return before Domain Reload completion | | `--stop-on-external-scene-changes` | flag | - | Stop before compilation if open Scene files changed externally instead of auto-reloading them | +## When to use --force-recompile + +`--force-recompile` is almost never needed. Detecting changed files is Unity's job: even when +files were edited outside the Editor, a plain `uloop compile` refreshes assets and runs every +recompilation the changes require. "The files were changed externally, so recompile everything +just in case" is not a valid reason. + +Why to avoid it: + +- On large projects a full recompile plus domain reload can freeze Unity for a long time. +- The result crosses a domain reload, so it often comes back as `COMPILE_RESULT_UNKNOWN` and + does not work as a verification step. +- It puts the Editor into the unstable just-after-reload state for no benefit. + +The one legitimate use case: you need warnings hidden by other asmdefs surfaced by a full +build. Otherwise always run plain `uloop compile`. + ## Output Returns JSON: From df96d090db383cc25cc3bb770944189267af8978 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sun, 19 Jul 2026 02:31:05 +0900 Subject: [PATCH 2/3] 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` --- .../uloop-execute-dynamic-code/SKILL.md | 2 + .agents/skills/uloop-pause-point/SKILL.md | 44 ++++++++++++++++++- .../uloop-execute-dynamic-code/SKILL.md | 2 + .claude/skills/uloop-pause-point/SKILL.md | 44 ++++++++++++++++++- .../CliOnlyTools~/PausePoint/Skill/SKILL.md | 44 ++++++++++++++++++- .../ExecuteDynamicCode/Skill/SKILL.md | 2 + 6 files changed, 135 insertions(+), 3 deletions(-) diff --git a/.agents/skills/uloop-execute-dynamic-code/SKILL.md b/.agents/skills/uloop-execute-dynamic-code/SKILL.md index 42208832b9..9831d85fa2 100644 --- a/.agents/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.agents/skills/uloop-execute-dynamic-code/SKILL.md @@ -13,6 +13,8 @@ For basic selected GameObject discovery or property inspection, use `find-game-o 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 --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. +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. + ## Parameters - `--code ''`: Inline C# statements to execute. Use direct statements only; `return` is optional, and `using` directives may appear at the top of the snippet. diff --git a/.agents/skills/uloop-pause-point/SKILL.md b/.agents/skills/uloop-pause-point/SKILL.md index 433843c0b1..959b5135b7 100644 --- a/.agents/skills/uloop-pause-point/SKILL.md +++ b/.agents/skills/uloop-pause-point/SKILL.md @@ -94,9 +94,51 @@ Watch expressions are in-memory Editor state. A domain reload clears them, so re ## Marker Types - `uloop enable-pause-point --file --line` patches the already-compiled method at a source line. No code edit or recompile is required. -- `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 ` (no `--file`/`--line`). +- `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 ` (no `--file`/`--line`). The call does not need to live in committed source — a dynamic-code watcher can fire it (see the next section). - For ordinary file:line debugging you do not need `UloopPausePoint.Pause` in source. Prefer CLI enable when the target line can be patched. +## Catching a Runtime Condition with a Dynamic-Code Trigger + +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. + +1. Enable an id-only marker: `uloop enable-pause-point --id hit-peak --timeout-seconds 120` (single-shot by default). +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")`. +3. Wait on the CLI side: `uloop await-pause-point --id hit-peak --timeout-seconds 120`. +4. While Unity is paused, collect evidence: `uloop screenshot`, state reads with `execute-dynamic-code`, or `control-play-mode --action Step` frame stepping. +5. Resume with `uloop control-play-mode --action Play`. + +Example watcher (freeze when the Hit animation passes 30% of the motion): + +```csharp +using UnityEngine; +using UnityEditor; +using io.github.hatayama.UnityCliLoop.Runtime; +Animator animator = GameObject.Find("Zombie").GetComponent(); +EditorApplication.CallbackFunction watcher = null; +watcher = () => +{ + AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); + if (!state.IsName("Hit") || state.normalizedTime < 0.3f) return; + EditorApplication.update -= watcher; + UloopPausePoint.Pause("hit-peak"); +}; +EditorApplication.update += watcher; +return "watcher registered"; +``` + +Rules for this pattern: + +- 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`. +- The watcher must unsubscribe itself from `EditorApplication.update` when it fires. A leaked delegate keeps running until the next domain reload. +- `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. +- 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. + +## Pausing Right After Simulated Input, Plus N Frames + +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). + +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. + ## Hit Preconditions 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. diff --git a/.claude/skills/uloop-execute-dynamic-code/SKILL.md b/.claude/skills/uloop-execute-dynamic-code/SKILL.md index 42208832b9..9831d85fa2 100644 --- a/.claude/skills/uloop-execute-dynamic-code/SKILL.md +++ b/.claude/skills/uloop-execute-dynamic-code/SKILL.md @@ -13,6 +13,8 @@ For basic selected GameObject discovery or property inspection, use `find-game-o 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 --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. +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. + ## Parameters - `--code ''`: Inline C# statements to execute. Use direct statements only; `return` is optional, and `using` directives may appear at the top of the snippet. diff --git a/.claude/skills/uloop-pause-point/SKILL.md b/.claude/skills/uloop-pause-point/SKILL.md index 433843c0b1..959b5135b7 100644 --- a/.claude/skills/uloop-pause-point/SKILL.md +++ b/.claude/skills/uloop-pause-point/SKILL.md @@ -94,9 +94,51 @@ Watch expressions are in-memory Editor state. A domain reload clears them, so re ## Marker Types - `uloop enable-pause-point --file --line` patches the already-compiled method at a source line. No code edit or recompile is required. -- `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 ` (no `--file`/`--line`). +- `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 ` (no `--file`/`--line`). The call does not need to live in committed source — a dynamic-code watcher can fire it (see the next section). - For ordinary file:line debugging you do not need `UloopPausePoint.Pause` in source. Prefer CLI enable when the target line can be patched. +## Catching a Runtime Condition with a Dynamic-Code Trigger + +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. + +1. Enable an id-only marker: `uloop enable-pause-point --id hit-peak --timeout-seconds 120` (single-shot by default). +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")`. +3. Wait on the CLI side: `uloop await-pause-point --id hit-peak --timeout-seconds 120`. +4. While Unity is paused, collect evidence: `uloop screenshot`, state reads with `execute-dynamic-code`, or `control-play-mode --action Step` frame stepping. +5. Resume with `uloop control-play-mode --action Play`. + +Example watcher (freeze when the Hit animation passes 30% of the motion): + +```csharp +using UnityEngine; +using UnityEditor; +using io.github.hatayama.UnityCliLoop.Runtime; +Animator animator = GameObject.Find("Zombie").GetComponent(); +EditorApplication.CallbackFunction watcher = null; +watcher = () => +{ + AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); + if (!state.IsName("Hit") || state.normalizedTime < 0.3f) return; + EditorApplication.update -= watcher; + UloopPausePoint.Pause("hit-peak"); +}; +EditorApplication.update += watcher; +return "watcher registered"; +``` + +Rules for this pattern: + +- 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`. +- The watcher must unsubscribe itself from `EditorApplication.update` when it fires. A leaked delegate keeps running until the next domain reload. +- `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. +- 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. + +## Pausing Right After Simulated Input, Plus N Frames + +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). + +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. + ## Hit Preconditions 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. diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md index 433843c0b1..959b5135b7 100644 --- a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md @@ -94,9 +94,51 @@ Watch expressions are in-memory Editor state. A domain reload clears them, so re ## Marker Types - `uloop enable-pause-point --file --line` patches the already-compiled method at a source line. No code edit or recompile is required. -- `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 ` (no `--file`/`--line`). +- `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 ` (no `--file`/`--line`). The call does not need to live in committed source — a dynamic-code watcher can fire it (see the next section). - For ordinary file:line debugging you do not need `UloopPausePoint.Pause` in source. Prefer CLI enable when the target line can be patched. +## Catching a Runtime Condition with a Dynamic-Code Trigger + +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. + +1. Enable an id-only marker: `uloop enable-pause-point --id hit-peak --timeout-seconds 120` (single-shot by default). +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")`. +3. Wait on the CLI side: `uloop await-pause-point --id hit-peak --timeout-seconds 120`. +4. While Unity is paused, collect evidence: `uloop screenshot`, state reads with `execute-dynamic-code`, or `control-play-mode --action Step` frame stepping. +5. Resume with `uloop control-play-mode --action Play`. + +Example watcher (freeze when the Hit animation passes 30% of the motion): + +```csharp +using UnityEngine; +using UnityEditor; +using io.github.hatayama.UnityCliLoop.Runtime; +Animator animator = GameObject.Find("Zombie").GetComponent(); +EditorApplication.CallbackFunction watcher = null; +watcher = () => +{ + AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); + if (!state.IsName("Hit") || state.normalizedTime < 0.3f) return; + EditorApplication.update -= watcher; + UloopPausePoint.Pause("hit-peak"); +}; +EditorApplication.update += watcher; +return "watcher registered"; +``` + +Rules for this pattern: + +- 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`. +- The watcher must unsubscribe itself from `EditorApplication.update` when it fires. A leaked delegate keeps running until the next domain reload. +- `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. +- 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. + +## Pausing Right After Simulated Input, Plus N Frames + +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). + +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. + ## Hit Preconditions 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. diff --git a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md index 42208832b9..9831d85fa2 100644 --- a/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md +++ b/Packages/src/Editor/FirstPartyTools/ExecuteDynamicCode/Skill/SKILL.md @@ -13,6 +13,8 @@ For basic selected GameObject discovery or property inspection, use `find-game-o 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 --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. +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. + ## Parameters - `--code ''`: Inline C# statements to execute. Use direct statements only; `return` is optional, and `using` directives may appear at the top of the snippet. From e75cc88f580206199f4d4de1dfcb286acee434a1 Mon Sep 17 00:00:00 2001 From: hatayama Date: Sun, 19 Jul 2026 03:14:28 +0900 Subject: [PATCH 3/3] docs: Add deadline cleanup to pause-point watcher example The watcher example only unsubscribed from EditorApplication.update on the hit path, so a condition that never holds would leak the delegate until the next domain reload. Add a deadline guard matching the marker's --timeout-seconds and update the rules bullet accordingly. Mirrored copies under .agents/ and .claude/ are synced byte-identically. Addresses CodeRabbit review feedback on PR #1836. --- .agents/skills/uloop-pause-point/SKILL.md | 9 ++++++++- .claude/skills/uloop-pause-point/SKILL.md | 9 ++++++++- .../src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md | 9 ++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/.agents/skills/uloop-pause-point/SKILL.md b/.agents/skills/uloop-pause-point/SKILL.md index 959b5135b7..be0e3f4c4d 100644 --- a/.agents/skills/uloop-pause-point/SKILL.md +++ b/.agents/skills/uloop-pause-point/SKILL.md @@ -114,9 +114,16 @@ using UnityEngine; using UnityEditor; using io.github.hatayama.UnityCliLoop.Runtime; Animator animator = GameObject.Find("Zombie").GetComponent(); +// Match the marker's --timeout-seconds so an unmet condition cannot leak the delegate +double deadline = EditorApplication.timeSinceStartup + 120d; EditorApplication.CallbackFunction watcher = null; watcher = () => { + if (EditorApplication.timeSinceStartup > deadline) + { + EditorApplication.update -= watcher; + return; + } AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); if (!state.IsName("Hit") || state.normalizedTime < 0.3f) return; EditorApplication.update -= watcher; @@ -129,7 +136,7 @@ return "watcher registered"; Rules for this pattern: - 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`. -- The watcher must unsubscribe itself from `EditorApplication.update` when it fires. A leaked delegate keeps running until the next domain reload. +- The watcher must unsubscribe itself from `EditorApplication.update` when it fires, and also on a deadline in case the condition never holds — a leaked delegate keeps running until the next domain reload. Match the deadline to the marker's `--timeout-seconds`. - `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. - 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. diff --git a/.claude/skills/uloop-pause-point/SKILL.md b/.claude/skills/uloop-pause-point/SKILL.md index 959b5135b7..be0e3f4c4d 100644 --- a/.claude/skills/uloop-pause-point/SKILL.md +++ b/.claude/skills/uloop-pause-point/SKILL.md @@ -114,9 +114,16 @@ using UnityEngine; using UnityEditor; using io.github.hatayama.UnityCliLoop.Runtime; Animator animator = GameObject.Find("Zombie").GetComponent(); +// Match the marker's --timeout-seconds so an unmet condition cannot leak the delegate +double deadline = EditorApplication.timeSinceStartup + 120d; EditorApplication.CallbackFunction watcher = null; watcher = () => { + if (EditorApplication.timeSinceStartup > deadline) + { + EditorApplication.update -= watcher; + return; + } AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); if (!state.IsName("Hit") || state.normalizedTime < 0.3f) return; EditorApplication.update -= watcher; @@ -129,7 +136,7 @@ return "watcher registered"; Rules for this pattern: - 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`. -- The watcher must unsubscribe itself from `EditorApplication.update` when it fires. A leaked delegate keeps running until the next domain reload. +- The watcher must unsubscribe itself from `EditorApplication.update` when it fires, and also on a deadline in case the condition never holds — a leaked delegate keeps running until the next domain reload. Match the deadline to the marker's `--timeout-seconds`. - `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. - 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. diff --git a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md index 959b5135b7..be0e3f4c4d 100644 --- a/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md +++ b/Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md @@ -114,9 +114,16 @@ using UnityEngine; using UnityEditor; using io.github.hatayama.UnityCliLoop.Runtime; Animator animator = GameObject.Find("Zombie").GetComponent(); +// Match the marker's --timeout-seconds so an unmet condition cannot leak the delegate +double deadline = EditorApplication.timeSinceStartup + 120d; EditorApplication.CallbackFunction watcher = null; watcher = () => { + if (EditorApplication.timeSinceStartup > deadline) + { + EditorApplication.update -= watcher; + return; + } AnimatorStateInfo state = animator.GetCurrentAnimatorStateInfo(0); if (!state.IsName("Hit") || state.normalizedTime < 0.3f) return; EditorApplication.update -= watcher; @@ -129,7 +136,7 @@ return "watcher registered"; Rules for this pattern: - 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`. -- The watcher must unsubscribe itself from `EditorApplication.update` when it fires. A leaked delegate keeps running until the next domain reload. +- The watcher must unsubscribe itself from `EditorApplication.update` when it fires, and also on a deadline in case the condition never holds — a leaked delegate keeps running until the next domain reload. Match the deadline to the marker's `--timeout-seconds`. - `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. - 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.