From ec0c7af7275008b3719235e94326461d31ec9c0d Mon Sep 17 00:00:00 2001 From: hatayama Date: Sun, 12 Jul 2026 17:44:35 +0900 Subject: [PATCH 1/3] Add pause point capture mode constants Define the stable string values for single-shot, continuous, and trace pause point behavior so later registry and tool changes share one contract. --- .../PausePoints/UloopPausePointCaptureMode.cs | 14 ++++++++++++++ .../PausePoints/UloopPausePointCaptureMode.cs.meta | 11 +++++++++++ 2 files changed, 25 insertions(+) create mode 100644 Packages/src/Runtime/PausePoints/UloopPausePointCaptureMode.cs create mode 100644 Packages/src/Runtime/PausePoints/UloopPausePointCaptureMode.cs.meta diff --git a/Packages/src/Runtime/PausePoints/UloopPausePointCaptureMode.cs b/Packages/src/Runtime/PausePoints/UloopPausePointCaptureMode.cs new file mode 100644 index 0000000000..7008919684 --- /dev/null +++ b/Packages/src/Runtime/PausePoints/UloopPausePointCaptureMode.cs @@ -0,0 +1,14 @@ +#if UNITY_EDITOR +namespace io.github.hatayama.UnityCliLoop.Runtime +{ + /// + /// Defines how a pause point behaves after it captures a hit. + /// + internal static class UloopPausePointCaptureMode + { + public const string SingleShot = "single-shot"; + public const string Continuous = "continuous"; + public const string Trace = "trace"; + } +} +#endif diff --git a/Packages/src/Runtime/PausePoints/UloopPausePointCaptureMode.cs.meta b/Packages/src/Runtime/PausePoints/UloopPausePointCaptureMode.cs.meta new file mode 100644 index 0000000000..ffb2758adf --- /dev/null +++ b/Packages/src/Runtime/PausePoints/UloopPausePointCaptureMode.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2057563b6f48b41ada9ba6d245b357b3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 5ac4cb06bca07d3f7516626a9effd9183884f91f Mon Sep 17 00:00:00 2001 From: hatayama Date: Sun, 12 Jul 2026 17:51:29 +0900 Subject: [PATCH 2/3] Add pause point capture modes and hit history Extend pause point state with continuous and trace behavior plus bounded capture history while preserving single-shot defaults. Record frame-level evidence and retain history through clear and expiry so status consumers can inspect prior captures. --- .../Editor/PausePointCaptureModeTests.cs | 185 ++++++++++++++++++ .../Editor/PausePointCaptureModeTests.cs.meta | 11 ++ .../UloopPausePointCapturedHistoryFrame.cs | 32 +++ ...loopPausePointCapturedHistoryFrame.cs.meta | 11 ++ .../PausePoints/UloopPausePointEntry.cs | 51 ++++- .../PausePoints/UloopPausePointRegistry.cs | 41 +++- .../PausePoints/UloopPausePointSnapshot.cs | 16 ++ 7 files changed, 335 insertions(+), 12 deletions(-) create mode 100644 Assets/Tests/Editor/PausePointCaptureModeTests.cs create mode 100644 Assets/Tests/Editor/PausePointCaptureModeTests.cs.meta create mode 100644 Packages/src/Runtime/PausePoints/UloopPausePointCapturedHistoryFrame.cs create mode 100644 Packages/src/Runtime/PausePoints/UloopPausePointCapturedHistoryFrame.cs.meta diff --git a/Assets/Tests/Editor/PausePointCaptureModeTests.cs b/Assets/Tests/Editor/PausePointCaptureModeTests.cs new file mode 100644 index 0000000000..37e3102078 --- /dev/null +++ b/Assets/Tests/Editor/PausePointCaptureModeTests.cs @@ -0,0 +1,185 @@ +using System; +using System.Linq; + +using NUnit.Framework; + +using io.github.hatayama.UnityCliLoop.Runtime; + +namespace io.github.hatayama.UnityCliLoop.Tests.Editor +{ + /// + /// Verifies pause point capture modes and bounded hit history behavior. + /// + [TestFixture] + public sealed class PausePointCaptureModeTests + { + private DateTime _nowUtc; + private FakePausePointPauseController _pauseController; + + [SetUp] + public void SetUp() + { + _nowUtc = new DateTime(2026, 6, 3, 0, 0, 0, DateTimeKind.Utc); + _pauseController = new FakePausePointPauseController(); + UloopPausePointRegistry.ConfigureForTests(_pauseController, () => _nowUtc); + } + + [TearDown] + public void TearDown() + { + UloopPausePointRegistry.ResetForTests(); + } + + /// + /// Verifies continuous capture keeps the marker armed while exposing ordered history and the latest variables. + /// + [Test] + public void Continuous_WhenHitMultipleTimes_PreservesHistoryAndLatestVariables() + { + UloopCapturedVariable[] firstVariables = { CreateVariable("speed", "1") }; + UloopCapturedVariable[] secondVariables = { CreateVariable("speed", "2") }; + UloopPausePointRegistry.Enable("jump", 30, UloopPausePointCaptureMode.Continuous, 20); + + UloopPausePointRegistry.HitWithCapturedVariables("jump", firstVariables, false); + UloopPausePointRegistry.HitWithCapturedVariables("jump", secondVariables, false); + + UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus("jump"); + + Assert.That(snapshot.IsEnabled, Is.True); + Assert.That(snapshot.HitCount, Is.EqualTo(2)); + Assert.That(snapshot.CapturedVariables.Single().Value, Is.EqualTo("2")); + Assert.That(snapshot.CapturedVariableHistory.Select(frame => frame.HitSequence), Is.EqualTo(new[] { 1, 2 })); + Assert.That(snapshot.CapturedVariableHistory.Last().CapturedVariables.Single().Value, Is.EqualTo("2")); + Assert.That(_pauseController.PauseCount, Is.EqualTo(2)); + } + + /// + /// Verifies trace capture records a hit without requesting an Editor pause. + /// + [Test] + public void Trace_WhenHit_DoesNotPauseAndKeepsMarkerArmed() + { + UloopPausePointRegistry.Enable("trace", 30, UloopPausePointCaptureMode.Trace, 20); + + UloopPausePointSnapshot snapshot = UloopPausePointRegistry.Hit("trace"); + + Assert.That(snapshot.IsEnabled, Is.True); + Assert.That(snapshot.HitCount, Is.EqualTo(1)); + Assert.That(snapshot.Mode, Is.EqualTo(UloopPausePointCaptureMode.Trace)); + Assert.That(snapshot.CapturedVariableHistory, Has.Count.EqualTo(1)); + Assert.That(_pauseController.PauseCount, Is.EqualTo(0)); + } + + /// + /// Verifies the bounded history drops the oldest frame and reports the dropped count. + /// + [Test] + public void History_WhenMaxHistoryIsExceeded_DropsOldestFrames() + { + UloopPausePointRegistry.Enable("jump", 30, UloopPausePointCaptureMode.Trace, 2); + + UloopPausePointRegistry.Hit("jump"); + UloopPausePointRegistry.Hit("jump"); + UloopPausePointRegistry.Hit("jump"); + + UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus("jump"); + + Assert.That(snapshot.CapturedVariableHistory.Select(frame => frame.HitSequence), Is.EqualTo(new[] { 2, 3 })); + Assert.That(snapshot.HistoryDroppedCount, Is.EqualTo(1)); + } + + /// + /// Verifies the default single-shot mode retains current disarm behavior and records its hit. + /// + [Test] + public void SingleShot_WhenHit_DisarmsAndKeepsOneHistoryFrame() + { + UloopPausePointRegistry.Enable("jump", 30); + + UloopPausePointRegistry.Hit("jump"); + UloopPausePointRegistry.Hit("jump"); + + UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus("jump"); + + Assert.That(snapshot.IsEnabled, Is.False); + Assert.That(snapshot.HitCount, Is.EqualTo(1)); + Assert.That(snapshot.CapturedVariableHistory, Has.Count.EqualTo(1)); + Assert.That(_pauseController.PauseCount, Is.EqualTo(1)); + } + + /// + /// Verifies clearing an armed continuous marker disarms it without erasing captured history. + /// + [Test] + public void Clear_WhenContinuousMarkerHasHits_DisarmsAndPreservesHistory() + { + UloopPausePointRegistry.Enable("jump", 30, UloopPausePointCaptureMode.Continuous, 20); + UloopPausePointRegistry.Hit("jump"); + + UloopPausePointSnapshot snapshot = UloopPausePointRegistry.Clear("jump"); + + Assert.That(snapshot.IsEnabled, Is.False); + Assert.That(snapshot.CapturedVariableHistory, Has.Count.EqualTo(1)); + Assert.That(snapshot.ClearedReason, Is.EqualTo(UloopPausePointClearedReason.ExplicitClear)); + Assert.That(snapshot.Message, Is.EqualTo("Pause point cleared after 1 hit(s); capture history is preserved.")); + } + + /// + /// Verifies expiry after a continuous hit reports a capture-window message and retains history. + /// + [Test] + public void Expire_WhenContinuousMarkerHasHits_DisarmsAndPreservesHistory() + { + UloopPausePointRegistry.Enable("jump", 1, UloopPausePointCaptureMode.Continuous, 20); + UloopPausePointRegistry.Hit("jump"); + _nowUtc = _nowUtc.AddSeconds(2); + + UloopPausePointSnapshot snapshot = UloopPausePointRegistry.GetStatus("jump"); + + Assert.That(snapshot.Status, Is.EqualTo(UloopPausePointStatus.Expired)); + Assert.That(snapshot.CapturedVariableHistory, Has.Count.EqualTo(1)); + Assert.That(snapshot.Message, Is.EqualTo("Pause point capture window expired after 1 hit(s); capture history is preserved.")); + } + + /// + /// Verifies re-enabling a marker starts a fresh generation with empty history. + /// + [Test] + public void Enable_WhenMarkerIsReenabled_ResetsHistory() + { + UloopPausePointRegistry.Enable("jump", 30, UloopPausePointCaptureMode.Trace, 2); + UloopPausePointRegistry.Hit("jump"); + + UloopPausePointSnapshot snapshot = UloopPausePointRegistry.Enable( + "jump", 30, UloopPausePointCaptureMode.Trace, 2); + + Assert.That(snapshot.HitCount, Is.EqualTo(0)); + Assert.That(snapshot.CapturedVariableHistory, Is.Empty); + Assert.That(snapshot.HistoryDroppedCount, Is.EqualTo(0)); + } + + private static UloopCapturedVariable CreateVariable(string name, string value) + { + return new UloopCapturedVariable( + name, + UloopCapturedVariableScope.Local, + "System.String", + value, + string.Empty, + string.Empty, + 0); + } + + private sealed class FakePausePointPauseController : IUloopPausePointPauseController + { + public int PauseCount { get; private set; } + public bool IsPlaying => true; + public bool IsPaused => PauseCount > 0; + + public void Pause() + { + PauseCount++; + } + } + } +} diff --git a/Assets/Tests/Editor/PausePointCaptureModeTests.cs.meta b/Assets/Tests/Editor/PausePointCaptureModeTests.cs.meta new file mode 100644 index 0000000000..02f102676b --- /dev/null +++ b/Assets/Tests/Editor/PausePointCaptureModeTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cf4f284db0df5429a8d287aa1833ae45 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Runtime/PausePoints/UloopPausePointCapturedHistoryFrame.cs b/Packages/src/Runtime/PausePoints/UloopPausePointCapturedHistoryFrame.cs new file mode 100644 index 0000000000..dd0b2a2062 --- /dev/null +++ b/Packages/src/Runtime/PausePoints/UloopPausePointCapturedHistoryFrame.cs @@ -0,0 +1,32 @@ +#if UNITY_EDITOR +using System.Collections.Generic; + +namespace io.github.hatayama.UnityCliLoop.Runtime +{ + /// + /// Stores the formatted evidence captured by one pause point hit. + /// + internal sealed class UloopPausePointCapturedHistoryFrame + { + public UloopPausePointCapturedHistoryFrame( + int hitSequence, + int frameCount, + string hitAtUtc, + IReadOnlyList capturedVariables, + bool truncated) + { + HitSequence = hitSequence; + FrameCount = frameCount; + HitAtUtc = hitAtUtc; + CapturedVariables = capturedVariables; + Truncated = truncated; + } + + public int HitSequence { get; } + public int FrameCount { get; } + public string HitAtUtc { get; } + public IReadOnlyList CapturedVariables { get; } + public bool Truncated { get; } + } +} +#endif diff --git a/Packages/src/Runtime/PausePoints/UloopPausePointCapturedHistoryFrame.cs.meta b/Packages/src/Runtime/PausePoints/UloopPausePointCapturedHistoryFrame.cs.meta new file mode 100644 index 0000000000..c4252b0b75 --- /dev/null +++ b/Packages/src/Runtime/PausePoints/UloopPausePointCapturedHistoryFrame.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 709b465a62e244755a4c8ee3a2208b35 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Packages/src/Runtime/PausePoints/UloopPausePointEntry.cs b/Packages/src/Runtime/PausePoints/UloopPausePointEntry.cs index 7c5f6e90de..bf76e682fc 100644 --- a/Packages/src/Runtime/PausePoints/UloopPausePointEntry.cs +++ b/Packages/src/Runtime/PausePoints/UloopPausePointEntry.cs @@ -10,10 +10,18 @@ namespace io.github.hatayama.UnityCliLoop.Runtime /// internal sealed class UloopPausePointEntry { - public UloopPausePointEntry(string id, int timeoutSeconds, DateTime enabledAtUtc, int generation) + public UloopPausePointEntry( + string id, + int timeoutSeconds, + string mode, + int maxHistory, + DateTime enabledAtUtc, + int generation) { Id = id; TimeoutSeconds = timeoutSeconds; + Mode = mode; + MaxHistory = maxHistory; EnabledAtUtc = enabledAtUtc; ExpiresAtUtc = enabledAtUtc.AddSeconds(timeoutSeconds); Generation = generation; @@ -21,10 +29,13 @@ public UloopPausePointEntry(string id, int timeoutSeconds, DateTime enabledAtUtc IsEnabled = true; Message = "Pause point enabled."; CapturedVariables = Array.Empty(); + _capturedVariableHistory = new Queue(maxHistory); } public string Id { get; } public int TimeoutSeconds { get; } + public string Mode { get; } + public int MaxHistory { get; } public DateTime EnabledAtUtc { get; } public DateTime ExpiresAtUtc { get; } public int Generation { get; } @@ -40,6 +51,9 @@ public UloopPausePointEntry(string id, int timeoutSeconds, DateTime enabledAtUtc public string Message { get; private set; } public IReadOnlyList CapturedVariables { get; private set; } public bool CapturedVariablesTruncated { get; private set; } + public IReadOnlyList CapturedVariableHistory => + new List(_capturedVariableHistory); + public int HistoryDroppedCount { get; private set; } public string ClearedReason { get; private set; } = string.Empty; public string StatusBeforeClear { get; private set; } = string.Empty; public bool LateHitDiscardedAfterClear { get; private set; } @@ -58,7 +72,9 @@ public void ExpireIfNeeded(DateTime nowUtc) IsEnabled = false; Status = UloopPausePointStatus.Expired; - Message = "Pause point expired before it was hit."; + Message = HitCount == 0 + ? "Pause point expired before it was hit." + : $"Pause point capture window expired after {HitCount} hit(s); capture history is preserved."; } public void MarkCleared(string clearedReason, string message = "Pause point cleared.") @@ -78,6 +94,7 @@ public void MarkCleared(string clearedReason, string message = "Pause point clea ClearedReason = UloopPausePointClearedReason.AfterExpired; } else if (Status == UloopPausePointStatus.Hit && + !IsEnabled && clearedReason == UloopPausePointClearedReason.ExplicitClear) { ClearedReason = UloopPausePointClearedReason.AlreadyHit; @@ -102,7 +119,8 @@ public void MarkLateHitDiscardedAfterClear() public void RecordHit(DateTime nowUtc, bool isPlaying, bool isPaused, int hitSequence) { RecordHitWithCapturedVariables( - nowUtc, isPlaying, isPaused, hitSequence, Array.Empty(), false); + nowUtc, isPlaying, isPaused, hitSequence, Time.frameCount, + Array.Empty(), false); } public void RecordHitWithCapturedVariables( @@ -110,6 +128,7 @@ public void RecordHitWithCapturedVariables( bool isPlaying, bool isPaused, int hitSequence, + int frameCount, IReadOnlyList capturedVariables, bool capturedVariablesTruncated) { @@ -127,11 +146,27 @@ public void RecordHitWithCapturedVariables( LastHitSequence = hitSequence; IsPlayingAtHit = isPlaying; IsPausedAtHit = isPaused; - IsEnabled = false; + IsEnabled = Mode != UloopPausePointCaptureMode.SingleShot; Status = UloopPausePointStatus.Hit; - Message = "Pause point hit; Unity pause was requested."; + Message = Mode == UloopPausePointCaptureMode.Trace + ? "Pause point hit; recorded to history without pausing (trace mode)." + : "Pause point hit; Unity pause was requested."; CapturedVariables = capturedVariables; CapturedVariablesTruncated = capturedVariablesTruncated; + + if (_capturedVariableHistory.Count == MaxHistory) + { + _capturedVariableHistory.Dequeue(); + HistoryDroppedCount++; + } + + _capturedVariableHistory.Enqueue( + new UloopPausePointCapturedHistoryFrame( + hitSequence, + frameCount, + FormatUtc(nowUtc), + capturedVariables, + capturedVariablesTruncated)); } public UloopPausePointSnapshot ToSnapshot(DateTime nowUtc, IUloopPausePointPauseController pauseController) @@ -161,6 +196,10 @@ public UloopPausePointSnapshot ToSnapshot(DateTime nowUtc, IUloopPausePointPause isHit, HitCount, TimeoutSeconds, + Mode, + MaxHistory, + CapturedVariableHistory, + HistoryDroppedCount, expired, FormatUtc(EnabledAtUtc), elapsedMilliseconds, @@ -201,6 +240,8 @@ private static string FormatUtc(DateTime value) DateTime utcValue = value.Kind == DateTimeKind.Utc ? value : value.ToUniversalTime(); return utcValue.ToString("O"); } + + private readonly Queue _capturedVariableHistory; } } #endif diff --git a/Packages/src/Runtime/PausePoints/UloopPausePointRegistry.cs b/Packages/src/Runtime/PausePoints/UloopPausePointRegistry.cs index 826f9ae708..595579eac4 100644 --- a/Packages/src/Runtime/PausePoints/UloopPausePointRegistry.cs +++ b/Packages/src/Runtime/PausePoints/UloopPausePointRegistry.cs @@ -15,6 +15,8 @@ namespace io.github.hatayama.UnityCliLoop.Runtime internal static class UloopPausePointRegistry { public const int DefaultTimeoutSeconds = 30; + public const int DefaultMaxHistory = 20; + public const int MaxHistoryLimit = 100; private static readonly ConcurrentDictionary Entries = new(); private static IUloopPausePointPauseController _pauseController = new UnityEditorPausePointPauseController(); @@ -35,14 +37,21 @@ internal static class UloopPausePointRegistry public static Action OnCleared { get; set; } public static Action OnClearedAll { get; set; } - public static UloopPausePointSnapshot Enable(string id, int timeoutSeconds) + public static UloopPausePointSnapshot Enable( + string id, + int timeoutSeconds, + string mode = UloopPausePointCaptureMode.SingleShot, + int maxHistory = DefaultMaxHistory) { Debug.Assert(!string.IsNullOrWhiteSpace(id), "id must not be null or empty"); Debug.Assert(timeoutSeconds > 0, "timeoutSeconds must be greater than zero"); + Debug.Assert(IsSupportedMode(mode), "mode must be a supported pause point capture mode"); + Debug.Assert(maxHistory > 0, "maxHistory must be greater than zero"); + Debug.Assert(maxHistory <= MaxHistoryLimit, "maxHistory must not exceed the history limit"); DateTime now = NowUtc(); int generation = ++_nextGeneration; - UloopPausePointEntry entry = new(id, timeoutSeconds, now, generation); + UloopPausePointEntry entry = new(id, timeoutSeconds, mode, maxHistory, now, generation); Entries[id] = entry; ClearLatestHitSnapshotIfMatches(id); return entry.ToSnapshot(now, _pauseController); @@ -63,10 +72,16 @@ public static UloopPausePointSnapshot Clear(string id) UloopPausePointEntry entry = Entries[id]; // Resolve expiry first so a clear after the timeout reports "expired", not a normal clear. entry.ExpireIfNeeded(now); - string message = entry.Status switch + string message = !entry.IsEnabled && entry.Status == UloopPausePointStatus.Hit + ? "Pause point was already hit (auto-disarmed); nothing to clear." + : entry.Status switch { - UloopPausePointStatus.Hit => "Pause point was already hit (auto-disarmed); nothing to clear.", - UloopPausePointStatus.Expired => "Pause point had already expired before being hit; nothing to clear.", + UloopPausePointStatus.Hit => + $"Pause point cleared after {entry.HitCount} hit(s); capture history is preserved.", + UloopPausePointStatus.Expired when entry.HitCount > 0 => + "Pause point capture window had already expired; nothing to clear.", + UloopPausePointStatus.Expired => + "Pause point had already expired before being hit; nothing to clear.", UloopPausePointStatus.Cleared => "Pause point was already cleared.", _ => "Pause point cleared." }; @@ -190,11 +205,16 @@ private static UloopPausePointSnapshot HitCore( return entry.ToSnapshot(now, _pauseController); } - _pauseController.Pause(); + if (entry.Mode != UloopPausePointCaptureMode.Trace) + { + _pauseController.Pause(); + } + int hitSequence = ++_nextHitSequence; + int frameCount = Time.frameCount; entry.RecordHitWithCapturedVariables( now, _pauseController.IsPlaying, _pauseController.IsPaused, hitSequence, - capturedVariables, capturedVariablesTruncated); + frameCount, capturedVariables, capturedVariablesTruncated); UloopPausePointSnapshot snapshot = entry.ToSnapshot(now, _pauseController); _latestHitSnapshot = snapshot; _hitSnapshots.RemoveAll(hitSnapshot => hitSnapshot.Id == id); @@ -267,6 +287,13 @@ private static DateTime NowUtc() DateTime now = _nowProvider(); return now.Kind == DateTimeKind.Utc ? now : now.ToUniversalTime(); } + + private static bool IsSupportedMode(string mode) + { + return mode == UloopPausePointCaptureMode.SingleShot || + mode == UloopPausePointCaptureMode.Continuous || + mode == UloopPausePointCaptureMode.Trace; + } } } #endif diff --git a/Packages/src/Runtime/PausePoints/UloopPausePointSnapshot.cs b/Packages/src/Runtime/PausePoints/UloopPausePointSnapshot.cs index 61b986d222..fb9914863c 100644 --- a/Packages/src/Runtime/PausePoints/UloopPausePointSnapshot.cs +++ b/Packages/src/Runtime/PausePoints/UloopPausePointSnapshot.cs @@ -18,6 +18,10 @@ public UloopPausePointSnapshot( bool isHit, int hitCount, int timeoutSeconds, + string mode, + int maxHistory, + IReadOnlyList capturedVariableHistory, + int historyDroppedCount, bool expired, string enabledAtUtc, long elapsedMilliseconds, @@ -44,6 +48,10 @@ public UloopPausePointSnapshot( IsHit = isHit; HitCount = hitCount; TimeoutSeconds = timeoutSeconds; + Mode = mode ?? UloopPausePointCaptureMode.SingleShot; + MaxHistory = maxHistory; + CapturedVariableHistory = capturedVariableHistory ?? Array.Empty(); + HistoryDroppedCount = historyDroppedCount; Expired = expired; EnabledAtUtc = enabledAtUtc ?? string.Empty; ElapsedSinceEnabledMilliseconds = elapsedMilliseconds; @@ -69,6 +77,10 @@ public UloopPausePointSnapshot( public bool IsHit { get; } public int HitCount { get; } public int TimeoutSeconds { get; } + public string Mode { get; } + public int MaxHistory { get; } + public IReadOnlyList CapturedVariableHistory { get; } + public int HistoryDroppedCount { get; } public bool Expired { get; } public string EnabledAtUtc { get; } public long ElapsedSinceEnabledMilliseconds { get; } @@ -98,6 +110,10 @@ public static UloopPausePointSnapshot NotEnabled(string id, IUloopPausePointPaus false, 0, 0, + UloopPausePointCaptureMode.SingleShot, + 0, + Array.Empty(), + 0, false, string.Empty, 0, From e1edde82551cb4c1b94b52ddef8d273fea14c2bc Mon Sep 17 00:00:00 2001 From: hatayama Date: Sun, 12 Jul 2026 18:00:18 +0900 Subject: [PATCH 3/3] Remove unsafe pause point hit helper Keep frame count capture inside the registry hit path, where main-thread execution is guaranteed, and make history snapshot allocation explicit at the call site. --- .../src/Runtime/PausePoints/UloopPausePointEntry.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/Packages/src/Runtime/PausePoints/UloopPausePointEntry.cs b/Packages/src/Runtime/PausePoints/UloopPausePointEntry.cs index bf76e682fc..a153cfaec9 100644 --- a/Packages/src/Runtime/PausePoints/UloopPausePointEntry.cs +++ b/Packages/src/Runtime/PausePoints/UloopPausePointEntry.cs @@ -51,8 +51,6 @@ public UloopPausePointEntry( public string Message { get; private set; } public IReadOnlyList CapturedVariables { get; private set; } public bool CapturedVariablesTruncated { get; private set; } - public IReadOnlyList CapturedVariableHistory => - new List(_capturedVariableHistory); public int HistoryDroppedCount { get; private set; } public string ClearedReason { get; private set; } = string.Empty; public string StatusBeforeClear { get; private set; } = string.Empty; @@ -116,13 +114,6 @@ public void MarkLateHitDiscardedAfterClear() LateHitDiscardedAfterClear = true; } - public void RecordHit(DateTime nowUtc, bool isPlaying, bool isPaused, int hitSequence) - { - RecordHitWithCapturedVariables( - nowUtc, isPlaying, isPaused, hitSequence, Time.frameCount, - Array.Empty(), false); - } - public void RecordHitWithCapturedVariables( DateTime nowUtc, bool isPlaying, @@ -198,7 +189,7 @@ public UloopPausePointSnapshot ToSnapshot(DateTime nowUtc, IUloopPausePointPause TimeoutSeconds, Mode, MaxHistory, - CapturedVariableHistory, + new List(_capturedVariableHistory), HistoryDroppedCount, expired, FormatUtc(EnabledAtUtc),