On this page

Development

Building and testing #

You need the .NET 10 SDK and a Vintage Story 1.22.x install, with VINTAGE_STORY pointing at the folder that holds VintagestoryAPI.dll and VintagestoryLib.dll (the .pdb next to the first is required too, or the engine's logger crashes at boot). Both dlls are compile-time references only; neither is copied into the mod, which still ships as one file.

sh
export VINTAGE_STORY=/path/to/vintagestory
dotnet build Pulse.slnx -c Release
dotnet test                      # unit tests, then the Atlas scenarios
dotnet build Pulse/Pulse.csproj -c Release -t:PackageMod            # artifacts/pulse_x.x.x.zip
dotnet build Pulse.Otlp/Pulse.Otlp.csproj -c Release -t:PackageMod  # artifacts/pulseotlp_x.x.x.zip

The scenarios in Pulse.Scenarios boot a real headless server in-process through Atlas, load the mod, and scrape it over HTTP for real. The atlas CLI runs the same assembly without VSTest, which is faster to iterate against:

sh
atlas run Pulse.Scenarios/bin/Release/net10.0/Pulse.Scenarios.dll
atlas run Pulse.Otlp.Scenarios/bin/Release/net10.0/Pulse.Otlp.Scenarios.dll

Pulse.Otlp.Scenarios is a separate project because it stages both mods, laid out exactly as their zips are, and the base suite's staging should stay as it is. It stands up a fake collector, points the mod at it, runs the world, and asserts on the protobuf that arrives. There is one collector per protocol the config accepts: an HttpListener for http/protobuf, and for grpc a small HTTP/2 server, since a gRPC client wants its own service path, a length-prefixed message and a status trailer before it calls an export delivered.

Unit tests in Pulse.Tests cover the aggregator, the exposition writer, the log classifier and the small classes behind the wave of engine and world metrics: the busy-time average, the ping aggregates, the entity top-ten with its series retirement rule, and the suspend window. None of them needs a server. Pulse.Otlp.Tests covers the config translation, which is where the OTLP mod's only non-obvious logic lives. CI also runs both unit suites on Windows. The scenarios stay on Linux, since they boot a server build made for it.

Mutation testing runs at two depths. tools/mutation-check.sh applies ninety-one representative mutations one at a time and requires the suite to fail on every one; CI runs it on every code change, deterministic and done in about six minutes. .github/workflows/mutation.yml runs dotnet-stryker incrementally on pull requests into dev touching Pulse/: it mutates only the files the pull request changed and reports without gating. The full run mutates the whole project except the files that only run under a live server, scored 94 percent at the 0.2.0 release, fails below 83, and runs weekly and on workflow_dispatch. Pulse.Otlp's own Stryker lane is workflow_dispatch only, because Stryker launches the wrong project's test host for it and the score swings too much between runs to gate a pull request on; it scored 81 percent at 0.2.0 and fails below 70.

Static analysis runs on SonarCloud for pushes to dev and pull requests into it. The job waits for the quality gate, so a failing gate turns the check red instead of sitting unnoticed on SonarCloud's side. The gate judges new code only: an A rating for reliability, security and maintainability, at least 80 percent coverage, at most 3 percent duplication, and every security hotspot reviewed. The coverage badge at the top of this page comes from the same job, which runs the two unit suites and the base scenarios under coverlet. Five files are left out of that figure: the two ModSystems, the engine probe, the attribution probe and the live-server half of the attribution metrics. The game's loader loads the staged dll outside coverlet's instrumentation, so nothing a scenario executes in them can reach a coverage report. The scenarios are still what tests them.

The documentation site is built by docs/site/build.mjs from the README, the getting-started guide, the two contrib READMEs, the alert rules, the dashboard JSON and the changelog. It also reads both modinfo.json, NOTICE and the replies in Pulse/PulseCommands.cs, and it stops when any of these changes in a way a page depends on: a renamed section, a reworded /pulse reply. So a pull request that touches no document can still fail the pages workflow, which runs the build on every pull request and deploys the result from main. The build needs Node 22 or newer and has two dependencies.

sh
cd docs/site
npm ci
npm test         # the build rules and the logic of the client scripts
npm run build    # writes docs/site/dist
npm run serve    # serves it on http://127.0.0.1:4173 (needs Python 3)

Where this is going #

The survey in docs/metrics-feasibility.md lists what is measurable on this engine and what is not. Save duration is the honest gap: the world-save event fires before any writing happens and there is no completion signal, so Pulse reports the suspend window instead of inventing a number. Per-player traffic does not exist anywhere in the engine, not even internally.

Traces and logs over OTLP would reuse most of the exporter mod, but neither has an obvious consumer on a game server yet, so they stay unbuilt until someone asks.

License #

MIT, see LICENSE and NOTICE.

Source: README.md