You are viewing a potentially older version of this package. View all versions.
Nosferatu-SmoothServer-0.5.0 icon

SmoothServer

Networking and performance tuning for Valheim dedicated servers: send cadence, adaptive per-peer budgets, async save, telemetry. Optional client half adds zstd compression and a shared map.

Date uploaded 13 hours ago
Version 0.5.0
Download link Nosferatu-SmoothServer-0.5.0.zip
Downloads 398
Dependency string Nosferatu-SmoothServer-0.5.0

This mod requires the following mods to function

denikson-BepInExPack_Valheim-5.4.2350 icon
denikson-BepInExPack_Valheim

BepInEx pack for Valheim. Preconfigured with the correct entry point for mods and preferred defaults for the community.

Preferred version: 5.4.2350

README

SmoothServer

Networking and performance tuning for Valheim dedicated servers — and, since 0.3.0, an optional client half in the same DLL.

The server half is the point of the mod and it still needs zero client installs: [General] EnforceClientMod defaults to false, so vanilla players can always join. Install the same package on players' machines as well and you additionally get packet compression, a client send budget and a server-wide shared map.

Installing

Server admins — install into the server's profile (BepInEx/plugins/SmoothServer/). Everything under "Server modules" below starts working immediately; nobody has to install anything. Config file: Nosferatu.SmoothServer.cfg in the server's BepInEx/config/.

Players — install into your own profile only if the server also runs SmoothServer 0.3.0+. [General] Mode is Auto: a dedicated server runs the server half, your game runs the client half. You get compression, the client send budget and the shared map; server-owned settings are pushed to you by the server.

The package contains six DLLs, not one: SmoothServer.dll plus ZstdSharp.dll, System.Memory.dll, System.Buffers.dll, System.Numerics.Vectors.dll and System.Runtime.CompilerServices.Unsafe.dll (the zstd codec and its dependencies — none ship with Valheim or BepInEx). A mod manager handles this; hand-installers must copy all six.

Mods this replaces — uninstall them first

Uninstall Replaced by Where
CW_Jesse / BetterNetworking Compression + SteamRates + SendQueueGuard server and clients
Mydayyy / ServerSideMap SharedMap server and clients

Both must go from every machine. If BetterNetworking is still installed, Compression refuses to patch (FAILED(...) in the module summary) and the rest of SmoothServer loads normally — it will not break your server, it just will not compress. SharedMap imports an existing <world>.mod.serversidemap.explored file once on first start, so no exploration is lost.

Server modules

