Zaldaryon.Pharos.Templates
0.8.0
dotnet new install Zaldaryon.Pharos.Templates@0.8.0
<p align="center"> <img src="docs/pharos-logo.svg" alt="Pharos logo" width="120" height="120" /> </p>
Pharos
In-process headless test harness for Vintage Story client and server systems. Pharos boots a real Vintage Story client or server inside your test process, drives it frame by frame or tick by tick, and exposes rendering and world-state inspection APIs to xUnit test code.
What It Does
Pharos runs Vintage Story inside your test process. The client loads chunks, meshes geometry, and issues OpenGL draw calls exactly like the normal client, but with no visible window and no audio. The server runs a full simulation loop with a real player registry and savegame stack, but in an isolated temporary sandbox. Your test code controls the frame and tick loops, positions the camera, inspects rendering state, places blocks, and asserts on results.
Use cases:
- Rendering regression tests for client-side mods
- Server-side integration tests without a running game instance
- Performance benchmarks that measure actual draw calls and allocations
- Visual diff tests comparing framebuffer output across versions
- Automated smoke tests for mod compatibility
- Lockstep client-server end-to-end scenario testing
Requirements
- Vintage Story 1.22.x (1.22.7 recommended)
- .NET 10 SDK
- OpenGL 4.3+ for full inspection features (3.3 for basic headless)
- Windows or Linux (macOS not supported)
See the compatibility matrix for version details.
Installation
Client scenario testing
Add the Pharos XUnit metapackage to your test project:
dotnet add package Zaldaryon.Pharos.XUnit
This brings in the core library, xUnit integration, and xUnit itself.
Server scenario testing
The server APIs are part of the same package. No extra package is needed.
Migrating from Pixnop.Atlas
If you have existing Atlas test suites, install the compatibility shim for zero-code-change migration:
dotnet add package Zaldaryon.Pharos.AtlasCompat
Or use the CLI migration tool:
pharos migrate-atlas ./your-test-project
See Migrating from Atlas for the full guide.
Set the VINTAGE_STORY environment variable to your game installation:
# Windows (PowerShell)
$env:VINTAGE_STORY = "C:\Program Files\Vintage Story"
# Linux
export VINTAGE_STORY=~/.local/share/vintagestory
Quick Start: Client Scenarios
Create a test class that inherits from ClientScenarioBase and mark test methods with [ClientScenario]:
using Zaldaryon.Pharos.XUnit;
using Xunit;
public class ChunkRenderingTests : ClientScenarioBase
{
[ClientScenario]
public void ClientLoadsChunksAroundPlayer()
{
Player!.SetPosition(0, 100, 0);
Player.LookAt(32, 64, 32);
FrameController!.Frames(60);
Assert.True(Client!.WorldMap.LoadedChunks > 0);
}
}
Quick Start: Server Scenarios
Inherit from ServerScenarioBase and use [ServerScenario]:
using Zaldaryon.Pharos.XUnit;
using Xunit;
[ServerWorld(seed: 12345, playStyle: "creativebuilding", worldType: "superflat")]
public class InventoryTests : ServerScenarioBase
{
[ServerScenario]
public async Task GivingItemToPlayer_UpdatesInventory()
{
var player = await CreateTestPlayerAsync("TestPlayer");
player.GiveItem("game:sword-iron");
Assert.True(player.HasItem("game:sword-iron"));
}
}
Quick Start: Client-Server Scenarios
Combine both in one locked-step test with [ClientServerScenario]:
using Zaldaryon.Pharos.XUnit;
using Xunit;
public class BlockInteractionTests : ClientServerScenarioBase
{
[ClientServerScenario]
public async Task BreakingBlock_UpdatesBothSides()
{
var pos = new BlockPos(10, 64, 10);
Server.Api.World.BlockAccessor.SetBlock(1, pos);
await Session.StepUntilAsync(() => ClientServerAssert.BlockSynced(Session, pos));
ClientServerAssert.ClientServerBlockSynced(Session, pos);
}
}
Quick Start: Smoke-Test a Mod
Check that a mod loads, joins and plays without errors, with no test project:
dotnet tool install -g Zaldaryon.Pharos.Cli
export VINTAGE_STORY=/path/to/vintagestory
xvfb-run -a pharos smoke --mod bin/Release/mymod.zip --strict
Or, in a test project, inherit the ready-made scenario:
[ServerMods("../../../../MyMod/bin/Release/mymod.zip")]
[StrictBoot]
public class Smoke : ModSmokeTest;
See Smoke Testing a Mod.
To run a live suite faster, pharos run runs its test classes in parallel worker processes and
merges their results into one TRX:
pharos run tests/MyMod.Tests --filter "Category=Live" --parallel 2 --trx results.trx
See Parallel Runs.
To compare a change against a baseline and see what each metric did, pharos compare runs the same
tests on both sides, alternating and repeating them:
pharos compare tests/MyMod.Tests --baseline origin/main --repeat 5 --budgets pharos.compare.json
See Comparing Before and After.
To build a test world, stamp in a WorldEdit schematic, or boot each class from a saved world that
pharos fixture made from a builder test:
await this.PlaceSchematicAsync("Fixtures/furnace-room.json", new BlockPos(512, 4, 512));
pharos fixture tests/MyMod.Tests --scenario VillageBuilder.Build --out tests/MyMod.Tests/Fixtures/village.vcdbs
See Test Worlds.
To see where the server's tick time goes, and who the traffic is for, measure a stretch of ticks:
ServerProfile profile = await Server.Profile.MeasureAsync(ticks: 600);
Assert.True(profile.Mods["mymod"].MeanMsPerTick < 0.5);
Assert.True(profile.BlockEntities["BlockEntityQuern"].P95MsPerTick < 1);
profile.Publish(); // feed the numbers into result.json and pharos compare
See Server Profiling.
To see what worldgen costs, what a save writes, and how much of the world is loaded:
WorldgenProfile worldgen = await Server.Worldgen.MeasureAsync(64, 64, 65, 65);
SaveProfile save = await Server.Saves.MeasureAsync();
WorldStats stats = Server.WorldStats;
Assert.True(worldgen.MeanMsPerColumn < 5);
Assert.True(save.TotalBytes < 100_000_000);
Assert.True(stats.LoadedChunks > 0);
See World Metrics.
Quick Start: A Real Client Joining a Server
Boot the client in engine mode to run the vanilla client startup. The client joins the server, goes through character creation, receives chunks and renders the world through the game's own pipeline. It needs no auth server and no outbound network access:
using var server = EmbeddedServerHost.Boot(new ServerWorldOptions { WorldType = "superflat" });
using var client = HeadlessClientBootstrap.Boot(new HeadlessClientOptions { BootMode = ClientBootMode.Engine });
using var session = client.ConnectLoopback(server, "Pilot");
Assert.True(session.WaitForPlayerJoined(TimeSpan.FromSeconds(120)));
FramebufferSnapshot frame = client.CaptureFrame();
See Engine Mode for what runs and how the offline join works.
Run tests with:
dotnet test -c Release
On Linux without a display:
./scripts/run-headless-linux.sh
Packages
| Package | Description |
|---|---|
| Zaldaryon.Pharos | Core headless client bootstrap, frame stepper, and inspection APIs |
| Zaldaryon.Pharos.Bridge | In-client mod exposing inspection hooks to scenario code |
| Zaldaryon.Pharos.XUnit | xUnit integration: [ClientScenario], [ServerScenario], [ClientServerScenario] |
| Zaldaryon.Pharos.AtlasCompat | Drop-in compatibility shim for Atlas.Api and Atlas.XUnit |
| Zaldaryon.Pharos.Cli | The pharos command: pharos smoke, pharos run --parallel, benchmarks, Atlas migration. Uses your own game install |
Install Zaldaryon.Pharos.XUnit for client and server testing. Install Zaldaryon.Pharos.AtlasCompat to migrate existing Atlas test suites without code changes.
Platform Support
| Platform | Method | Notes |
|---|---|---|
| Windows | Hidden GLFW window with FBO | Works on any GPU |
| Linux | Mesa llvmpipe under Xvfb | Works in CI without a GPU |
| macOS | Not supported | GLFW cannot create headless OpenGL contexts |
Both Windows and Linux produce identical rendering behavior and report OpenGL 4.5 core profile.
Inspection APIs
Client rendering
| Class | Purpose |
|---|---|
GlCommandProxy |
Count draw calls, buffer allocations, and VAO operations |
CullingInspector |
Query which chunks are visible, frustum-culled, or occlusion-culled |
MemoryInspector |
Track mesh pool hit/miss rates and type allocations |
ShaderInspector |
Intercept uniform uploads and detect redundant state changes |
FramebufferSnapshot |
Capture pixels for visual regression comparisons |
IndirectDrawInspector |
Verify GPU indirect draw dispatch and command buffer contents |
Server and network
| Class | Purpose |
|---|---|
EmbeddedServerHost |
Native embedded server with tick control and sandbox isolation |
ServerSandbox |
Isolated temp directory, mod staging, SQLite-safe cleanup |
WorldSnapshot |
In-memory world state capture and rollback |
PacketRecorder, RecordedSession |
Record client and server packets with their frames, and replay them into a client or a server |
Host.StateHash |
Hash the server's blocks, entities and players after each tick, to find where two runs diverge |
PacketTracer |
Assert that specific packet IDs cross the loopback boundary |
NetworkDegradationSimulator |
Inject latency, packet drop, and jitter |
Server.ModNetwork, Server.Visibility |
Record per-player mod messages and inspect actual chunk/entity tracking |
PlayerLayout, Server.SameTick |
Join player crowds and queue reproducible same-tick bot actions |
Assertions
| Class | Purpose |
|---|---|
PharosAssert |
Client-side rendering assertions (draw collapse, chunk visibility, UV, GL leaks) |
ClientServerAssert |
End-to-end block sync, position sync, inventory sync assertions |
See the inspection API reference for method signatures and examples.
Documentation
- Writing Client Scenarios:
[ClientScenario],[ClientTheory],[PharosMods], and isolation modes - Writing Server Scenarios:
[ServerScenario],ServerScenarioBase,IServerTestPlayer, and world helpers - Engine Mode: Booting a client that joins a real server offline, with character creation and the vanilla render pipeline
- Smoke Testing a Mod:
pharos smokeandModSmokeTest, in a terminal or in CI - Doctor and Init:
pharos doctor, which checks the machine, andpharos init, which writes a first test project - Game-state snapshots: Verify converters, block regions, runtime-value scrubbing and game-version baselines for xUnit v2 and v3
- Templates and the Reusable Workflow:
dotnet new pharos-mod-tests,dotnet new pharos-ciandmod-ci.yml, which runs a mod's tests on Linux and Windows - Boot Diagnostics: The warnings a client or server logs while booting,
[StrictBoot]and[AllowBootDiagnostic] - Parallel Runs:
pharos run --parallel, which runs test classes in worker processes and merges their results - Run Reports:
pharos report, which turns a run into an HTML page, a Markdown summary and timings - Test Worlds: Schematics, saved worlds with
[ServerWorld(SaveFile = ...)], andpharos fixture - Failure Artifacts and the Watchdog: The screenshot, logs, traffic and run details a failing scenario saves, and the timeout that stops a hung one
- Metrics: Named per-test metrics, automatic frame/tick/memory/GL/network/world collectors, and the
result.jsonschema - Comparing Before and After:
pharos compare, which runs the same tests against a baseline and reports each metric's delta, verdict and budget - Server Profiling:
Server.Profile.MeasureAsync, which breaks a measured stretch of ticks down by system, mod, block entity, entity, behavior, AI task and player traffic - World Metrics:
Server.Worldgen,Server.SavesandServer.WorldStats, which attribute generation by pass and handler, split a save by category and mod data key, and count the loaded world - Mod Loading:
Server.ModsandClient.Mods, which report mod states, systems, run order, JSON patch outcomes, compatibility assets and asset origins - Content Sweep:
[ContentSweep]andContentSweep.RunAsync, which place, break, round-trip and spawn each code from a mod - Commands:
Server.Commands, which reads a mod's command tree, runs its parsers without its handler, and records what the chat handlers did - Held Items: the held-item hooks, the mining times and the durability, and the holds a client or a bot makes
- Containers and Item Flow: the inventories of the chests, the crates and the ground, the totals of a region, and what the hoppers moved
- Bags, Wearables and Liquids: the contents of a bag, the gear a player wears, and the litres a container holds
- Spoilage and Temperature: the transitions of a stack, the rates they run at, and the temperature of an item
- Projectiles: how a shot flies, what it hits and what happened to its stack
- Block outcomes: the drops of a block, an explosion and the hooks a block calls
- Block ticks and snow: ticking a block on demand, and the snow that lies on it
- Animals: what an animal breeds, how it grows and what a hen box holds
- Traders and villagers: what a trader offers, what a trade did and what a villager says
- Temporal stability: the stability of a place, storms, rifts, rift wards and temporal gears
- Multiplayer: player crowds, server mod channels, per-player visibility and same-tick inventory contention
- Server administration: the whitelist, the bans, the kicks and the data the server keeps of its players
- Configuration:
[WorldConfig],[ServerConfig]and the configuration matrices, which set a scenario's configuration before the server boots and put it back after a test - CI and Cloud Environments: Running the suite in CI and cloud environments, offline or online auth, and a dedicated server in Docker
- Client-Server Testing:
[ClientServerScenario], lockstep stepping, and packet assertions - HUD inspection: composed stat bars, hotbar, block/entity info, interaction help, messages, boss bars and custom HUD layouts
- Inspection API Reference: Detailed API for all inspector classes
- Compatibility Matrix: Supported VS versions, OpenGL requirements, and platform notes
- Migrating from Atlas: Drop-in shim and step-by-step migration guide
- Roadmap: Development history and phase completion status
- Contributing: How to contribute to Pharos
Status
v0.3.0 complete. All 36 issues across milestones M1 through M14 are merged. Packages are available on NuGet. See the roadmap for the full history.
License
Support
If Pharos helps your mod development, consider supporting via Ko-fi.
-
net10.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.8.0 | 60 | 10/26/2026 |