A state hash detects when playback differs from the recorded crowd.
dotnet add package RecursiveAI.OnePathRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
replay.bin comes from the example zip and is created by the full program below. FindMismatch() returns a zero-based index from the start of the recording; null means all steps matched.
using RecursiveAI.OnePath.Replay; var file = new FileInfo("replay.bin"); if (file.Length > 2 * 1024 * 1024) throw new IOException("Too large"); var replay = CrowdReplay.Read( File.ReadAllBytes(file.FullName)); Console.WriteLine(replay.FindMismatch());
3Unzip the example, then dotnet run.
replay.bin in the zip deliberately has a changed expected hash at index 3. The snippet caps the input at 2 MiB and prints the first mismatching index.
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.
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)); for (int i = 0; i < 10; i++) recorder.Step(); var clean = recorder.ToReplay(); var hashes = clean.Hashes.ToArray(); hashes[3] ^= 1; // Deliberately change one expected hash, not the operation bytes. var altered = CrowdReplay.Create(clean.Setup.Span, clean.Steps, hashes); File.WriteAllBytes("replay.bin", altered.ToBytes()); var file = new FileInfo("replay.bin"); if (file.Length > 2 * 1024 * 1024) throw new IOException("Replay exceeds this example’s 2 MiB cap."); var replay = CrowdReplay.Read(File.ReadAllBytes(file.FullName)); Console.WriteLine($"First mismatching index: {replay.FindMismatch()}"); Console.WriteLine($"Clean replay matches: {clean.FindMismatch() is null}"); try { CrowdReplay.Read(new byte[4]); } catch (CrowdReplayException) { Console.WriteLine("Malformed bytes: CrowdReplayException"); }
dotnet run prints:
First mismatching index: 3 Clean replay matches: True Malformed bytes: CrowdReplayException
Malformed setup or replay bytes throw CrowdReplayException; edits to the mesh while recording make recorder.Step() throw InvalidOperationException.
Variations
Add this variation at the end of the full program to check a matching replay. It prints True.
// Use the clean replay created in the full program. Console.WriteLine(clean.FindMismatch() is null);
Related recipes
- Record and play a crowd simulation replay in C#
- Start crowd replay from the current simulation state in C#
- Publish live crowd simulation replay records in C#
- Restore a crowd’s saved state
The short answer
Call CrowdReplay.FindMismatch() after reading the replay. It restores the setup and compares the state after each simulated step against the saved hash. A non-null result is the first differing zero-based step index. Null means every recorded step matched. An exception is a different result: the setup or an operation could not be read or applied.
How do I find a crowd replay state hash mismatch in C#?
The downloadable program makes a small replay, copies its hashes, and changes just one expected hash with an XOR. It then writes and reads that altered replay. This creates a reproducible mismatch without depending on a particular rendering engine or a nondeterministic input source. The untouched replay still matches.
Index 3 means the fourth step stored in this replay, not necessarily absolute simulation step 3. A recording can begin after a crowd has already advanced. Use the setup’s initial step count when relating the returned index to the host’s timeline. The hash is taken after the step, so inspect the commands preceding that step and any earlier changes that bypassed recording.
Why does a simulation replay diverge?
A recorder sees only changes made through it. A direct destination change or stop on the live crowd member is not added to the replay’s operation bytes. Playback may remain structurally valid while its hash differs. Query filters are also not recorded; playback uses no filter. Mesh edits cannot be included in the command stream, and CrowdRecorder.Step() throws if the mesh was edited.
A mismatch says where the states first disagree, not which command is wrong. The return value does not describe a position delta or identify the particular crowd member responsible. Keep the original replay intact and compare the host’s command routing around the returned step. Changing a saved expected hash does not repair an incorrectly recorded run.
How do I handle invalid replay files?
Reading and reproducing a replay are separate checks. CrowdReplay.Read checks lengths against the available bytes before allocating. It rejects malformed framing with CrowdReplayException; playback can also throw that exception for invalid setup data or an operation referring to a missing crowd member. Do not label either exception as a hash mismatch.
The example’s file-size cap is an application policy, not a library format limit. It avoids reading an arbitrarily large file into memory before handing it to the parser. The format permits larger files; choose a cap appropriate to your application. The format documentation describes its step-count and operation-length bounds. A file that passes parsing can still fail later when its commands are applied, so keep exception handling around both reading and FindMismatch() when accepting external files.