Recursive AI

Debug running game physics live in C#

OneScope is a free, open-source viewer for deterministic libraries from Recursive AI. MIT or Apache-2.0.

This recipe works only for games built on OnePhysics with the probe setup below. The viewer reconstructs their physics while the game runs.

Terminal
dotnet tool install --global RecursiveAI.OneScope.Tool

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

Terminal
onescope attach
onescope attach 12345

Replace 12345 with your host’s PID from the first command. The second opens its physics world in a viewer window.

Unzip the example, then dotnet run.

With no probe-equipped hosts running, bare onescope attach prints:

JSON
{"probes":[],"removedStale":[],"unreadable":[]}

With a host running, probes contains its PID, socket, module, library version, and format version.

Run a host and attach a viewer

The download runs a small host and an in-process viewer connection: one circle starts at (0, 4), the viewer attaches at step zero, then the host advances ten steps.

The project pins RecursiveAI.OneScope.Probe and .Attach to 1.0.0, RecursiveAI.OnePhysics to 1.0.0, and RecursiveAI.OnePhysics.Scope to 1.1.0, the module version bundled with the tool.

To add the host and viewer APIs to your own console project:

Terminal
dotnet add package RecursiveAI.OneScope.Probe
dotnet add package RecursiveAI.OneScope.Attach
dotnet add package RecursiveAI.OnePhysics
dotnet add package RecursiveAI.OnePhysics.Scope
C#
using System.Buffers;
using OneScope.Attach;
using OneScope.Probe;
using RecursiveAI.OneMath;
using RecursiveAI.OnePhysics.Physics2D;
using RecursiveAI.OnePhysics.Scope;
 
var world = new PhysicsWorld2D();
world.AddCircle(0.5f, new Vector2D(0, 4));
var feed = new LiveFeedStream(world);
using var probe = ScopeProbe.Attach(
    new ProbeInfo("onephysics", "1.0.0", 2), new Snapshots(feed));
feed.Record = probe.Record;
using var recording = world.StartRecording(feed);
 
if (args.Contains("--host"))
{
    Console.WriteLine($"Attach with: onescope attach {Environment.ProcessId}");
    for (int i = 0; i < 3600; i++)
    {
        world.Step(1f / 60f);
        probe.Poll();
        Thread.Sleep(16);
    }
    return;
}
 
// The simulation thread services the snapshot request at a step boundary.
var connection = Task.Run(() => LiveSession.Connect(
    probe.SocketPath ?? throw new InvalidOperationException("Probe could not listen."), [new OnePhysicsModule()]));
while (!connection.IsCompleted)
{
    probe.Poll();
    Thread.Sleep(1);
}
using var session = connection.GetAwaiter().GetResult();
Console.WriteLine($"Attached at step {session.History.StartStep}");
for (int i = 0; i < 10; i++)
{
    world.Step(1f / 60f);
    probe.Poll();
}
var deadline = DateTime.UtcNow.AddSeconds(5);
while (session.History.EndStep < 10 && DateTime.UtcNow < deadline)
{
    session.Pump();
    Thread.Sleep(1);
}
if (session.History.EndStep != 10) throw new TimeoutException("Missing live records.");
session.History.Seek(10);
Console.WriteLine($"Viewer step: {session.History.CurrentStep}");
Console.WriteLine($"Host and viewer hashes match: {world.ComputeHash() == session.History.StateHash}");
 
sealed class Snapshots(LiveFeedStream feed) : ISnapshotSource
{
    public void WriteSnapshot(IBufferWriter<byte> writer) => feed.WriteSnapshot(writer);
}
Text
Attached at step 0
Viewer step: 10
Host and viewer hashes match: True

ScopeProbe.Attach can leave SocketPath null when it cannot listen; the example checks that before connecting. LiveSession.Connect waits for the host’s next snapshot and throws on a timeout or an incompatible module.

Variations

To keep the sample host running while you open a separate viewer, run this in the download folder:

Terminal
dotnet run -- --host

It prints an attach command with its actual PID. Run that command in another terminal before the host finishes. The sample runs for 3,600 steps with a 16 ms delay between steps.

For a terminal-only attach, append --headless --run sources to the printed command. It returns JSON describing the source, including its live state and the first and newest received steps.

The short answer

To debug a running OnePhysics game in C#, record the world into a LiveFeedStream, forward the feed’s records to ScopeProbe.Record, and let the probe obtain snapshots through ISnapshotSource. Start the OneScope viewer with onescope attach and the host’s PID. The viewer reconstructs the simulation locally from the snapshot and the following replay records.

The distinction matters: attaching does not stop a managed thread at a breakpoint or expose arbitrary objects from the game’s heap. It opens the physics state that the module knows how to reconstruct. Your host must cooperate by recording and servicing the probe. An uninstrumented process has no stream for OneScope to consume.

How do I debug running game physics live in C#?

Keep the probe and recording alive for the duration of the simulation. In the full program, StartRecording(feed) supplies the library’s replay bytes, feed.Record = probe.Record connects that stream to the probe, and Snapshots.WriteSnapshot supplies the initial world state. The viewer loads the OnePhysics module to interpret those bytes. The probe itself does not interpret the replay.

Call probe.Poll() from the simulation thread at step boundaries, including when no new records are being emitted. A new viewer needs the host to take its snapshot there. Connecting from a worker does not remove that requirement: a host that never polls can leave the connection waiting until it times out. The sample services the pending connection before advancing, which makes its initial step reproducible.

The viewer’s first available step is the host’s step at the snapshot. It cannot rewind into the part of the game that happened before attachment. Once records arrive, you can seek among the steps already received. The viewer reports the library’s own world hash, so compare hashes at the same step, not at the same wall-clock time. The host may already be further ahead when you read the result.

These behaviors are described in live attach. The sample uses a separate connection task only to demonstrate both sides inside one runnable console project; a game and the viewer normally live in separate processes.

How can I debug a live simulation without stopping the game?

Inspect the viewer’s reconstructed world rather than pausing the host. Following a live source moves the viewer cursor to the newest received step. Seeking away from the newest step turns following off; seeking back to it or using the follow operation turns it on. This lets you inspect an earlier contact while the host continues to produce records.

A slow viewer does not turn the stream into an unlimited buffer. The host can drop a viewer that falls behind. The viewer also has a record-byte cap; reaching it closes the connection and retains what was received. If the host exits or closes the probe, the received history remains usable. Reattaching starts a new session from a fresh snapshot, not from the old connection’s last step.

Use a fork when you want to experiment with an impulse or another declared physics change. Pokes apply to forks, not to the running host. See the shell operation reference for live-state fields and the follow, fork, and poke operations.

All onescope recipes