---
title: "Publish live crowd replay records in C#"
url: https://recursiveai.org/onepath/cookbook/publish-live-crowd-replay-records/
description: "Send crowd changes to a consumer as they happen, with a setup snapshot so the consumer can reconstruct the same simulation."
version: "1.2.0"
---

# Publish live crowd replay records in C#

**OnePath** is a free, open-source pathfinding library for .NET from Recursive AI. MIT or Apache-2.0, no native dependencies.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OnePath) · [GitHub](https://github.com/recursive-ai-oss/onepath-csharp) · [Docs](https://recursiveai.org/onepath/docs/) · [Download example (.zip)](https://recursiveai.org/onepath/cookbook/publish-live-crowd-replay-records/publish-live-crowd-replay-records.zip)

Stream crowd changes and step hashes—checksums of the crowd state after each step—to a consumer, the code receiving the records. This is a callback, not a network transport.

```shell
dotnet add package RecursiveAI.OnePath
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

`setup.bin` comes from the example zip and is created by the full program below.

```csharp
using RecursiveAI.OnePath.Replay;

var setup = File.ReadAllBytes("setup.bin");
var crowd = new CrowdPlayer(setup).Crowd;
var recorder =
    new CrowdRecorder(crowd, 1.0 / 30);
var records = new List<byte[]>();
recorder.Record = bytes =>
    records.Add(bytes.ToArray());
recorder.Step();
Console.WriteLine(records.Count);
```

```text
1
```

Unzip [the example](https://recursiveai.org/onepath/cookbook/publish-live-crowd-replay-records/publish-live-crowd-replay-records.zip), then `dotnet run`.

`setup.bin` in the zip contains the square-floor crowd after step 1. The snippet publishes one step record to an in-memory consumer; it does not open a network connection.

## The full program

The program creates its own floor geometry and crowd in code; it needs no external assets. `RecursiveAI.OneMath`, used for the geometry types, is installed automatically as a dependency of OnePath. It regenerates the binary input used by the snippet at the top.

```csharp
using System.Buffers;
using RecursiveAI.OneMath;
using RecursiveAI.OnePath;
using RecursiveAI.OnePath.Replay;

var floor = new[]
{
    new Triangle3D(new(0, 0, 0), new(0, 0, 40), new(40, 0, 0)),
    new Triangle3D(new(40, 0, 0), new(0, 0, 40), new(40, 0, 40))
};
var crowd = NavMesh.Build(floor, NavSettings.Humanoid).CreateCrowd();
var recorder = new CrowdRecorder(crowd, 1.0 / 30, keepSteps: true);
var walker = recorder.Add(new(5, 0, 5));
recorder.SetDestination(walker, new(30, 0, 30));

// Attach at a completed step, on the same thread as all recorder calls.
recorder.Step();
var snapshot = new ArrayBufferWriter<byte>();
recorder.WriteSnapshot(snapshot);
File.WriteAllBytes("setup.bin", snapshot.WrittenSpan.ToArray());
var records = new List<byte[]>();
recorder.Record = record => records.Add(record.ToArray());
recorder.Stop(walker);
recorder.Step();
var player = new CrowdPlayer(snapshot.WrittenSpan);
foreach (byte[] record in records)
{
    if (record[0] == 1) player.Apply(record.AsSpan(1));
    else if (record[0] == 2)
    {
        player.Step();
        ulong expected = System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(record.AsSpan(1));
        if (player.Crowd.StateHash() != expected) throw new InvalidOperationException("Live hash mismatch");
    }
    else throw new InvalidDataException("Unknown record kind");
}
Console.WriteLine($"Delivered records: {records.Count}");
Console.WriteLine($"Consumer step: {player.Crowd.StepCount}");
Console.WriteLine($"Hash matches: {player.Crowd.StateHash() == crowd.StateHash()}");
```

`dotnet run` prints:

```text
Delivered records: 2
Consumer step: 2
Hash matches: True
```

Malformed setup or replay bytes throw `CrowdReplayException`; edits to the mesh while recording make `recorder.Step()` throw `InvalidOperationException`.

## Variations

Detach the consumer. Future records are dropped; the already copied list still has `2` entries.

```csharp
// After the delivery loop in the full program:
recorder.Record = null;
recorder.Step();
Console.WriteLine(records.Count);
```

## Related recipes

- [Record and play a crowd simulation replay in C#](https://recursiveai.org/onepath/cookbook/record-and-play-crowd-replay/)
- [Find a crowd replay state hash mismatch in C#](https://recursiveai.org/onepath/cookbook/find-crowd-replay-hash-mismatch/)
- [Start crowd replay from the current simulation state in C#](https://recursiveai.org/onepath/cookbook/start-crowd-playback-from-snapshot/)
- [Restore a crowd’s saved state](https://recursiveai.org/onepath/cookbook/crowd-rollback/)

## The short answer

Assign `CrowdRecorder.Record` a handler. It receives one record after each recorded change or step. Copy the supplied bytes if the consumer needs to retain them: the span is valid only during the callback. Give a new consumer a current setup snapshot before delivering subsequent records. The full example uses a local queue and a second crowd, with no network service.

## How do I stream live crowd simulation records in C#?

The recorder calls the handler synchronously after changes. A kind-1 record contains one operation after its leading byte. A kind-2 record contains the state hash after a simulation step. The consumer applies each operation, then advances once when it receives the corresponding step record and compares the resulting hash.

The full program attaches after a completed step. It captures a setup, installs the handler, stops the moving crowd member through the recorder, and steps. The consumer receives the stop operation and then the step hash. That preserves the distinction between a command issued between steps and a completed simulation step. Read the [record layout](https://recursiveai.org/onepath/docs/replay/) when implementing another consumer.

## How do I start a live consumer from a snapshot?

`WriteSnapshot` must describe exactly the state after the last record the consumer has received. In the example there are no prior delivered records, so the snapshot establishes the initial boundary. The callback is installed before the next operation. The snapshot and subsequent recorder calls all run on one thread; the recorder and player are documented for one thread at a time.

Do not capture a snapshot, allow the simulation to advance, and only then attach the handler. The consumer would miss the intervening commands or steps. Do not send old queued operations on top of a snapshot that already contains their effects. Starting with the right mesh but the wrong boundary is not enough to reproduce the host state.

## Can live crowd records be watched later?

Live delivery and retaining a replay are independent choices. Construct the recorder with `keepSteps: true` if you also need `ToReplay()` for later playback. With the default false value, the recorder does not retain a growing history of steps. The example’s list does retain its delivered records, so it is a demonstration queue, not an unbounded production streaming design.

The callback is not a network transport. It does not provide message acknowledgments, reconnection or backpressure. If you send records through a byte stream, preserve each callback’s message boundary in your own framing and retain their order; a stream read can split or combine writes. Copying to a byte array in the example makes the lifetime explicit. A handler must not call back into the recorder. For late attachment or reconnect, establish a new consistent snapshot boundary instead of silently skipping missing records. Changes that bypass the recorder, mesh edits and query filters cannot be reconstructed from this live stream.
