ValheimTune
Server-side performance patches for Valheim dedicated servers. Object sync 4.1ms -> 0.09ms per call on a 700k-object world. Players install nothing, wire format untouched. Valheim 1.0 ready.Changelog
0.7.9 targets dedicated-server build 25730807 (game 1.0.17, network version 40) and remains valid for 1.0.16, 1.0.15, 1.0.14, 1.0.12 and 1.0.7. 0.7.5 to 0.7.8 target build 25527701 (game 1.0.16, network version 40). 0.7.4 targets build 25390671 (game 1.0.15, network version 40). 0.7.3 targets build 25364309 (game 1.0.14, network version 40). 0.7.1 targets build 25253791 (game 1.0.12, network version 40). 0.7.0 targets build 25185644 (game 1.0.7, network version 39). 0.6.0 and earlier target build 21981590 (game 0.221.12, network version 36).
0.7.9 - 2026-10-06
Compatibility rebuild for Valheim 1.0.17 (dedicated-server build 25730807). No behaviour change; the code is 0.7.8's.
Why you want it: on 1.0.17, 0.7.8 does not list the build in
KnownGoodBuilds, so with the default DisableOnUnknownBuild = true every
replacement patch and every [Fixes] fix falls back to vanilla and only the
stats line keeps running. 0.7.9 adds 1.0.17 to the shipped list. No config
edit needed.
Network version is still 40, so 1.0.17 locks no one out.
Not verified live. A decompile diff of 1.0.17 against 1.0.16 changes 6
files: Inventory.cs, ItemDrop.cs, ServerJoinDataUtils.cs,
VariantDialog.cs, Version.cs and one line of ZDOMan.cs. That line is in
AddObjectsPerChunk, which GetSaveClonePerChunk calls: the incremental save
now also writes chunks missing from the chunk save mapping, not only dirty
ones. The plugin does not patch that method. SpawnerLinkFix reads the
chunk list it returns, so it simply sees more chunks written; SaveDirtyFix
is still needed, because a player edit in a chunk that is already mapped is
still skipped unless something marks it dirty. Every other file the plugin
patches is unchanged.
0.7.8 - 2026-10-01
Documentation only; the plugin code is identical to 0.7.7.
- The README's links to the changelog, the porting notes and older releases were relative, so on Thunderstore and in mod managers they pointed nowhere (#4). They are absolute now, and the docs check rejects a relative link.
- The Thunderstore package now includes this changelog, shown on its Changelog tab.
0.7.7 - 2026-09-30
Vanilla bug fixes and review fixes. No config edit needed; every new fix is on by
default and has its own switch under [Fixes]. Found by a full review of the
plugin and a first read of the server code it had never looked at; the complete
record, including what was checked and found clean, is in the analysis repo's
docs/AUDIT-2026-09-30.md.
Vanilla bugs fixed (any 1.0 server has these):
- Player edits skipped by the incremental save (
SaveDirtyFix). Valheim 1.0 saves only chunks marked dirty, and only changes the server makes mark them. Updates arriving from players - building, chests, signs - never do. An area only players touched since the last save can be skipped, and it reverts on restart while the character keeps the items. It usually hides behind incidental marks (ownership changes, objects crossing zones). - Spawners duplicating creatures after a restart (
SpawnerLinkFix). Every save gives spawner/creature links new hashes, but an unsaved chunk keeps the old ones; a link split across two chunks no longer matches at load and the spawner spawns another creature. Both sides are now saved together. The load log lineRemoved connection from N orphan spawn:sshows the vanilla bug; on our own vanilla world it read 0 at every restart for a month, so it is rare. - 100 ms server freeze on every disconnect (
DisconnectNoSleep, restart to apply).ZSteamSocket.Closesleeps the main thread for 100 ms - for every player leaving, timing out or being refused at join. Removed; the connection closes with Steam's linger so queued messages still go out. - Destroyed-object records kept until restart (
DeadZdoPrune): pruned to the last hour at each save. - Repeat global-key broadcasts (
GlobalKeyDedupe): vanilla compares keys case-sensitively, so e.g. every unshielded ship in the Ashlands ocean re-broadcasts every global key to every player every 10 s.
Fixed in the plugin:
- Dirty sets could hold back distant objects (the far ring) until a player came near, whenever a full scan found 10 or more nearby objects to send - most of the time while travelling.
- Changing Simulation Distance in the graphics settings now triggers a full scan; newly in-range objects no longer wait up to 30 s.
SendWindowBytesmax is now 262144. Above ~450 KB a single package exceeds Steam's 512 KB limit and the player times out. Existing configs above the new max are clamped on load.MinHeadroomBytes >= SendWindowBytesnow leavesSendZDOsvanilla instead of applying a window that sends nothing.- Turning
DeferAssetUnloadoff while a collection was held no longer causes an immediate unload stall when it is turned back on later.
New on the stats line: release max / removePeer max (ms), gc /
heap, dead, saveMarks, linkFixes, keyDedupes. The first two measure
two suspected stalls (the ownership pass every 2 s, and the disconnect cleanup
that walks every object) before anything is changed there.
The log now reads 18 methods patched.
Not verified live. 53/53 unit tests; the disconnect transpiler was checked
against the real 1.0.16 IL of ZSteamSocket.Close. Nobody has booted 0.7.7.
0.7.6 - 2026-09-30
Bug fix. No config change, no new setting.
Why you want it: with dirty sets on (the default), an object the server
creates never reached players until the next safety-net full scan, up to
ReconcileSeconds (30 s) later. Server-side mods that recreate objects hit
this every time: ServersideQoL PrefabConfigurator destroys and recreates
building pieces to apply its changes, and with 0.7.5 each piece vanished for
15-20 s instead of a fraction of a second
(#3).
Cause: dirty sets learn about changes from the DataRevision /
OwnerRevision setters. A ZDO built with ZDOMan.CreateNewZDO and filled with
Deserialize or SetOwnerInternal touches neither. Objects players place were
never affected - they arrive through RPC_ZDOData, which sets the revision.
Fix: a postfix on ZDOMan.CreateNewZDO marks every new object for every
player. All creation paths go through that one method. The log now reads
12 methods patched, and the stats line's marks count now includes object
creations, so it runs higher during building or zone generation. Workaround on
0.7.5: [Sync] DirtySets = false.
Not verified live. Unit tests pass (47/47); nobody has booted 0.7.6.
0.7.5 - 2026-09-25
Compatibility rebuild for Valheim 1.0.16 (dedicated-server build 25527701). No behaviour change.
Why you want it: on 1.0.16, 0.7.4 does not list the build in
KnownGoodBuilds, so with the default DisableOnUnknownBuild = true every
replacement patch falls back to vanilla and only the stats line keeps running.
0.7.5 adds 1.0.16 to the shipped list. No config edit needed.
Network version is still 40, so 1.0.16 locks no one out.
Not verified live. Same caveat as 0.7.1 to 0.7.4. A decompile diff of 1.0.16
against 1.0.15 changes 19 files, all gameplay or client side: SaveSystem.cs
(cloud-only mount on backup restore), SpawnSystem.cs (spawn-hash counter fix),
TerrainComp.cs (duplicate handling again), Player.cs, Achievements.cs,
ObjectDB.cs, PlayerProfile.cs (food-achievement exclusions), a handful of
UI and gamepad files, and Version.cs. Every file this plugin patches -
ZDOMan.cs, ZRpc.cs, ZSteamSocket.cs, ZDO.cs, Game.cs, Heightmap.cs,
ZoneSystem.cs - is unchanged.
0.7.4 - 2026-09-19
Compatibility rebuild for Valheim 1.0.15 (dedicated-server build 25390671). No behaviour change.
Why you want it: on 1.0.15, 0.7.3 does not list the build in
KnownGoodBuilds, so with the default DisableOnUnknownBuild = true every
replacement patch falls back to vanilla and only the stats line keeps running.
0.7.4 adds 1.0.15 to the shipped list. No config edit needed.
Network version is still 40, so 1.0.15 locks no one out.
Not verified live. Same caveat as 0.7.1 to 0.7.3. A decompile diff of 1.0.15
against 1.0.14 changes six files: TerrainComp.cs (the duplicated-terrain fix),
Inventory.cs, InventoryGrid.cs, InventoryGui.cs, ItemDrop.cs (the
cheated-item fix), and Version.cs. Every file this plugin patches - ZDOMan.cs,
ZRpc.cs, ZSteamSocket.cs, ZDO.cs, Game.cs, Heightmap.cs, ZoneSystem.cs -
is unchanged.
0.7.3 - 2026-09-17
Compatibility rebuild for Valheim 1.0.14 (dedicated-server build 25364309). No behaviour change.
Network version is still 40, the same as 1.0.12, so unlike the 1.0.12 patch this one does not lock older servers out. A 1.0.12 server keeps accepting 1.0.14 clients; update when it suits you.
Not verified live. Same caveat as 0.7.1 and 0.7.2. What it rests on: a
decompile diff of the 1.0.14 dedicated-server assembly against 1.0.12 shows
every file this plugin patches is unchanged outright - ZDOMan.cs, ZRpc.cs,
ZSteamSocket.cs, ZDO.cs, Game.cs, Heightmap.cs and ZoneSystem.cs all
diff to zero lines. The anchors are at identical line numbers: CreateSyncList
(1261), ServerSortSendZDOS (1360), SendZDOToPeers2 (886), the SendZDOs
window constants 10240/10240/2048 (1060/1064/1065), and ZDO.DataRevision /
OwnerRevision still auto-properties. 1.0.14 is a broad patch - 32 source files
changed, in combat, inventory, UI and settings - but none of it is in the sync
or networking path. The only networking-adjacent edit is ZNet.Save, which
1.0.14 makes save the player profile on a client-issued save; this plugin does
not patch it.
It compiles against the 1.0.14 assemblies and 47/47 unit tests pass. What stays
unproven is IL-level and a source diff cannot settle it: that Harmony attaches
to all 11 methods, and that the constant-swap transpiler still finds exactly 3
constants. Both show in the first two log lines on boot - if the load line does
not say 11 methods patched or the window line does not say
3 constants replaced (expected 3), set [Compat] DisableOnUnknownBuild = true
and report it.
[Compat] KnownGoodBuildsshipped list is now1.0.7, 1.0.12, 1.0.14. It is a floor, so an existing config keeps its patches without an edit.
0.7.2 - 2026-09-13
Plays nicely with other networking mods (#1).
- Vanilla now means hands off. The Steam send-rate postfix used to write both
rates on every boot, even at the vanilla 153600, which could reset a rate another
networking mod had just set. Each rate is now only written when it differs from
vanilla. Likewise the
SendZDOswindow transpiler leaves the method untouched whileSendWindowBytesandMinHeadroomBytesare both vanilla. No change for anyone who tuned these values. - New
[Steam] OverrideSendRateand[Sync] OverrideSendWindow(defaulttrue). Set either tofalseto stop ValheimTune touching that setting at all, whatever the values say. Patch-time: restart to apply. The load log says which one left the game alone and why. - Four new tests (47 total).
Not verified live. Same caveat as 0.7.1.
0.7.1 - 2026-09-11
Compatibility rebuild for Valheim 1.0.12 (build 25253791, network version 40). No behaviour change.
Not verified live. 0.7.0 was booted on a real server with a player online;
0.7.1 has not been. What it does rest on: a decompile diff of the 1.0.12
dedicated-server assembly shows every method this plugin patches is
byte-identical to 1.0.7, at identical line numbers - CreateSyncList (1261),
ServerSortSendZDOS (1360), the SendZDOs window constants 10240/10240/2048
(1060/1064/1065), and ZDO.DataRevision / OwnerRevision still auto-properties.
ZRpc.cs, ZSteamSocket.cs, ZDO.cs, Game.cs and Heightmap.cs are
unchanged outright. The only ZDOMan edits in 1.0.12 are in
ConvertInventories / ConvertContainers, the world-migration path, which this
plugin does not touch. It compiles against the 1.0.12 assemblies and passes
40/40 unit tests. The gap that leaves: nobody has confirmed Harmony attaches at
runtime on 1.0.12, or that the constant-swap transpiler still finds exactly 3
constants. Read the load line on first boot and treat a
constants replaced (expected 3) mismatch as a reason to set
DisableOnUnknownBuild = true and report it.
- The shipped version list is now a floor, not a default. BepInEx keeps an
existing config on upgrade, so before this change every 0.7.0 install would
have kept
KnownGoodBuilds = 1.0.7and silently dropped every patch the moment it reached 1.0.12 - an upgrade that quietly does nothing. The gate now passes if the build is in the user's list or inCompat.DefaultKnownGoodBuilds(1.0.7, 1.0.12), and logs once when the shipped list is what allowed it. Configs can still add builds; narrowing the list was never a documented way to disable anything - that isDisableOnUnknownBuildand the per-feature knobs. - Builds against
tools/server-managed-1012/;src_server_1012/added. - Five new tests (43 total, was 38).
0.7.0 — 2026-09-09
Port to Valheim 1.0.7, deployed and verified live on 2026-09-09 with a player
online: 11 methods patched, replacements on, 3 constants replaced (expected 3), sync cost 0.09 ms/call against 4.1 ms vanilla, frame 16.7 avg /
17.0 max, no errors. B4a's meshSkips fired for the first time (34), 1.0 zone
generation finally giving it virgin terrain. G1 verified at 18:54 - the
hour boundary landed with a player on and the unload was deferred, frames
staying 16.9-24.6 ms with none of the 443-607 ms stall seen on 0.221.12.
Every feature in the port is verified live. See docs/PORTING-1.0.md for the
full survey and the live numbers.
- S1 sliced save removed. 1.0 rewrote the save path:
ZDOMan.SaveAsyncis gone, replaced bySaveChunks/SaveChunk/SaveCleanupwriting one file per chunk, andGetSaveClonePerChunkclones only dirty chunks. That is a strictly better answer to the freeze S1 was working around, so the feature is deleted rather than ported:SavePatches.cs,SaveSlicer.cs,SaveSlicerTests.cs, and the[Save] SlicedSave/[Save] SaveSliceMsknobs. Vanilla 1.0 autosave cost on a 698k-ZDO world is still unmeasured. - B1 dirty sets ported to per-peer simulation distance. 1.0 removed
ZoneSystem.m_activeArea/m_activeDistantArea; the sync radius is now a per-peerSimulationDistancenegotiated inZNet.RPC_RequestValidSimulationDistanceand clamped server-side tomin(client request, server cap).DirtyPatchesnow mirrors the ring predicate inZDOMan.FindSectorObjects(Chebyshev ring andZonesWithinRadius, except in classic mode) instead of calling the removedZNetScene.InActiveArea(sector, zone, radius). Zone indices areVector2s, notVector2i. NewZoneRingTestscover the ring maths. Version.m_networkVersionrenamed toVersion.c_networkVersion.[Compat] KnownGoodBuildsdefault 0.221.12 -> 1.0.7.- Unchanged and rebuilt as-is: B2 top-K, R2 relay throttle, the
SendZDOsconstant swap, the receive cap, the Steam send rate,TargetFrameRate, G1 deferred asset unload, B4a skip render mesh, the watchdog and the stats line.ZRpc.csandZSteamSocket.csare byte-identical between 0.221.12 and 1.0.7;ServerSortSendZDOSandSendZDOToPeers2have identical bodies. - 38 unit tests.
0.6.0 — 2026-09-07
- G1 deferred asset unload (
[Server] DeferAssetUnload, default off): vanilla runsResources.UnloadUnusedAssets()every hour (Game.cs:239InvokeRepeating, the 3599 s check atGame.cs:299). Measured on GalinBalin at 443-607 ms of main-thread stall, and the cost is independent of what it frees - Unity'sMarkObjectswalks all ~207,000 loaded objects to release one asset. Caught live at 21:04:26 with two players online (frame max 512 ms) and at 22:40:23 idle (frame max 457 ms, matching Unity's own 455.78 ms line). Both vanilla entry points route throughGame.CollectResources, so one prefix covers them. The collection is deferred, not skipped: it runs the moment the last player disconnects, or afterAssetUnloadMaxDeferMinutes(default 240) if the server never empties. Found by reading SmoothServer's module list; the deferral policy is ours. - 33 unit tests.
0.5.0 — 2026-09-07
- B4a skip render mesh (
[Server] SkipRenderMesh, default off): prefix onHeightmap.RebuildRenderMeshthat returns false on a dedicated server. The server regenerates a heightmap for every ghost zone a player explores (ZoneSystem.SpawnZone-> the zone prefab'sHeightmap.OnEnable) and for every terrain edit that loads with one (TerrainComp.Poke); the render half is(m_width+1)^2vertices, colours, UVs and indices plusRecalculateNormals/Tangents/Boundson a-nographicsprocess. The collision mesh, paint mask and material instance are untouched, and every other read ofm_renderMeshis null-guarded. NewmeshSkipscounter on the stats line. Off until a live exploration run shows the counter climbing with nothing visibly wrong.
0.4.2 — 2026-09-07
- Watchdog false trip fixed: "received" is now counted by our own
ZDO.Deserializepostfix over the same window as the marks, instead of the game's one-second-lagging counter, which tripped it when the last player logged out (live at 21:00 with three players on, B1 and the throttle silently off). The watchdog also re-arms itself when marks reappear.
0.4.1 — 2026-09-07
TopKSortdefault on after a live join: syncList ~11 -> ~5.5 ms avg, 24-32 -> 12-20 ms max. Gate tested both ways on the live server.
0.4.0 — 2026-09-07
- Version gate: replacement patches run only on a build in
[Compat] KnownGoodBuilds; unknown build logs a warning and runs vanilla + measurement. Constant-swap transpiler aborts unless exactly 3 constants matched. - B2 top-K selection (
TopKSort, default off): bounded heap instead of a full sort of every candidate; closes the vanillam_tempSortValueleak as a side effect. OneGetZDOper id in the dirty-set drain.
0.3.1 — 2026-09-07
SlicedSavedefault on after the live round-trip (687,133 saved at shutdown, 687,133 loaded). Sync-mode log line says "slices back-to-back" instead of "frames".
0.3.0 — 2026-09-07
- S1 sliced save (
SlicedSave, default off): world serialized on the main thread in SaveSliceMs slices into a memory buffer; the writer thread no longer reads live ZDO arrays. Off until a live round-trip is verified.
0.2.2 — 2026-09-07
- Watchdog uses its own 10 s window and counter; no longer races the stats-line reset (would have disabled B1 after restarts).
0.2.1 — 2026-09-07
- Final-review fixes: all-peers round respects the watchdog; marks split by setter; forced full scan when DirtySets is re-enabled; ReconcileSeconds read per call; floating scan cutoff 0.25 m above water and logs positions;
drainedon the stats line; ZDOID.None ignored in Mark.AllPeersPerRounddefault back to false pending a multi-player test.
0.2.0 — 2026-09-07
- B1 dirty sets: per-peer pending set fed by the revision setters; full scan on join, zone change and every 30 s; watchdog falls back to vanilla if the hook is silent. Live: sync cost per call 4.1 ms -> 0.07 ms.
- R2 relay throttle (
RelayMinIntervalMs), non-prioritized objects only. DirtySetsdefault on.
0.1.5 — hot-objects ignore list.
0.1.4 — hot-objects line with world coordinates.
0.1.3 — floating scan includes felled logs (TreeLog).
0.1.2 — floating item-drop scan/delete; config reload every 5 s.
0.1.1 — per-prefab tally of received ZDOs.
0.1.0 — 2026-09-07
- Measurement stats line;
ConstSwapIL constant swap; Tier C knobs (send window, headroom, all-peers round, Steam send rate); per-peer packet cap;TargetFrameRateknob. Final review fixes: restart grace 150 s, config ranges, per-peer try/catch, measurement hygiene.
