Waiting
How a test waits on the game, and what an owned launch waits on at startup. Owning and stopping a server is on Owned server session. The types are in Valheim.Testing.Game; hosts on other machines are in Valheim.Testing.GameSessions.
Waiting
Wait for the event that announces a change, never for time to pass. Every wait takes an explicit, finite timeout; there is no default.
| Outcome | Exception | Reports |
|---|---|---|
| The timeout expires | WaitTimeoutException (a TimeoutException) |
what was awaited, the elapsed time, the last thing seen |
| The wait fails early | WaitFailedException (an InvalidOperationException) |
the same, plus Context for a log failure |
| Source | Wait | Notes |
|---|---|---|
| A log line | LogWait |
Opens at the file's current end or a given byte offset; the file may not exist yet |
| A process exit | Raced for you | Owned sessions race every startup stage against the process exit |
| A game state | StateWait |
Subscribes to ValheimCLI's state pushes on a connection used for nothing else |
| No event | ObservedWait.Until / UntilAsync |
The one polling fallback: re-observes a read-only source until it matches |
LogWait matches complete lines only, checks the optional failure patterns first, and re-reads a truncated or replaced file from its start. A FileSystemWatcher wakes it; a re-read every SafetyInterval (2 s) covers filesystems that drop watcher events. Open it before launching:
using var log = new LogWait(Path.Combine(runtime, "BepInEx", "LogOutput.log")); // before launching
// ... start the server ...
await log.WaitAsync(StartupEvents.CliListening, TimeSpan.FromMinutes(5), [LogWait.Literal("[Fatal")]);
For a process you start yourself, use Process.WaitForExitAsync with a token from new CancellationTokenSource(timeout).
StateWait.Connect(host, port) asks the current state once, then awaits STATE_CHANGED pushes with a pending read; nothing is sent while it waits, and a push that arrives during the question counts. A dedicated server's loaded world is InWorldNoPlayer; StateWait.WorldLoaded lists both loaded states. A state is not mod readiness. The transport keeps no history, so a wait sees pushes from its own subscription on. A failure state, a closed connection or a line that is not a push fails the wait at once. When a wait expires or is cancelled, StateWait closes its connection, because the abandoned read leaves the stream's position unknown; later waits on that instance fail, so connect a new one.
ObservedWait takes an optional fails that ends the wait at once with a reason, a describe that names the last observation on expiry, and a changed event (such as a LogWait for the line the change writes) that ends each pause early. The token cancels the pause too.
GameActor.WaitForEnvironment takes an event too, for example (left, token) => log.WaitAsync(loadLine, left, cancellation: token): it rechecks the pins when the line appears rather than every 200 ms. Pins are on Actors and pins.
Startup events
StartupEvents tells an owned launch what to wait on. ServerRunPlan.DedicatedStartupEvents(runtime) builds the dedicated server's; owned client launches use the same failure list.
| Field | Does |
|---|---|
CliLog |
This boot's BepInEx log; lines from an earlier boot never count |
Failures |
Patterns that end startup at once (StartupEvents.StartupFailures) |
States |
Opens the StateWait for the world-loaded push, for example () => StateWait.Connect(host, port) |
ReadyStates |
The states that count as loaded; the dedicated plan sets [StateWait.InWorldNoPlayer] |
OwnedServerSession and ClientSession wait on these in order, and race every stage against the process exit.
StartupFailures joins two groups:
| Group | Ends startup on |
|---|---|
BepInExPluginLoadFailures |
BepInEx's own "Could not load [" and "Error loading [" lines |
RuntimeLoadFailures |
A TypeLoadException, MissingMethodException or MissingFieldException in a warning, error or fatal line, or an exception line starting with its name; an older split ValheimCLI pack logging CLI core 1.1 is not ready. |
An info or debug line that only mentions one of these exceptions, such as a handled soft dependency, does not end startup. A log failure carries the matched line and the five lines read before it in WaitFailedException.Context and in its message. The same regexes fail the log scan at teardown.
World entry has one special case: a mod may hold Unity's main thread while ValheimCLI's socket still answers. If ValheimCLI reports during a bounded world-entry wait that a read-only command timed out before it started, GameActor reads STATUS on a separate connection; a stale mainThreadIdleMs means the game is busy, so it waits within the original deadline and sends the observation again once the heartbeat resumes. No mutation is retried, a missing STATUS reply fails at once, and a fresh heartbeat paired with an unstarted timeout is reported as a harness fault. ClientRounds records the longest frame age as clientMainThreadStallMs and the last busy note as clientBusyNote in the report's provenance.