Campaigns
A campaign is a run with several actors placed from an inventory: one dedicated server or none, and named clients, on this machine or others. A session manifest declares the actors; the toolkit prepares a profile for each, leases its Steam account and runs your plan on them.
PinnedServerRun.RunCampaignAsync runs one, and valheim-test session check checks one. Run a session with several clients walks through it.
The session manifest
These paths are illustrative:
{
"inventory": "environments.json", "world": "fixture-world", "join": "linux-box.example:2466",
"server": { "dependencyLock": "locks/server.json" },
"clients": { "player": { "dependencyLock": "locks/client.json",
"character": { "store": "characters", "registeredName": "tester", "fileName": "vt-player" } } }
}
| Field | Holds |
|---|---|
inventory |
The inventory file; left out, this machine alone |
world |
The pinned world fixture |
worldUid |
The fixture's reviewed world UID; another fixture is refused before copying |
join |
The server's game address with its port; none without a server |
server |
The dedicated server's role; left out, one client hosts the world |
clients |
The named clients' roles |
Relative paths are relative to the manifest. HostedCampaignManifest.Read refuses an unknown field.
| Role field | Holds |
|---|---|
dependencyLock |
The role's reviewed dependency lock: the mod, its dependencies and one pinned ValheimCLI (resolve dependencies) |
environmentCandidates |
Inventory environments to try, in order; empty tries every environment for the role |
differentHostFrom |
Actor names that must run on another host |
loaderPackage |
A reviewed loader package for this role's profile |
steamRuntimePackage |
A Mac dedicated server's SteamCMD libraries |
architecture |
A client's slice; empty inherits the environment's |
files |
Extra config, script or asset files for the profile |
character |
A client's disposable character: store, registeredName, fileName |
Each role has its own lock, so a server-only mod never reaches a client by accident.
Place the actors
Each actor gets an environment of its role from the inventory, by its environmentCandidates and differentHostFrom. If an earlier choice leaves a later actor without a host, the placement backs up and tries another. The output's prepared/environment-assignments.json records the choice; the observed Steam IDs are never written.
Check before a run
valheim-test session check SESSION [--hosts] [--json]
| Check | Without --hosts |
With --hosts |
|---|---|---|
| Manifest, locks, characters, fixture, inventory | Yes | Yes |
| Actor placement | Yes | Yes |
| Installs, loaders, ports and free space on each host | Yes | |
| Signed-in Steam accounts, and the account playing elsewhere | Yes | |
| Every host's run journal, the lease host's included | Yes | |
| Conflicting games | Yes |
It reports every independent problem in one pass and never copies or launches anything. It refuses while another run is LIVE on one of the campaign's hosts, left something there or on the lease host, or may still run elsewhere. A Valheim client already running where a campaign client needs the desktop session is a conflict.
Run it
return await PinnedServerRun.RunCampaignAsync(manifestFile, plan, clients, output, options);
clients maps each client name to its plan section. The run:
- checks the plan against the campaign;
- takes one lock per host and prepares every actor's profile concurrently, after the full preflight passes;
- binds the plan to the prepared actors: installs, ports, runtime, world, pins and characters;
- runs the plan as the pinned runner does;
- retires the characters, then each profile, then releases the leases and the host locks.
HostedCampaignPreparation.PrepareAsync and InspectAsync expose the preparation and the check to your own code.
Clients
session.OpenClient(plan.Client, "player") starts an owned client on its assigned host. On another machine, it starts in that host's desktop session (clients on a host). The client's host lock is held until teardown, and ValheimCLI is reached through its host's tunnel.
| Rule | Detail |
|---|---|
| Evidence | Each client writes into its own folder <output>/<name>/ |
| A client on this machine | Must use its role's CLI port; a plan naming another port is refused |
-savedir |
Refused in a campaign client's launch arguments: its character and world are staged in the default save folder |
| A client on a Linux host | Refused: Linux hosts run the dedicated server only |
| Steam account | Leased before anything else on its host (leases) |
Sign into Steam on each client host before the run. The runner never signs in or handles a Steam password.
Characters
Each client stages its disposable character into its host user's characters_local. It first checks that host's Steam userdata for a same-named Steam Cloud character, and refuses one.
| Host | characters_local |
|---|---|
| Windows | %USERPROFILE%\AppData\LocalLow\IronGate\Valheim\characters_local |
| macOS | ~/Library/Application Support/IronGate/Valheim/characters_local |
session check --hosts prints both resolved folders. Set charactersLocalDirectory or steamUserDataDirectory on the character only for a non-standard place. Two simultaneous clients need different player IDs, so register two characters; one seed copied twice is still one player.
A campaign without a dedicated server
Leave out server and join. One client hosts the world and the others are its peers. The manifest's world is the hosted fixture.
In the plan, exactly one client section has hostWorld, and every other sets joinsHost (HostedCampaignPreparation.CheckHostedPlan). Run it with PinnedServerRun.RunCampaignAsync(manifest, plan, clients, output, HostedRunOptions<TPlan>). The session's Host is the hosting client and its Clients are the peers.
The hosting client's world is placed in its host user's own worlds_local through the journalled placement:
- Refuses any entry already named for the world:
<name>,<name>.*or<name>_backup*, in any case. - Journals the placement, ships the verified fixture into
<runtime>/<runId>/host-world-stage, and checks every file. - Moves the world's entries into
worlds_local. - Once no client can host it, moves every entry named for the world, backups included, into
<runtime>/<runId>/host-world, and fetches it into the evidence.
Nothing in the user's worlds is ever deleted. hostWorldOn in the provenance names the folder.
A Mac dedicated server
Give the inventory a macos/bash/ssh host, and a server environment pointing to that Mac's SteamCMD server install. The server role adds two reviewed manifests:
"server": {
"dependencyLock": "locks/server.json",
"loaderPackage": "locks/native-mac-loader.json",
"steamRuntimePackage": "locks/steam-runtime.json"
}
Capture steam-runtime.json on the Mac (Steam runtime package), then copy it to the runner. session check --hosts checks the manifest, the arm64 loader, free space and the host session before any write.
Limits
- Two hosts signed in to one test account have not been run natively.
- Rented GPU VM clients are experimental.
- A crossplay host's peer join is refused.