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
15 changes: 15 additions & 0 deletions .agents/skills/uloop-pause-point/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@ While Unity is paused on a hit, `execute-dynamic-code` can read live captured re

The holder clears when Unity resumes (not when you `Step` while still paused), when the matching pause point is cleared, when a new hit replaces the snapshot, or when PlayMode exits. After resume, `TryGetCapturedValue` returns `Found=false`.

## Watch Expressions

Use watch expressions when the value should be evaluated automatically after each paused Play Mode Step:

```bash
uloop enable-watch --id "speed" --expression "UloopPausePoint.TryGetCapturedValue(\"speed\").Value" --max-history 20
uloop get-watch-values --id "speed"
```

`enable-watch` compiles the C# expression once, evaluates it immediately for a baseline, and then evaluates it once per changed `Time.frameCount` while Unity is both playing and paused. Multiple watches run in registration order. `enable-watch` rejects a duplicate id instead of overwriting; clear with `clear-watch --id <id>` before re-registering a changed expression. `clear-watch --id <id>` removes one watch; `clear-watch --all` removes all watches. `get-watch-values` without `--id` returns every registered watch.

The expression may use `UloopPausePoint.TryGetCapturedValue("name")` to inspect the latest raw pause-point capture while paused. Each history entry includes the frame and either a stringified value or an explicit error type and message. A throwing expression is recorded as an error and does not stop the Editor update loop. `--max-history` accepts 1 through 100 and drops the oldest entries after the limit.

Watch expressions are in-memory Editor state. A domain reload clears them, so re-register them after `uloop compile`, script recompilation, or an Editor restart. For reliable per-Step changes, keep the expression attached to a continuous pause point on an `Update` or `FixedUpdate` line and use `control-play-mode --action Step`.

## Marker Types

- `uloop enable-pause-point --file --line` patches the already-compiled method at a source line. No code edit or recompile is required.
Expand Down
15 changes: 15 additions & 0 deletions .claude/skills/uloop-pause-point/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,21 @@ While Unity is paused on a hit, `execute-dynamic-code` can read live captured re

The holder clears when Unity resumes (not when you `Step` while still paused), when the matching pause point is cleared, when a new hit replaces the snapshot, or when PlayMode exits. After resume, `TryGetCapturedValue` returns `Found=false`.

## Watch Expressions

Use watch expressions when the value should be evaluated automatically after each paused Play Mode Step:

```bash
uloop enable-watch --id "speed" --expression "UloopPausePoint.TryGetCapturedValue(\"speed\").Value" --max-history 20
uloop get-watch-values --id "speed"
```

`enable-watch` compiles the C# expression once, evaluates it immediately for a baseline, and then evaluates it once per changed `Time.frameCount` while Unity is both playing and paused. Multiple watches run in registration order. `enable-watch` rejects a duplicate id instead of overwriting; clear with `clear-watch --id <id>` before re-registering a changed expression. `clear-watch --id <id>` removes one watch; `clear-watch --all` removes all watches. `get-watch-values` without `--id` returns every registered watch.

The expression may use `UloopPausePoint.TryGetCapturedValue("name")` to inspect the latest raw pause-point capture while paused. Each history entry includes the frame and either a stringified value or an explicit error type and message. A throwing expression is recorded as an error and does not stop the Editor update loop. `--max-history` accepts 1 through 100 and drops the oldest entries after the limit.

Watch expressions are in-memory Editor state. A domain reload clears them, so re-register them after `uloop compile`, script recompilation, or an Editor restart. For reliable per-Step changes, keep the expression attached to a continuous pause point on an `Update` or `FixedUpdate` line and use `control-play-mode --action Step`.

## Marker Types

- `uloop enable-pause-point --file --line` patches the already-compiled method at a source line. No code edit or recompile is required.
Expand Down
3 changes: 2 additions & 1 deletion Assets/Tests/Editor/UnityCLILoop.Tests.Editor.asmdef
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@
"GUID:332048ead1ebf4658b9961338e5d68c3",
"GUID:33fef506e6744f2982e8c13b1196f696",
"GUID:da253b56f9d34380980fbe7bafe64666",
"GUID:96b8d4624fb74ea2849fed17e7ad69d3"
"GUID:96b8d4624fb74ea2849fed17e7ad69d3",
"GUID:e3fd8b49afd6f4e3cae09011028922b4"
],
"includePlatforms": [
"Editor"
Expand Down
63 changes: 63 additions & 0 deletions Assets/Tests/Editor/WatchExpressionCompilerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using System.Collections;
using System.Threading;
using System.Threading.Tasks;

using NUnit.Framework;

using UnityEngine.TestTools;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Verifies watch expressions compile through the shared dynamic-code compiler without executing user code.
/// </summary>
[TestFixture]
public sealed class WatchExpressionCompilerTests
{
private const int MaxCompilationWaitTicks = 600;

/// <summary>
/// Verifies a valid expression reaches the compile-only path and produces an evaluator.
/// </summary>
[UnityTest]
public IEnumerator CompileAsync_ValidExpressionReturnsCompiledEvaluator()
{
WatchExpressionCompiler compiler = new(new DynamicCodeCompiler());
Task<WatchCompilationResult> compilationTask = compiler.CompileAsync("1 + 2", CancellationToken.None);

int waitTicks = 0;
while (!compilationTask.IsCompleted && waitTicks < MaxCompilationWaitTicks)
{
waitTicks++;
yield return null;
}

if (!compilationTask.IsCompleted)
{
Assert.Fail($"Watch expression compilation did not complete within {MaxCompilationWaitTicks} editor ticks.");
yield break;
}

Assert.That(compilationTask.IsCompletedSuccessfully, Is.True);
Assert.That(compilationTask.Result.Success, Is.True);
Assert.That(compilationTask.Result.Evaluator, Is.Not.Null);
}

/// <summary>
/// Verifies an empty expression is rejected before invoking the compiler.
/// </summary>
[Test]
public void CompileAsync_EmptyExpressionReturnsFailure()
{
WatchExpressionCompiler compiler = new(new DynamicCodeCompiler());

Task<WatchCompilationResult> compilationTask = compiler.CompileAsync("", CancellationToken.None);

Assert.That(compilationTask.IsCompletedSuccessfully, Is.True);
Assert.That(compilationTask.Result.Success, Is.False);
Assert.That(compilationTask.Result.ErrorMessage, Does.Contain("must not be empty"));
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Editor/WatchExpressionCompilerTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 42 additions & 0 deletions Assets/Tests/Editor/WatchExpressionEvaluationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System;
using System.Reflection;

using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Verifies user-code evaluation failures become explicit watch history results.
/// </summary>
[TestFixture]
public sealed class WatchExpressionEvaluationTests
{
/// <summary>
/// Verifies an exception from the generated evaluator is returned as a typed failure result.
/// </summary>
[Test]
public void Evaluate_WhenUserCodeThrows_ReturnsFailureResult()
{
MethodInfo method = typeof(ThrowingWatchExpression).GetMethod(nameof(ThrowingWatchExpression.Evaluate));
CompiledWatchExpressionEvaluator evaluator = new(
new ThrowingWatchExpression(),
method);

WatchEvaluationResult result = evaluator.Evaluate();

Assert.That(result.Success, Is.False);
Assert.That(result.ErrorTypeName, Is.EqualTo(typeof(InvalidOperationException).FullName));
Assert.That(result.ErrorMessage, Is.EqualTo("watch failed"));
}

private sealed class ThrowingWatchExpression
{
public object Evaluate()
{
throw new InvalidOperationException("watch failed");
}
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Editor/WatchExpressionEvaluationTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading