Everything a contributor needs to build, test, benchmark and extend NationSim.
| Tool | Version | Notes |
|---|---|---|
| .NET SDK | 10.0.100 or later (the SDK is pinned in global.json with rollForward: latestFeature) |
The ASP.NET Core runtime is needed for the server |
| An IDE | Rider, Visual Studio 2022 17.13+ or the .NET SDK + your favourite editor | The solution is a classic .sln |
No other tool is required: the package restore pulls the ECS framework, the serializer, the console library and the benchmark harness from NuGet.
dotnet --version # 10.0.201 for exampledotnet restore # restore the packages
dotnet build # build the debug configuration
dotnet build -c Release # build the release configuration
dotnet test # run the 125 tests
dotnet test --filter "FullyQualifiedName~Replay" # run one suite
dotnet run --project src/NationSim.UI.Console # start the interactive console
dotnet run --project src/NationSim.Server # start the headless serverdotnet run -c Release --project tests/NationSim.Core.Tests -- --filter *Benchmark*
dotnet run -c Release --project tests/NationSim.Core.Tests -- --filter *Benchmark* --job shortThe memory diagnoser of BenchmarkDotNet prints the allocated bytes per tick; it must
stay at 0 B. The benchmark runs with 12,000 citizens, 1,200 firms and 16 regions, so
the hot path is exercised at a realistic size.
The benchmark project is an executable: the test project has a Program.cs that hands the
command line to BenchmarkSwitcher. dotnet test still discovers and runs the xUnit
tests of the same assembly.
NationSim.sln
├── src/
│ ├── NationSim.Core/ The primitives: the tick lifecycle, the event bus, the PRNG,
│ │ the macro math, the modifiers, the commands, the metrics
│ ├── NationSim.Domain/ The aggregates and the value objects
│ ├── NationSim.Simulation/ The ECS world, the components, the systems, the scheduler
│ ├── NationSim.Infrastructure/ The file formats, the telemetry, the channel queue, the host
│ ├── NationSim.Server/ The headless server (ASP.NET Core)
│ └── NationSim.UI.Console/ The interactive console
├── tests/
│ └── NationSim.Core.Tests/ The unit, integration, time travel, replay and server tests
├── docs/ This documentation
├── Directory.Build.props The shared compiler settings
└── global.json The pinned SDK
Directory.Build.props applies to every project:
| Setting | Value | Why |
|---|---|---|
TreatWarningsAsErrors |
true |
The build must be clean, a warning is a defect |
Nullable |
enable |
The nullability annotations are part of the contract |
ImplicitUsings |
enable |
Keeps the files small |
LangVersion |
latest |
The modern C# syntax is used throughout |
Deterministic |
true |
The builds are reproducible |
InvariantGlobalization |
true |
The formatting must not depend on the locale of the host |
AllowUnsafeBlocks |
true |
Available for the low level buffers |
| Suite | Tests | What it covers |
|---|---|---|
Primitives |
8 | The PRNG: the reproducibility, the ranges, the capture and restore |
Events |
5 | The event bus: the order, the unsubscription, the allocation free publishing |
Tick |
4 | The tick lifecycle: the phase order, the clock, the context, the allocation |
Domain |
24 | The aggregates: the voting, the bills, the treasury, the ministries |
Economics |
28 | The macro engine, the national accounts and the emergent economy |
Regions |
9 | The regional graph, the topology and the distribution |
Ministries |
9 | The ministries, the modifiers and the law routing |
Commands |
6 | The command queue and the input phase |
Metrics |
6 | The telemetry ring buffer |
Memory |
2 | The allocation budgets of the tick and of the state hash |
Determinism |
15 | The determinism and the time travel across the snapshot |
Replay |
5 | The recording, the binary format and the replay |
Server |
4 | The WebSocket stream and the HTTP endpoints |
| Total | 125 |
-
Derive from
ISimulationSystemand pick the phase:public sealed class MySystem : ISimulationSystem { public string Name => nameof(MySystem); public TickPhase Phase => TickPhase.Execution; public void Update(in SimulationContext context) { /* ... */ } }
-
If the system reads the components, add a query description to
QueryDescriptionsand cache the handler in a field:private readonly ForEach<Citizen> _handler; public MySystem() => _handler = OnCitizen;
A closure that captures
thiswould allocate a delegate on every tick. -
If the system runs in parallel, take a
RegionSchedulerand a cachedAction<int>and remember that every region has to be independent. -
Register the system in
SimulationKernelin the right phase.
- Put the aggregate in
NationSim.Domainand keep it free of any reference to the simulation layer. - Publish its changes as immutable
record structevents on the event bus. - Let a system in
NationSim.Simulationtranslate the event into the state of the ECS.
The endpoints live in NationSimServerHost.MapEndpoints. Capture the engine in the
closure; a parameter of the handler would be inferred as the request body.
The complete list is in the architecture reference. The three that break the most often:
- Never destroy an entity inside the tick. Use the
IsDeadflag: the chunk layout of the ECS is LIFO and a removal would permute the iteration order, which changes the order in which the random source is consumed. - Never rely on the iteration order of a query. Use the agent id: the working set uses it as its slot, which is what makes a restored run identical to the original.
- Never allocate on the hot path. The benchmark is the judge.
| Symptom | Where to look |
|---|---|
| The state hash differs between two runs | The random streams (the region streams are in the snapshot), the order of the agents (the slot is the agent id) |
| A replay diverges | The tick at which the command was recorded; the input phase applies it before the tick |
| The benchmark allocates | GC.GetTotalAllocatedBytes in a diagnostic; look for LINQ, closures and Parallel.For |
| The economy collapses | The EmergentEconomyTests show the wealth and the demand; a tax rate that is too high starves the demand |
| The server does not answer | The engine host logs; the UseWebSockets middleware is not part of the default pipeline |
The process wide allocation of a tick catches the allocations of the worker threads that the thread local probe cannot see:
long before = GC.GetTotalAllocatedBytes(precise: true);
for (int i = 0; i < 20; i++) kernel.RunTick();
long allocated = GC.GetTotalAllocatedBytes(precise: true) - before;Before a release:
dotnet clean
dotnet build # 0 warnings, 0 errors
dotnet build -c Release # 0 warnings, 0 errors
dotnet test # 125 tests
dotnet run -c Release --project tests/NationSim.Core.Tests -- --filter *Benchmark*And then update the release audit report with the measured numbers.