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
4 changes: 2 additions & 2 deletions .agents/skills/uloop-pause-point/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ While Unity is paused on a hit, `execute-dynamic-code` can read live captured re
- `GetCapturedNames()` lists captured variable names from that snapshot.
- `GetCapturedPausePointId()` returns the pause-point id for the held snapshot.

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`.
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`. Re-enabling the same pause point while still paused (for example to refresh its timeout during a step session) keeps the held references, because a re-enable does not resume Unity.

## Watch Expressions

Expand All @@ -84,7 +84,7 @@ uloop enable-watch --id "speed" --expression "UloopPausePoint.TryGetCapturedValu
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.
`enable-watch` compiles the C# expression once, evaluates it immediately for a baseline, and then evaluates it once per changed `Time.frameCount`, but only while Play Mode is running and the Editor is paused (each hit pause and each `Step`); nothing is recorded while the game runs unpaused. 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.

Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/uloop-pause-point/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ While Unity is paused on a hit, `execute-dynamic-code` can read live captured re
- `GetCapturedNames()` lists captured variable names from that snapshot.
- `GetCapturedPausePointId()` returns the pause-point id for the held snapshot.

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`.
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`. Re-enabling the same pause point while still paused (for example to refresh its timeout during a step session) keeps the held references, because a re-enable does not resume Unity.

## Watch Expressions

Expand All @@ -84,7 +84,7 @@ uloop enable-watch --id "speed" --expression "UloopPausePoint.TryGetCapturedValu
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.
`enable-watch` compiles the C# expression once, evaluates it immediately for a baseline, and then evaluates it once per changed `Time.frameCount`, but only while Play Mode is running and the Editor is paused (each hit pause and each `Step`); nothing is recorded while the game runs unpaused. 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.

Expand Down
39 changes: 39 additions & 0 deletions Assets/Tests/Editor/PausePointTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,45 @@ public void TryGetCapturedValue_WhenUnrelatedPausePointIsCleared_KeepsLatestHitR
Assert.That(UloopPausePoint.GetCapturedPausePointId(), Is.EqualTo("land"));
}

[Test]
public void TryGetCapturedValue_WhenSamePausePointIsReenabledWhilePaused_KeepsLatestHitRawCapture()
{
// Verifies re-enabling the same id while still paused (e.g. to refresh its timeout
// during a step session) keeps the held raw capture instead of clearing it.
UloopPausePointRegistry.Enable("jump", 30);
UloopPausePointCapturedVariableFrame frame = new(
new[] { new UloopPausePointCapturedVariableEntry("speed", UloopCapturedVariableScope.Local, 1) },
false);
UloopPausePointRegistry.HitWithCapturedFrame("jump", frame, Array.Empty<UloopCapturedVariable>(), false);

UloopPausePointRegistry.Enable("jump", 30);

(bool found, object value) = UloopPausePoint.TryGetCapturedValue("speed");
Assert.That(found, Is.True);
Assert.That(value, Is.EqualTo(1));
Assert.That(UloopPausePoint.GetCapturedPausePointId(), Is.EqualTo("jump"));
}

[Test]
public void TryGetCapturedValue_WhenSamePausePointIsReenabledThenCleared_StillClearsRawCapture()
{
// Verifies Clear(id) after a same-id re-enable still drops the raw capture holder,
// even though the re-enable already reset the hit snapshot bookkeeping.
UloopPausePointRegistry.Enable("jump", 30);
UloopPausePointCapturedVariableFrame frame = new(
new[] { new UloopPausePointCapturedVariableEntry("speed", UloopCapturedVariableScope.Local, 1) },
false);
UloopPausePointRegistry.HitWithCapturedFrame("jump", frame, Array.Empty<UloopCapturedVariable>(), false);

UloopPausePointRegistry.Enable("jump", 30);
UloopPausePointRegistry.Clear("jump");

(bool found, object value) = UloopPausePoint.TryGetCapturedValue("speed");
Assert.That(found, Is.False);
Assert.That(value, Is.Null);
Assert.That(UloopPausePoint.GetCapturedPausePointId(), Is.Empty);
}

