Unequal whole-world hashes are expected: only the entities both worlds hold are compared. OneScope debugs recorded simulation steps, such as physics; a fork is a branched copy of a recording.
dotnet tool install --global RecursiveAI.OneScope.ToolRequires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.
The examples use --headless, which renders offscreen but still needs a graphics device and driver. On Linux, a Vulkan software renderer such as lavapipe can supply it; ONEGAME_GPU_ADAPTER=software selects software rendering. Remove --headless to open the window.
onescope open full.sim --headless \ --module module.dll \ --run 'open {"path":"partial.sim"}' \ --run 'diff_shared {"source":1, "other":0,"step":2, "map":"identity"}'
full.sim is source 0, partial.sim is source 1. Both files and Module.cs are in the zip. A shared entity is one paired between the worlds; an inspector row is a named property exposed by the module for inspection.
- Install the global tool with the command above.
- Unzip the example and run
dotnet runin its folder to buildmodule.dlland run the comparisons. - Run the
onescopecommand above in that folder to request the comparison directly.
Unzip the example, then dotnet run.
The final operation returns:
{ "source": 1, "other": 0, "step": 2, "map": "identity", "equal": true, "shared": 2, "onlySource": 0, "onlyOther": 1, "hashSource": "00000000000007d2", "hashOther": "0000000000000bba", "difference": null }
The full program
The sample module models child counters under a world root. All replay inputs are shown here in full:
// full.sim {"entities":3,"bias":0} // partial.sim {"entities":2,"bias":0} // changed.sim {"entities":2,"bias":1}
The file contents are the JSON objects only, without the filename comments. Module.cs creates three recorded steps, increments a counter each step, and exposes Value as an inspector row. Bias changes each child’s value without changing its identity. The root exposes the child count and is deliberately excluded by the shared-entity comparison.
The launcher uses the SDK module defined in the same project and installs the published tool into obj. For a new module project, the dependency is:
dotnet add package RecursiveAI.OneScope.Sdkusing System.Diagnostics; using System.Text.Json; 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); string assembly = typeof(CounterModule).Assembly.Location; File.Copy(assembly, "module.dll", true); string Load(string path) => "open " + JsonSerializer.Serialize(new { path, moduleAssembly = assembly }); var results = Open("full.sim", Load("partial.sim"), "diff_shared {\"source\":1,\"other\":0,\"step\":2,\"map\":\"identity\"}"); var same = results[^1]; Console.WriteLine($"Shared equal: {same.GetProperty("equal")}"); Console.WriteLine($"Shared: {same.GetProperty("shared")}; only partial: {same.GetProperty("onlySource")}; only full: {same.GetProperty("onlyOther")}"); var changed = Open("full.sim", Load("changed.sim"), "diff_shared {\"source\":1,\"other\":0,\"step\":2,\"map\":\"identity\"}"); var different = changed[^1]; Console.WriteLine($"Changed equal: {different.GetProperty("equal")}"); Console.WriteLine($"First differing field: {different.GetProperty("difference").GetProperty("field")}"); string Run(string command, params string[] arguments) { var start = new ProcessStartInfo(command) { RedirectStandardOutput = true, RedirectStandardError = true }; foreach (string argument in arguments) start.ArgumentList.Add(argument); 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; } JsonElement[] Open(string file, params string[] operations) { var arguments = new List<string> { "open", file, "--headless", "--size", "640x480", "--artifacts", "output", "--module", typeof(CounterModule).Assembly.Location }; foreach (string operation in operations) arguments.AddRange(["--run", operation]); return Run(tool, arguments.ToArray()) .Split('\n', StringSplitOptions.RemoveEmptyEntries) .Select(line => JsonDocument.Parse(line).RootElement.Clone()).ToArray(); }
dotnet run prints:
Shared equal: True Shared: 2; only partial: 0; only full: 1 Changed equal: False First differing field: Value
A ghost pose-cache source cannot be used with diff_shared; the operation refuses it instead of comparing its drawn positions.
Variations
Change the second opened file to changed.sim, keeping the same diff_shared operation. Its result is:
{ "source": 1, "other": 0, "step": 2, "map": "identity", "equal": false, "shared": 2, "onlySource": 0, "onlyOther": 1, "hashSource": "00000000000007d3", "hashOther": "0000000000000bba", "difference": { "entity": 2, "field": "Value", "source": "3", "other": "2" } }
For agreement over the shared recording range, replace the diff_shared operation with:
compare {"source":1,"other":0, "kind":"shared"}
This is a --run operation string. For partial.sim, agreement.basis is shared entities, divergentSteps is 0, and complete is true.
Related recipes
- Save and reopen a debugging session
- Compare library versions
- Overlay a reference pose cache
- Write a reference pose cache
The short answer
Use diff_shared for a partial world against a full world. Equal ids are paired with map:"identity"; map:"links" uses the modules’ declared entity links. OneScope compares the shared entities, counts entities present on only one side, and returns the first shared-field difference. Unequal whole-world hashes do not by themselves mean the shared entities differ.
In the example, the partial world has two child counters and the full world has three. Both shared children have the same inspected value. The result is equal:true, with two shared entities and one entity only in the full world. The returned hashes differ because the worlds do not contain the same state. That is expected, not a contradiction.
How do I compare partial and full simulation state?
Open both sources and identify their source numbers. Set source, other, and the step you want to inspect. State which mapping applies instead of relying on similar display names. With identity mapping, entity 2 in one world is paired with entity 2 in the other; a reused or unrelated id would make that pairing misleading.
The default SDK implementation compares inspector rows, matched by group and name. It skips root entities, those with no parent, because they commonly describe the entire world. Comparing an aggregate body count would incorrectly flag the intended difference between a partial and complete world. Child entities present on only one side are counted but not compared.
The changed sample keeps the same two child identities but increases their values by one. At step 2 it reports entity 2’s Value as 3 on the partial side and 2 on the full side. That is the first difference in the first world’s entity order, not an ordered list of every possible difference. Reverse the source order and the one-sided counts and value labels reverse their roles.
How do I compare entities with different ids?
At the SDK level, EntityMap.Identity pairs matching ids. EntityMap.FromPairs accepts explicit entity pairs; ids not listed are not shared. In the shell, map:"links" obtains the pairing from declared links between sources. Those links name the other source through SourceInfo.LinkKey, or its name when no link key is set.
Use links when modules explicitly identify the same logical entity across different source ids. They are not fuzzy name matching and do not discover correspondence from similar positions. If your modules have no links, requesting link mapping is not a substitute for supplying the missing relationship. The sample uses identity because its ids are intentionally stable.
For a timeline view, compare with kind:"shared" gives agreement based on shared entities and lets you navigate divergent steps. Its scan is capped at 10,000 steps. Check agreement.complete to see whether the full range was scanned. A successful shared comparison establishes equality of the exposed shared rows, not equality of unexposed internal state or of the complete simulations. See the shared-entity contract.