Each module has its own Enabled toggle in its config section.

  • Telemetry [Telemetry] — periodic fps / frame-time / ZDO-rate log line. No gameplay effect.
  • FrameRate [FrameRate] — raises the dedicated server's Unity frame cap. 60 is a good default on modern hardware: roughly +4pp of one CPU core for half the tick latency.
  • SendCadence [SendCadence] — sends ZDO updates to every peer on a fixed interval (SendHz, default 20) instead of vanilla's one-peer-per-frame round robin. Scales with player count.
  • SendBudget [SendBudget] — the ZDO send-queue high-water mark and minimum chunk size, exposed as config: HighWaterBytes defaults to 65536 (vanilla 10240), MinChunkBytes to 2048 (same as vanilla). Acts as the fallback when AdaptiveBudget is off.
  • CreateBudget [CreateBudget] — objects created per frame, exposed as config (default matches vanilla, i.e. a no-op until raised).
  • PeerTelemetry [PeerTelemetry] — per-peer ping, throughput and Steam send-queue sampling. No patches; feeds AdaptiveBudget.
  • AdaptiveBudget [AdaptiveBudget] — a per-peer send budget from each peer's measured bandwidth-delay product, clamped, EMA-smoothed, and backed off while Steam's pending byte count says the pipe is congested. A player on fibre next door and one on satellite no longer share one number.
  • SteamRates [SteamRates] — raises Steam's SendRateMax (default 1 MB/s). SendRateMin stays at vanilla on purpose: it is a floor on Steam's own bandwidth estimate, and raising it stops the congestion controller backing off for weak connections.
  • SendQueueGuard [SendQueueGuard] — defers, never drops, while a peer's Steam send queue is backed up, and survives the Steamworks throws that can occur mid-handshake.
  • SyncListCache [SyncListCache] — caches the per-peer sector scan in ZDOMan.CreateSyncList briefly; the filter and sort still run on every send.
  • VPOServer [VPOServer] — the server-safe parts of ValheimPerformanceOptimizations (MIT): WearNTear support caching and a faster ReleaseNearbyZDOS scan.
  • AsyncSave [AsyncSave] — pre-sizes the save clone list. Measured on a 136 000-ZDO world, main-thread autosave stall 67 ms → 47 ms and 78 ms → 45 ms.
  • GcThrottle [GcThrottle] — rate-limits Resources.UnloadUnusedAssets on an idle server.
  • OwnershipRelease [OwnershipRelease] — shortens the ZDO ownership-release interval from vanilla's 2 s (default 0.5 s) so objects change hands faster as players move.
  • MapSelfTest [MapSelfTest] — headless unit tests for the shared-map codec at load; shows up as applied PASS in the module summary.
  • StatsLog [StatsLog] — writes stats-YYYY-MM-DD.jsonl (fps/frame time, ZDO counts, per-peer network/budget/compression state) every IntervalSec (default 10s) and events-YYYY-MM-DD.jsonl (joins/leaves, save stalls, GC sweeps, AdaptiveBudget backoff transitions, SendQueueGuard drops, config reloads) as they happen, to BepInEx/config/smoothserver/stats/ by default. Rotates daily, prunes past RetentionDays (default 30). Run tools/analyze.py against a few days of these files for a markdown report — see the repo's tools/README.md.

What the client half adds

  • Compression [Compression]both ends. zstd over Steam sockets using BetterNetworking's trained dictionaries, with an explicit per-message frame tag and an SS_Caps/SS_Ready capability handshake, so the receiver always knows whether a message is compressed. Peers without the mod stay uncompressed with no penalty. Typical self-test ratio on real ZDO traffic: ~8 % of the original size.
  • SharedMap [Map]both ends. The whole server shares map exploration and pins. The store is bit-packed and compressed (1.7 KB where ServerSideMap wrote 4.2 MB for the same world), the map size is read from your client rather than hardcoded, and updates are batched at 1 Hz into sparse chunks instead of one RPC per explored pixel. Merging goes through vanilla Minimap.Explore, so shared terrain bakes into your own map file and nothing is lost if the module is turned off. Pin sharing is configurable per pin type; death pins are off by default.
  • ClientNet [Client]client only. Your client's own ZDO send high-water mark (default 48 KB, vanilla 10 KB) and Steam SendRateMax. Inactive on a dedicated server.

Config

One file, Nosferatu.SmoothServer.cfg. Edits are picked up live on a running server ([General] HotReload) — no restart. Settings the server owns are pushed to clients that run the mod (ServerSync); [General] entries stay machine-local.

Dependencies

Credits

  • zstd dictionaries and the compression idea: CW_Jesse's BetterNetworking (MIT).
  • VPOServer: ValheimPerformanceOptimizations (ontrigger, MIT).
  • Config sync: blaxxun-boop's ServerSync (MIT-0).
  • SharedMap is a clean-room reimplementation inspired by Mydayyy's ServerSideMap (MIT/Unlicense).
  • Server-authoritative ownership idea: ddormer's Serverside Simulations (no published license — idea only, no code taken).

Source & issues

https://github.com/MJensen01/SmoothServer — MIT licensed.

CHANGELOG

Changelog — SmoothServer

0.5.0 (2026-09-09) — Valheim 1.0

Rebuilt for Valheim 1.0.7 (network version 39). Requires BepInExPack_Valheim 5.4.2350. Not compatible with 0.221.x — stay on 0.4.0 if your server is held on the default_pre1_0 branch.

  • VPOServer, SyncListCache: ported to 1.0's per-peer SimulationDistance and Vector2s zones; the WearNTear port mirrors 1.0's dirty-check so s_support is only broadcast when it actually changed; the ownership scan uses 1.0's point-distance active-area test.
  • AsyncSave: the pre-size optimisation is retired — 1.0 saves per chunk and pre-sizes itself. The module now instruments only (StatsLog still gets save timings) and says so loudly at boot.
  • MapSelfTest: worlds path via SaveSystem.GetWorldsSaveRootPath.
  • All three transpiler assertions hold at their original counts on 1.0; every patch target re-verified.

0.4.0 (2026-09-07)

[Profiles] Profile — one setting that tunes the whole mod. A synced enum (Default / FastLink / Custom) that writes a fixed table of values into ten existing settings at load, whenever the profile changes, and after every live cfg reload. It is a preset, not a new feature: nothing is shadowed or hidden, the values are written into the entries themselves, so the cfg file, cfg.py get and each module's own startup log line all agree on the single number that is actually running.

  • FastLink"make it feel like LAN", for a small group on strong PCs and good links: [SendCadence] SendHz=60, [AdaptiveBudget] CeilingBytes=262144 FloorBytes=32768, [SteamRates] SendRateMax=4194304, [Client] HighWaterBytes=131072 SendRateMaxBytesPerSec=4194304, [Compression] Enabled=true, [LowLatency] NagleMicros=0, [FrameRate] TargetFrameRate=60, [CreateBudget] MaxCreatedPerFrame=20. Nothing else is touched.
  • Default — those same ten keys are forced back to their shipped defaults.
  • Custom — the plugin overrides nothing and every value in the file stands.

Precedence is documented on each affected key's own description in the generated cfg, so it is discoverable from the file rather than from this changelog. Because Profile is synced, a client that joins a FastLink server runs FastLink too — including the machine-local keys ([Client] SendRateMaxBytesPerSec, [FrameRate] TargetFrameRate) that ServerSync does not push.

Upgrading from 0.3.x: the shipped default is Profile=Default, which means any of those ten keys you had hand-tuned is reset to its default on first boot (the plugin logs exactly what it changed). If you want your own numbers kept, set [Profiles] Profile = Custom — that is what it is for.

New module LowLatency ([LowLatency], both ends, on by default). Steam coalesces small reliable sends for k_ESteamNetworkingConfig_NagleTime microseconds before putting them on the wire — 5000µs by default, which vanilla Valheim never changes: a straight 0-5 ms of added latency on every ZDO update, in both directions, for a saving that mattered on a modem. SmoothServer already batches (SendCadence) and compresses (Compression), so what reaches Steam is already a big packet. NagleMicros=0 sends it immediately. The shipped default is vanilla's 5000 (the module logs a before/after readback but changes nothing until you lower it, or until FastLink does); SendBufferBytes (0 = leave Steam's 512 KB alone) is there for a server pushing large budgets to several peers. Picks the Steam interface its own build was compiled for — SteamGameServerNetworkingUtils on the dedicated server, SteamNetworkingUtils on the client (NOTES §20) — and goes quiet if that interface is not initialised.

New module SmoothMotion ([SmoothMotion], client only, off by default). Tunes how other players and mobs are drawn between the updates you receive. Purely local and purely cosmetic: nothing is sent, stored or simulated differently, and objects you own are never touched (vanilla's own if (zdo.IsOwner()) return; runs first). Two knobs, both transpiled over the hardcoded literals in ZSyncTransform.SyncPosition with an exact-match-count assertion, so a game patch that changes the maths makes the module refuse to load rather than silently do something else:

  • InterpolationFactor (0.2 = vanilla) — how much of the gap to the target position a non-owned object closes each physics tick. Higher = snappier, lower = smoother.
  • ExtrapolateMs (2000 = vanilla) — how far ahead an object may be dead-reckoned along its last known velocity. Valheim already does this, uncapped for a full two seconds; this knob only tightens it. 150-400 hides a lost packet without the two-second overshoot that produces the rubber-band snap when someone stops dead. 0 disables prediction entirely.
  • ApplyTo (Characters / All) — players and mobs only, or every synced object.

tools/cfg.py preset <mod> fastlink|default|custom — sets [Profiles] Profile and confirms the live reload, like any other cfg.py set.

ConfigWatcher gained a post-reload hook (AfterReload) and a Reloading flag. BepInEx raises SettingChanged entry-by-entry as Config.Reload() walks the file, so a handler that reacts to one entry by writing others is half-undone by the entries the reload has not reached yet. The profile is therefore applied once, after the whole file is read, instead of flapping mid-reload.

0.3.1 (2026-09-07)

Fix: server-side Steam socket modules used the client Steam interface on dedicated servers (connections failed). The two builds of assembly_valheim.dll are compiled differently: ZSteamSocket calls SteamNetworkingSockets on the client and SteamGameServerNetworkingSockets on the dedicated server, and only the matching half of Steamworks is initialised in each process. SendQueueGuard re-implemented vanilla's send drain with the client interface hard-coded, so on a dedicated server every send threw Steamworks is not initialized., no client could finish its handshake, and joiners were dropped with "Socket closed by peer". It no longer re-implements the drain at all: it wraps the vanilla one with a prefix/finalizer pair, so the game picks the interface its own build was compiled for. Back-off and once-per-episode logging are unchanged (progress is now measured through m_totalSent instead of the EResult). PeerTelemetry probes the build's own interface first and stops calling GetConnectionQuality where vanilla itself calls the wrong one, taking ping/quality/throughput off the real-time status instead. SteamRates no longer warns on the missing client interface — the first refusal per interface is noted once at Info and that interface is never touched again. New [General] SteamSelfTest (off by default, machine-local) logs which half of Steamworks is live and, straight out of ZSteamSocket's IL, which interfaces this build actually calls — turn it on once after a game update to re-prove it.

StatsLog: persistent JSONL stats + events for multi-day analysis. [StatsLog] (server-side, on by default) writes stats-YYYY-MM-DD.jsonl every IntervalSec (default 10s) — fps/frame time, ZDO/scene counts, per-peer RTT/pending/in-flight/queued bytes, AdaptiveBudget target + congestion, Compression framing state, and compression byte deltas — plus events-YYYY-MM-DD.jsonl for player join/leave, world-save stalls, GC sweeps, AdaptiveBudget backoff transitions, SendQueueGuard drops and config reloads, as they happen. Files land in BepInEx/config/smoothserver/stats/ by default, flush on every write, rotate daily (UTC) and are pruned past RetentionDays (default 30). IntervalSec/RetentionDays hot-reload. A companion tools/analyze.py (stdlib-only) turns a few days of logs into a markdown report — sessions, frame-time percentiles, per-player RTT/pending-bytes, AdaptiveBudget backoff time, compression ratio, save stalls, ZDO growth, and the worst 10-second windows.

0.3.0 (2026-09-07)

SmoothServer is now a both-ends mod. The same SmoothServer.dll runs on a dedicated server and in a player's BepInEx profile; [General] Mode (Auto/Server/Client) picks the half, and a module that does not belong to the running half reports disabled(side) and patches nothing.

[General] EnforceClientMod defaults to false, so nothing changes for existing server admins: SmoothServer still works as a pure server-side mod with zero client installs, and vanilla clients can still join. Turn it on only if your whole group installs the mod.

Read this before updating

  • BetterNetworking must be uninstalled — on the server and on every client. SmoothServer's Compression module replaces it and the two cannot coexist (both wrap ZSteamSocket's send queue). If BetterNetworking is present, Compression refuses to patch and reports FAILED(...); the rest of the plugin still loads.
  • ServerSideMap must be uninstalled — on the server and on every client. SharedMap replaces it. On first start SmoothServer imports your existing <world>.mod.serversidemap.explored file automatically (once), so no exploration is lost.
  • The zip now contains six DLLs, not one. SmoothServer.dll plus ZstdSharp.dll, System.Memory.dll, System.Buffers.dll, System.Numerics.Vectors.dll and System.Runtime.CompilerServices.Unsafe.dll. A mod manager installs all of them; if you copy files by hand, copy all six into BepInEx/plugins/SmoothServer/.