[Test]
public void Hit_WhenPausePointIsEnabled_ReportsEmptyCapturedVariables()
{
Expand Down
4 changes: 2 additions & 2 deletions Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ While Unity is paused on a hit, `execute-dynamic-code` can read live captured re
- `GetCapturedNames()` lists captured variable names from that snapshot.
- `GetCapturedPausePointId()` returns the pause-point id for the held snapshot.

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`.
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`. Re-enabling the same pause point while still paused (for example to refresh its timeout during a step session) keeps the held references, because a re-enable does not resume Unity.

## Watch Expressions

Expand All @@ -84,7 +84,7 @@ uloop enable-watch --id "speed" --expression "UloopPausePoint.TryGetCapturedValu
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.
`enable-watch` compiles the C# expression once, evaluates it immediately for a baseline, and then evaluates it once per changed `Time.frameCount`, but only while Play Mode is running and the Editor is paused (each hit pause and each `Step`); nothing is recorded while the game runs unpaused. 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.

Expand Down
33 changes: 23 additions & 10 deletions Packages/src/Runtime/PausePoints/UloopPausePointRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ public static UloopPausePointSnapshot Enable(
int generation = ++_nextGeneration;
UloopPausePointEntry entry = new(id, timeoutSeconds, mode, maxHistory, now, generation);
Entries[id] = entry;
ClearLatestHitSnapshotIfMatches(id);
// Why not clear the raw capture holder here: a re-enable does not resume Unity, so the
// paused-window constraint (see UloopPausePointRawCaptureHolder's class comment) is not
// violated by keeping the previous hit's live references across a same-id re-enable.
ForgetHitSnapshotForId(id);
return entry.ToSnapshot(now, _pauseController);
}

Expand Down Expand Up @@ -86,7 +89,7 @@ public static UloopPausePointSnapshot Clear(string id)
_ => "Pause point cleared."
};
entry.MarkCleared(UloopPausePointClearedReason.ExplicitClear, message);
ClearLatestHitSnapshotIfMatches(id);
ClearHitSnapshotAndRawCaptureForId(id);
return entry.ToSnapshot(now, _pauseController);
}

Expand Down Expand Up @@ -244,21 +247,31 @@ public static void ClearLatestHitSnapshot()
UloopPausePointRawCaptureHolder.Clear();
}

private static void ClearLatestHitSnapshotIfMatches(string id)
// Drops the id's own hit-history entry and, if it currently owns the latest hit, that
// pointer too - but never touches the raw capture holder. Enable() uses this so a same-id
// re-enable while paused only resets the entry's generation bookkeeping.
private static void ForgetHitSnapshotForId(string id)
{
_hitSnapshots.RemoveAll(hitSnapshot => hitSnapshot.Id == id);
if (_latestHitSnapshot == null)
if (_latestHitSnapshot != null && _latestHitSnapshot.Id == id)
{
return;
_latestHitSnapshot = null;
}
}

if (_latestHitSnapshot.Id != id)
// Same as ForgetHitSnapshotForId, plus clears the raw capture holder when the holder's
// captured snapshot belongs to the cleared id. Clear(id) uses this because an explicit
// clear is documented to drop captures. Ownership is checked against the holder itself
// (not _latestHitSnapshot) because Enable()'s ForgetHitSnapshotForId already nulls out
// _latestHitSnapshot on a same-id re-enable, which would otherwise make a later Clear(id)
// think it no longer owns a holder it actually still does.
private static void ClearHitSnapshotAndRawCaptureForId(string id)
{
ForgetHitSnapshotForId(id);
if (UloopPausePointRawCaptureHolder.GetCapturedPausePointId() == id)
{
return;
UloopPausePointRawCaptureHolder.Clear();
}

_latestHitSnapshot = null;
UloopPausePointRawCaptureHolder.Clear();
}

public static void ConfigureForTests(IUloopPausePointPauseController pauseController, Func<DateTime> nowProvider)
Expand Down
Loading