A replay is a recorded run. Its module supplies findings: diagnostic messages about particular simulation steps.
dotnet tool install --global RecursiveAI.OneScope.ToolRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported. The window and the no-window (headless) example both need a working graphics renderer; without one, OneScope cannot start.
Download and unzip the example first.
Window path — build the module, then open the replay:
dotnet build -o bin/demo onescope open sender.replay \ --module bin/demo/Example.dll \ --run 'seek {"step":10}' \ --run 'findings {"severity":"error"}'
Returns the step-7 error hot, referencing entity 1.
Console path — run the C# example without a window:
Unzip the example, then dotnet run.
The full program
The example emits a warning at step 3 and an error at step 7, then queries each severity separately. The warning suggests checking the heat input; the error has no suggested change.
The C# launcher installs OneScope.Tool 1.0.0 in its build folder and loads the module from its own assembly. The recordings and module are included example code, not a bundled OneScope file format.
The included eight-byte inputs are 43 4E 54 52 00 00 00 00 (sender) and 43 4E 54 52 02 00 00 00 (receiver): CNTR plus a file-role number. The reader creates ten simulation steps.
using System.Buffers; using System.Buffers.Binary; using System.Numerics; using System.Diagnostics; using System.Text.Json; using OneScope.Sdk; // CNTR + little-endian role: 0 stays, 1 sends, 2 receives; ten empty steps. File.WriteAllBytes("sender.replay", [67, 78, 84, 82, 0, 0, 0, 0]); File.WriteAllBytes("receiver.replay", [67, 78, 84, 82, 2, 0, 0, 0]); string assembly = typeof(CounterModule).Assembly.Location; string toolDir = Path.GetFullPath("obj/onescope-tool"); string tool = Path.Combine(toolDir, OperatingSystem.IsWindows() ? "onescope.exe" : "onescope"); if (!File.Exists(tool)) Run("dotnet", "tool", "install", "RecursiveAI.OneScope.Tool", "--version", "1.0.0", "--tool-path", toolDir); var r = Open("seek {\"step\":10}", "findings {\"from\":1,\"to\":10,\"severity\":\"warning\",\"limit\":10}", "findings {\"from\":1,\"to\":10,\"severity\":\"error\",\"limit\":1}"); Console.WriteLine($"Scanned through: {r[2].GetProperty("scannedThrough")}"); Console.WriteLine($"Warning filter count: {r[2].GetProperty("count")}"); Console.WriteLine($"Error filter count: {r[3].GetProperty("count")}"); Console.WriteLine($"Error: {r[3].GetProperty("findings")[0].GetProperty("code")}"); JsonElement[] Open(params string[] operations) { var args = new List<string> { "open", "sender.replay", "--module", assembly, "--headless", "--size", "640x480" }; foreach (string operation in operations) args.AddRange(["--run", operation]); return Run(tool, args.ToArray()).Split('\n', StringSplitOptions.RemoveEmptyEntries) .Select(s => JsonDocument.Parse(s).RootElement.Clone()).ToArray(); } string Run(string command, params string[] arguments) { var start = new ProcessStartInfo(command) { RedirectStandardOutput = true, RedirectStandardError = true }; foreach (string arg in arguments) start.ArgumentList.Add(arg); using var process = Process.Start(start)!; var output = process.StandardOutput.ReadToEndAsync(); var errors = process.StandardError.ReadToEndAsync(); process.WaitForExit(); if (process.ExitCode != 0) throw new InvalidOperationException(output.Result + errors.Result); return output.Result; } public sealed class CounterModule : IScopeModule { public string Id => "counter"; public string Name => "Counter"; public IScopeSource Open(ReadOnlyMemory<byte> data, string name) { if (data.Length != 8 || !data.Span[..4].SequenceEqual("CNTR"u8)) throw new ScopeFormatException(name, "Expected CNTR and an int32 role (8 bytes)."); int steps = 10; int role = data.Span[4]; if (role < 0 || role > 2 || data.Span[5..].IndexOfAnyExcept((byte)0) >= 0) throw new ScopeFormatException(name, "Expected role 0..2 and zero padding at offset 4."); var inputs = new ReadOnlyMemory<byte>[steps]; return new CounterSource(name, Recording.Create(ReadOnlyMemory<byte>.Empty, inputs), role); } } public sealed class CounterSource(string name, Recording recording, int role) : IScopeSource { public SourceInfo Info { get; } = new() { ModuleId = "counter", Name = name, Dimensions = 2, Unit = LengthUnit.Metre, LinkKey = role != 2 ? "sender" : "receiver", Fields = [DemoField.Info], Axes = [new AxisInfo(AxisIds.Time, "Time", 0)], EntityKinds = [new EntityKindInfo("counter", "Counter")], Overlays = [new OverlayInfo(0, "Counter", true)] }; public Recording Recording { get; } = recording; public IScopeWorld CreateWorld() => new CounterWorld(role); public IScopeSource WithRecording(Recording other) => new CounterSource(name, other, role); public void WriteReplay(IBufferWriter<byte> writer) { Span<byte> bytes = writer.GetSpan(8)[..8]; "CNTR"u8.CopyTo(bytes); BinaryPrimitives.WriteInt32LittleEndian(bytes[4..], role); writer.Advance(8); } } public sealed class CounterWorld(int role) : IScopeWorld { public long Step { get; private set; } public ulong Hash() => (ulong)Step; // This toy's entire state is its counter. public void Advance(ReadOnlySpan<byte> input, TimelineSink sink) { if (!input.IsEmpty) throw new ScopeFormatException("counter", "Expected empty step input."); Step = checked(Step + 1); if (Step == 3) sink.Finding("warm", Severity.Warning, [new EntityId(1)], "Temperature reached 3.", "Check the heat input."); if (Step == 7) sink.Finding("hot", Severity.Error, [new EntityId(1)], "Temperature reached 7."); } public void AdvanceAxis(string axis, int position, ReadOnlySpan<byte> input) => throw new ArgumentException("No secondary axes.", nameof(axis)); public void WriteSnapshot(IBufferWriter<byte> writer) { BinaryPrimitives.WriteInt64LittleEndian(writer.GetSpan(8), Step); writer.Advance(8); } public void Restore(ReadOnlySpan<byte> snapshot) { if (snapshot.Length != 8 || BinaryPrimitives.ReadInt64LittleEndian(snapshot) < 0) throw new ScopeFormatException("counter", "Expected a nonnegative int64 snapshot."); Step = BinaryPrimitives.ReadInt64LittleEndian(snapshot); } public void CollectEntities(List<EntityInfo> into) { into.Clear(); if (Present) into.Add(new(new EntityId(1), default, "counter", "Counter")); } public bool Inspect(EntityId entity, PropertyList into) { into.Clear(); if (entity.Value != 1 || !Present) return false; into.Add("State", "Value", ScopeValue.OfInteger(Step)); return true; } bool Present => role == 0 || (role == 1 ? Step < 5 : Step >= 5); public void CollectLinks(List<EntityLink> into) { into.Clear(); if (role == 0) return; into.Add(new(new EntityId(1), role == 1 ? LinkDirection.ContinuedBy : LinkDirection.Continues, role == 1 ? "receiver" : "sender", new EntityId(1), 5)); } public bool TryReadField(string field, EntityId entity, out double value) { value = Step; return field == "heat" && entity.Value == 1 && Present; } public bool TryReadFieldAt(string field, Vector3 point, out double value) { value = Step; return field == "heat"; } public void Draw(DrawList into, in DrawRequest request) { if (!Present) return; into.FieldMesh([new(0, 0, 0), new(2, 0, 0), new(0, 2, 0)], [0, 1, 2], [(float)Step, (float)Step, (float)Step], FieldDomain.Vertex, DemoField.Info, new DrawStyle(Color32.White, Entity: new EntityId(1))); into.FieldPoints([new Vector3(1, 1, 0)], [(float)Step], DemoField.Info, 12, new DrawStyle(Color32.White, Entity: new EntityId(1))); } public void EncodePoke(in PokeCall call, IBufferWriter<byte> into) => throw new ArgumentException("This counter declares no pokes."); public Difference? FirstDifference(IScopeWorld other) { if (other is not CounterWorld counter) throw new ArgumentException("Expected a counter."); return Step == counter.Step ? null : new Difference(new EntityId(1), "value", Step.ToString(System.Globalization.CultureInfo.InvariantCulture), counter.Step.ToString(System.Globalization.CultureInfo.InvariantCulture)); } } public static class DemoField { public static readonly FieldInfo Info = new("heat", "Temperature", ColorMap.Heat, 0, 10, "degrees"); }
dotnet run prints:
Scanned through: 10 Warning filter count: 1 Error filter count: 1 Error: hot
An empty findings array covers only the history scanned through scannedThrough, not necessarily the full replay.
Variations
Detect a truncated result
Run from the same example folder:
dotnet build -o bin/demo onescope open sender.replay \ --module bin/demo/Example.dll \ --run 'seek {"step":10}' \ --run 'findings {"from":1,"to":10,"limit":1}'
The final operation returns:
{"source":0,"scannedThrough":10,"findings":[{"step":3,"code":"warm","severity":"warning","entities":[1],"text":"Temperature reached 3.","suggestedChange":"Check the heat input."}],"count":2,"truncated":true}Related recipes
- Visualize simulation scalar fields in C#
- Track an entity across replay sources
- Inspect simulation replay entity properties
The short answer
Ask findings for the source’s recorded diagnostics and filter with from, to, severity and limit. Check scannedThrough before interpreting an empty result. Findings come from the module while history is reconstructed; the shell is not a general-purpose physics or correctness checker.
The sample module emits a warning at step 3 and an error at step 7. Filtering for warning returns only the first, while error returns only the second. Severity is an exact, case-insensitive match, not a minimum threshold: asking for warning does not include errors. The full program demonstrates that distinction with one result for each filter.
How do I filter diagnostic findings in a simulation replay?
Choose the source and the step interval relevant to your investigation. from and to include their endpoints. Add a severity when you want one category, or omit it to include all categories. The sample’s error query reports code hot, entity ID 1 and the message Temperature reached 7.
The diagnostic is intentionally generated by the toy module’s Advance method. Its source is the step-based example temperature, not a measurement from another library. Your real module must decide which events deserve findings and provide their text and entity references. A replay with no module-emitted findings can still contain behavior you consider incorrect.
The shell’s findings operation returns step, code, severity, entities, text and suggestedChange for each result. Use the code for a module-defined diagnostic category and the text for its explanation. A suggested change is optional; in the sample the warning suggests checking the heat input, while the error has no suggestion.
Why are replay findings empty before scanning?
History accumulates diagnostics as it scans steps. The response’s scannedThrough tells you how far that work has reached; it is not the last step containing a finding. A source can be fully scanned and have no diagnostics, or be partly scanned and already have some. Neither the current cursor nor the highest returned diagnostic alone describes scan coverage.
The short sample is scanned on opening and the program also seeks through its end. In a larger workflow, inspect coverage before treating a requested interval as complete. The open operation accepts a scan option, and seeking reconstructs history through the requested step. Merely asking findings for a later to value does not request a new scan: it filters findings already held in history.
That distinction matters when querying a late interval. If to exceeds scannedThrough, an empty array cannot establish that the rest of the interval has no findings. Scan or seek through the relevant recorded steps, then repeat the query. The module still controls which diagnostics are emitted during those steps.
How do I handle truncated findings and entity references?
The limit caps the returned array. count is the number of matching findings before that cap, and truncated reports whether matches were omitted. The variation requests one result across the whole sample: it returns the step-3 warning, reports a count of 2 and sets truncated to true. Reading the array length alone would hide the error at step 7.
Increase the limit or divide the step range into smaller intervals when you need all matches. Keep the filters consistent across those requests. Splitting a step interval does not remove the need to check scan coverage or truncation, especially if a module can emit many findings in one step.
Entity references are IDs supplied by the module, not embedded snapshots. To investigate one, seek to the finding’s step and inspect an appropriate entity in its source. An entity may have left the world by a later step, even though it existed when the finding was emitted. The sample keeps entity 1 present throughout the run, so both diagnostic steps can be inspected. In a module that removes entities, inspect the recorded step before assuming that the reference is stale or invalid.