New server modules

  • PeerTelemetry [PeerTelemetry] — per-peer ping, throughput and Steam send-queue sampling. No patches; it is the input to AdaptiveBudget and the source of the per-peer log line.
  • AdaptiveBudget [AdaptiveBudget] — per-peer ZDO send budget derived from each peer's measured bandwidth-delay product, clamped and EMA-smoothed, backing off while Steam's pending byte count says the pipe is congested. SendBudget's configured value becomes the fallback.
  • SteamRates [SteamRates] — raises Steam's SendRateMax on the game-server networking interface (default 1 MB/s). SendRateMin is deliberately left at vanilla.
  • SendQueueGuard [SendQueueGuard] — defers (never drops) sends while a peer's Steam queue is backed up, and swallows the "Steamworks is not initialized" throws that can happen mid-handshake.
  • SyncListCache [SyncListCache] — caches the per-peer sector scan in ZDOMan.CreateSyncList for a short window; the filter and sort still run on every send.
  • VPOServer [VPOServer] — server-safe parts of ValheimPerformanceOptimizations (MIT): WearNTear support caching and a faster ReleaseNearbyZDOS scan.
  • AsyncSave [AsyncSave] — pre-sizes ZDOMan.GetSaveClone()'s list from the live ZDO count. Measured on a 136 000-ZDO world: main-thread autosave stall 67 ms → 47 ms and 78 ms → 45 ms.
  • GcThrottle [GcThrottle] — rate-limits Resources.UnloadUnusedAssets on an idle server.
  • OwnershipRelease [OwnershipRelease] — shortens the ZDO ownership-release interval from vanilla's 2 s (default 0.5 s), so objects change hands faster as players move.

New both-ends / client modules

  • Compression [Compression], both ends — zstd (ZstdSharp, pure managed) over Steam sockets using BetterNetworking's trained dictionaries. Explicit 1-byte frame tag and an SS_Caps/SS_Ready handshake, so the receiver never guesses and never uses an exception as a signal; a peer without the mod simply stays uncompressed. Self-tested at load.
  • SharedMap [Map], both ends — server-side shared map exploration and pins, replacing ServerSideMap. Bit-packed and compressed store (1.7 KB where ServerSideMap wrote 4.2 MB for the same world), map size read from the client instead of hardcoded, deltas batched at 1 Hz into sparse chunks instead of one RPC per pixel. Merges through vanilla Minimap.Explore, so shared pixels bake into your own map file. Optional death-pin sharing.
  • MapSelfTest [MapSelfTest], server — headless unit tests for the map codec, run at load.
  • ClientNet [Client], client only — the client's own ZDO send high-water mark (default 48 KB) and Steam SendRateMax. Never active on a dedicated server.

Other

  • Modules are now discovered by reflection: one [BepInPlugin], one config file, one module summary line, hot reload covering every module.
  • Config sharing via ServerSync — the server's values win on clients that run the mod.
  • SendBudget's high-water hook now defers to AdaptiveBudget per peer; [AdaptiveBudget] UseCallSiteSwap is a legacy path and now defaults to false.

0.2.0 (2026-09-06)

  • Renamed from OrionNet. New plugin GUID Nosferatu.SmoothServer, new config file Nosferatu.SmoothServer.cfg. Old net.mjensen.orion.net.cfg settings are not migrated — re-apply them once.
  • Live config reload (hot reload): edits to the cfg file are picked up on a running server without a restart ([General] HotReload).

0.1.0 (2026-09-06)

  • Initial release. Modules: Telemetry, FrameRate, SendCadence, SendBudget, CreateBudget.
  • Server-only; no client install required.