You are viewing a potentially older version of this package. View all versions.
Akoozie-ValheimTune-0.7.7 icon

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.

Date uploaded a week ago
Version 0.7.7
Download link Akoozie-ValheimTune-0.7.7.zip
Downloads 63
Dependency string Akoozie-ValheimTune-0.7.7

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

ValheimTune

✅ Valheim 1.0.16 ready

0.7.7 runs on game 1.0.16 (network version 40), and still on 1.0.15, 1.0.14 and 1.0.12 (40) and 1.0.7 (39). It fixes three vanilla bugs any 1.0 server has — player edits skipped by the incremental save, spawners duplicating creatures after a restart, and a 100 ms server freeze on every disconnect — plus two sync gaps found in review. It has not been booted on 1.0.16 — read the CHANGELOG before you deploy it. Upgrading from 0.7.6 needs no config edit. Running game 0.221.12? Use 0.6.0 instead — the version gate will refuse to apply these patches to an older build.

On Thunderstore: Akoozie-ValheimTune

Makes a Valheim dedicated server with a big base and a handful of players feel like a small one.

Server-side only — players install nothing. Every change is on the server, the wire format is untouched, and vanilla clients connect exactly as before.

game     1.0.16, 1.0.15, 1.0.14 and 1.0.12 (network version 40), 1.0.7 (39), dedicated server only
needs    BepInEx 5.4.x
status   0.7.7 adds vanilla bug fixes on top of the 1.0.16 rebuild. Every method this
         plugin patches is byte-identical across 1.0.7 to 1.0.16
         (decompile diff), it compiles against the 1.0.16 assemblies and
         its unit tests pass - but it has NOT been booted on 1.0.16. 0.7.0
         WAS verified live on 1.0.7, 2026-09-09: 698,000 objects, a
         12,000-instance base, 2-6 players.

Why

Measured on that server, not modelled:

Vanilla ValheimTune
Sync scan, per player per round 4.1 ms 0.07 ms
Autosave 381 ms freeze in one frame 6 ms slices over ~350 frames
Join sync cost ~11 ms per call ~5.5 ms
Join stream rate 1,261 objects/s 3,617 objects/s
Server frame rate 30 fps, hard-capped 60 fps
Relayed updates, 2 players at a base baseline ~55 % fewer

Every knob defaults to vanilla except the ones proven live. What each number comes from is in How it works.

Requirements

  • Valheim dedicated server (Steam app 896660). Not the in-client host.
  • BepInEx 5.4.x for Valheim (BepInExPack_Valheim).
  • Game version listed in [Compat] KnownGoodBuilds (currently 1.0.7, 1.0.12, 1.0.14, 1.0.15, 1.0.16). On any other version the plugin runs in vanilla + measurement mode and says so in the log.

Install

On Thunderstore — Akoozie-ValheimTune. Note that mod managers install to a client profile; for a dedicated server you still need the DLL on the server, so the manual steps below are the normal route.

  1. Install BepInEx on the server. With the lloesche/valheim-server Docker image that is BEPINEX=true in server.env.
  2. Drop ValheimTune.dll into BepInEx/plugins/ (Docker: config/bepinex/plugins/).
  3. Restart. The config file appears at BepInEx/config/akoozie.valheimtune.cfg.
  4. Check the log for:
[ValheimTune] 0.7.7 loaded on game 1.0.16 (net 40), 18 methods patched, replacements on
[ValheimTune] SendZDOs window 10240/2048, 3 constants replaced (expected 3)

Recommended settings

What runs on the reference server. Apply one at a time and read the stats line between changes. Most knobs take effect within 5 seconds without a restart.

[Server]
TargetFrameRate = 60

[Sync]
SendWindowBytes = 32768
MinHeadroomBytes = 4096
AllPeersPerRound = true
RelayMinIntervalMs = 200

[Steam]
SendRateMaxBytesPerSec = 1048576

DirtySets and TopKSort are already on by default.

Upgrading from an earlier release

Drop the new DLL in and restart - no config edit needed. A new plugin version never rewrites an existing akoozie.valheimtune.cfg (BepInEx only writes a default when the key is absent), so your file still says KnownGoodBuilds = 1.0.7. As of 0.7.1 that no longer costs you anything: the list shipped in the release is treated as a floor, and your config can only add to it. You will see this once on boot:

[ValheimTune] game 1.0.16 is not in your KnownGoodBuilds (1.0.7) but ships in
this release (1.0.7, 1.0.12, 1.0.14, 1.0.15, 1.0.16); using the shipped list. Your config is
from an older version.

Tidy the line up if you like; nothing depends on it.

If the log says replacements OFF

[ValheimTune] game 1.0.17 not in KnownGoodBuilds (1.0.7, 1.0.12, 1.0.14, 1.0.15, 1.0.16):
replacement patches inactive, running vanilla + measurement

Your server updated to a game build this plugin has not been verified against. Nothing is broken — the version gate did its job and refused to run old patch logic against new code. But the plugin is now only printing the stats line; none of the optimizations are running. The log line tells you your exact game version, which is what the gate compares against.

You have three options.

1. Wait for a release that lists your version. The safe one. Check the releases page; each one names the game build it was verified against. Meanwhile the stats line still works, so you keep the diagnostics.

2. Force it on and accept the risk. Add your version to the list:

[Compat]
KnownGoodBuilds = 1.0.7, 1.0.12, 1.0.14, 1.0.15, 1.0.16, 1.0.17

Restart. Harmony will refuse to patch any method whose signature changed and log it, and the constant-swap transpiler self-aborts unless it matches exactly the three constants it expects — so a shape change fails loudly rather than silently. What it cannot catch is a method whose shape is unchanged but whose semantics moved.

Back up your world first. Every patch here affects performance only — none of them writes your world file — but a game update can move ground under any of them, and a backup costs nothing.

3. Build it yourself against the new server assemblies. See Building from source. If it works, please open an issue saying which game build — that is what gets it into the next release.

Reports welcome either way. DisableOnUnknownBuild = false is the blunt version of option 2; it forces every replacement patch on for any version, and carries the same caveat with none of the record of what you tested.

Reading the stats line

Every LogIntervalSeconds, prefixed [ValheimTune]:

frame avg 16.7 max 17.0 ms (60 fps) | syncList avg 0.07 max 0.20 ms | send avg 0.10 ms
| Z max 11418 | peer-sends 400 | zdos/s sent 430 recv 1000 | peers 2
| marks 15750 full 0 dirtyRounds 399 deferred 8300 drained 12 | meshSkips 0
| release max 1.2 removePeer max 14.0 ms | gc 3 heap 412 MB | dead 5210
| saveMarks 880 linkFixes 0 keyDedupes 6
recv by prefab (7848 in window): Fish1=1833 Fish2=1315 ...
hot objects: Wood=69@(-327,-631) ...

The release/gc/dead/saveMarks line is new in 0.7.7 and its numbers above only illustrate the format; it has not run on a live server yet.

What each field means
Field Meaning Healthy
frame main-thread frame time avg at your target, max under ~35 ms except during a join
syncList time per candidate search, per player per round ~0.1 ms steady, a few ms during a join
Z max largest candidate set seen in a full scan scales with your base
peer-sends send calls in the window (rounds x players) ~200 per player per 10 s with AllPeersPerRound
zdos/s sent / recv last-second counters recv is what your players' clients push; sent is the relay
marks change-hook hits in the window non-zero with players on; 0 means the hook is dead and the watchdog will fall back
full / dirtyRounds full scans vs dirty-set rounds full ~0, one per ReconcileSeconds per player
deferred relays held back by the throttle large is good
drained candidates from the last dirty round
meshSkips render-mesh rebuilds skipped by SkipRenderMesh climbs while players explore new ground, 0 elsewhere
DISABLED appended if the watchdog tripped should never appear
release max / removePeer max slowest ownership hand-off pass and slowest disconnect cleanup in the window measurement for the next release; tell us if either passes ~30 ms
gc / heap garbage collections in the window, managed heap size
dead destroyed-object records held (pruned to 1 h at each save)
saveMarks client updates whose save chunk SaveDirtyFix marked non-zero with players building or moving things
linkFixes spawner links saved on both sides by SpawnerLinkFix usually 0
keyDedupes repeat global-key sets dropped by GlobalKeyDedupe ~6/min per ship in the Ashlands ocean
recv by prefab which prefabs your players are pushing tells you what to clean up
hot objects per-object counts with world x,z find the log that never stops rolling
Full config reference — every knob, default, and when it takes effect

runtime = re-read every ConfigReloadSeconds without a restart. patch-time = read once when the plugin loads; restart to change.

Key Default When What
[Measure] LogIntervalSeconds 10 runtime Stats line cadence. 0 disables.
[Measure] ConfigReloadSeconds 5 runtime How often the cfg is re-read.
[Measure] HotObjectsIgnore Player,Fish1,Fish2,Fish3 runtime Prefabs left out of the hot-objects line.
[Server] DeferAssetUnload false runtime Hold vanilla's hourly UnloadUnusedAssets (443-607 ms of main-thread stall, measured) until no players are connected. Deferred, not skipped.
[Server] AssetUnloadMaxDeferMinutes 240 runtime Backstop: collect anyway once a deferral has been held this long, so a server that never empties still collects.
[Server] SkipRenderMesh false runtime Skip the heightmap render-mesh rebuild on a dedicated server; it is built for every zone a player explores and never drawn. Collision mesh untouched.
[Server] TargetFrameRate 0 runtime Override the server's hard-coded 30 fps. 0 = leave it. Sync round period is 0.05 s + players / fps.
[Sync] SendWindowBytes 10240 patch-time Bytes in flight per player before the server stops queueing. Vanilla 10240, max 262144 (Steam rejects messages over 512 KB).
[Sync] MinHeadroomBytes 2048 patch-time Below this much free window the player is skipped this round. Must be below the window, or SendZDOs is left vanilla.
[Sync] OverrideSendWindow true patch-time Set false if another networking mod changes the send queue size; SendZDOs is then left untouched. At vanilla window values nothing is changed either way.
[Sync] AllPeersPerRound false runtime Serve every player each round instead of one per frame. Pays off at 4+ players.
[Sync] RoundSeconds 0.05 runtime Round period when AllPeersPerRound is on.
[Sync] DirtySets true runtime Only consider changed objects each round. A watchdog falls back to vanilla if the change hook ever goes silent.
[Sync] ReconcileSeconds 30 per player at connect Safety-net full scan interval.
[Sync] RelayMinIntervalMs 0 runtime Re-send a non-prioritised object to the same player at most this often. 0 = vanilla. 200 is the tested value.
[Sync] TopKSort true runtime Bounded-heap selection instead of a full sort of every candidate.
[Sync] TopK 0 runtime Candidates ordered per round. 0 = SendWindowBytes / 64, never below 64.
[Steam] SendRateMaxBytesPerSec 153600 patch-time Steam per-connection send cap. Vanilla 150 KB/s. Do not exceed your upload divided by player count.
[Steam] SendRateMinBytesPerSec 153600 patch-time Leave at vanilla so Steam's estimator can back off on a lossy link.
[Steam] OverrideSendRate true patch-time Set false if another networking mod manages Steam send rates; ValheimTune then never writes them. A rate left at vanilla is never written either way.
[Receive] MaxPacketsPerPeerPerFrame 0 runtime Stop draining one player's socket after this many packets in a frame. 0 = vanilla. Try 64 if one player's burst ever stalls the rest.
[Fixes] SaveDirtyFix true runtime Mark a save chunk dirty when a client update arrives. Vanilla only marks chunks for changes the server makes itself, so an area only players touched since the last save can be skipped and revert on restart.
[Fixes] SpawnerLinkFix true runtime Save both sides of a spawner/creature link together. Vanilla re-hashes links every save, so a link split across a saved and an unsaved chunk breaks and the spawner spawns a duplicate after a restart.
[Fixes] DeadZdoPrune true runtime Forget destroyed-object records older than an hour at each save; vanilla keeps one per destroyed object until restart.
[Fixes] DisconnectNoSleep true patch-time Remove vanilla's 100 ms main-thread sleep on every disconnect and rejected join; close with Steam's linger so queued messages still go out.
[Fixes] GlobalKeyDedupe true runtime Ignore a global-key set that changes nothing (vanilla re-broadcasts every key to every player, e.g. every 10 s per ship in the Ashlands ocean).
[Cleanup] FloatingDropsRun false one-shot Set true to scan for item drops and felled logs floating in water. Resets itself. Dry run unless the next key is true. ~50 ms main-thread stall on a 698k-ZDO world.
[Cleanup] FloatingDropsDelete false runtime With Run: delete what the scan finds. Hourly backups first.
[Compat] KnownGoodBuilds 1.0.7, 1.0.12, 1.0.14, 1.0.15, 1.0.16 patch-time Game versions this plugin build was verified against. Comma-separated.
[Compat] DisableOnUnknownBuild true patch-time On an unlisted version, run only measurement, the send-rate cap and the constant swap.

How it works

Each row is one measured problem and the patch that answers it.

Problem by problem
Problem in vanilla What ValheimTune does Measured on a 690k-object world
Server is hard-capped at 30 fps, so every sync round waits for a slow frame TargetFrameRate knob 30 -> 60 fps, ~3 % CPU
Every sync round rescans every object near every player Dirty sets: only objects that changed since the last round are considered; full scans only on join, zone change and every 30 s 4.1 ms -> 0.07 ms per player per round
10 KB send window and a hidden 150 KB/s per-connection Steam cap Both raised, all players served every 50 ms instead of one per frame Join streams 2.8x faster
Every update from every player is relayed to every other nearby player, ~17 times a second for anything that moves Relay throttle: non-prioritised objects (fish, drifting items, pieces) re-sent to a given player at most every 200 ms; players and creatures exempt ~55 % fewer relays with two players at the base
Autosave clones the whole world on the main thread, then a writer thread reads memory the game keeps changing (a torn-save race) Sliced save: the world is serialised on the main thread in 6 ms slices into a buffer; the writer thread only writes 381 ms freeze in one frame -> 6 ms slices over ~350 frames
During a join, every candidate object is fully sorted each round to pick the ~300 that fit Top-K selection: a bounded heap keeps the best 512, no allocation Join sync cost ~11 -> ~5.5 ms per call; closes a vanilla field-table leak on the way
A game update silently runs old patch logic on new code Version gate: replacement patches only run on a build listed in the config; anything else logs a warning and runs vanilla plus measurement Tested both ways on the live server
Incremental saves only write chunks the server changed; builds, chests and signs players changed can be skipped and revert on restart Save-dirty fix: a client update marks its chunk not measured live
Spawner/creature links get new hashes every save; a link split across two chunks breaks after a restart and the spawner spawns a duplicate Spawner-link fix: both sides are saved together not measured live
Every disconnect or rejected join sleeps the main thread for 100 ms No disconnect sleep; Steam's linger flushes instead 100 ms -> 0 per disconnect
Hundreds of item drops and felled logs floating in water forever, each one a sync every round One-shot scan and optional delete 1,446 objects removed; idle inbound traffic 800 -> ~650 updates/s
The 18 patched methods

Harmony patches on 18 methods of the dedicated-server assembly, all in Patches/:

Method Patch Purpose
ZDOMan.CreateSyncList prefix + postfix Dirty sets, relay throttle; timing
ZDO.DataRevision / OwnerRevision setters postfix Mark changed objects
ZDOMan.CreateNewZDO postfix Mark objects the server creates itself
ZDOMan.GetSaveClonePerChunk postfix Save both sides of a spawner link
ZDOMan.PrepareSave postfix Prune destroyed-object records
ZDOMan.RemovePeer / ReleaseZDOS prefix + postfix Timing
ZSteamSocket.Close transpiler Remove the 100 ms disconnect sleep
ZoneSystem.RPC_SetGlobalKey prefix Drop repeat global-key sets
ZDOMan.ServerSortSendZDOS prefix Top-K selection
ZDOMan.SendZDOs transpiler + prefix/postfix Window constants; timing
ZDOMan.SendZDOToPeers2 prefix All players per round
ZRpc.Update prefix Receive cap
ZSteamSocket.RegisterGlobalCallbacks postfix Steam send rate
ZDO.Deserialize postfix Per-prefab tally; mark the save chunk of a client update
Heightmap.RebuildRenderMesh prefix Skip the render mesh on a headless server
Game.CollectResources prefix Defer the hourly asset unload until the server is empty

The save path is no longer patched. Valheim 1.0 writes one file per chunk and clones only dirty chunks, which supersedes the sliced save this plugin used to apply on 0.221.12; measured idle on a 698k-object world, an incremental autosave writes in 3 ms against 2,713 ms for a full one.

Every patch that replaces game logic checks the version gate first and runs vanilla when it is off. Measurement, the send-rate cap and the constant swap run regardless. The constant swap refuses to apply unless it matches exactly the three constants it expects.

Pure logic (dirty-set state, the watchdog rule, the slicer, the top-K heap, the version check) lives outside Patches/ and has unit tests that run without the game.

Deliberate limits

  • The relay throttle can delay a repeated update of a fish or a rolling log by up to 200 ms. First updates ship on the next round.
  • Nothing here changes what a client is asked to render or simulate. Render distance, client-side rate limits and reliable/unreliable lanes would need a client mod and are out of scope.

Building from source

Needs the .NET 8 SDK and the dedicated server's managed assemblies.

# point the build at your server's Managed folder (or copy the DLLs somewhere)
export GameManaged=/path/to/valheim_server_Data/Managed
dotnet build ValheimTune/ValheimTune.csproj -c Release
dotnet test ValheimTune.sln

Output: ValheimTune/bin/Release/netstandard2.1/ValheimTune.dll. Private game members are publicised at compile time by BepInEx.AssemblyPublicizer.MSBuild; BepInEx.Core and HarmonyX come from the BepInEx NuGet feed (nuget.config).

deploy.sh is the reference server's build-copy-restart loop; set HOST and adapt the docker lines to your setup.

When the game updates

See docs/PORTING.md. Short version: the plugin notices, switches its replacement patches off, and logs it. Players play vanilla plus the stats line until a rebuild adds the new version to KnownGoodBuilds.

License

MIT. Not affiliated with Iron Gate or Coffee Stain.

Reports from other servers welcome — open an issue with your stats line.

CHANGELOG

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 line Removed connection from N orphan spawn:s shows 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.Close sleeps 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.
  • SendWindowBytes max 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 >= SendWindowBytes now leaves SendZDOs vanilla instead of applying a window that sends nothing.
  • Turning DeferAssetUnload off 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] KnownGoodBuilds shipped list is now 1.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 SendZDOs window transpiler leaves the method untouched while SendWindowBytes and MinHeadroomBytes are both vanilla. No change for anyone who tuned these values.
  • New [Steam] OverrideSendRate and [Sync] OverrideSendWindow (default true). Set either to false to 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.7 and 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 in Compat.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 is DisableOnUnknownBuild and 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.SaveAsync is gone, replaced by SaveChunks/SaveChunk/SaveCleanup writing one file per chunk, and GetSaveClonePerChunk clones 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] SaveSliceMs knobs. 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-peer SimulationDistance negotiated in ZNet.RPC_RequestValidSimulationDistance and clamped server-side to min(client request, server cap). DirtyPatches now mirrors the ring predicate in ZDOMan.FindSectorObjects (Chebyshev ring and ZonesWithinRadius, except in classic mode) instead of calling the removed ZNetScene.InActiveArea(sector, zone, radius). Zone indices are Vector2s, not Vector2i. New ZoneRingTests cover the ring maths.
  • Version.m_networkVersion renamed to Version.c_networkVersion.
  • [Compat] KnownGoodBuilds default 0.221.12 -> 1.0.7.
  • Unchanged and rebuilt as-is: B2 top-K, R2 relay throttle, the SendZDOs constant swap, the receive cap, the Steam send rate, TargetFrameRate, G1 deferred asset unload, B4a skip render mesh, the watchdog and the stats line. ZRpc.cs and ZSteamSocket.cs are byte-identical between 0.221.12 and 1.0.7; ServerSortSendZDOS and SendZDOToPeers2 have identical bodies.
  • 38 unit tests.

0.6.0 — 2026-09-07

  • G1 deferred asset unload ([Server] DeferAssetUnload, default off): vanilla runs Resources.UnloadUnusedAssets() every hour (Game.cs:239 InvokeRepeating, the 3599 s check at Game.cs:299). Measured on GalinBalin at 443-607 ms of main-thread stall, and the cost is independent of what it frees - Unity's MarkObjects walks 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 through Game.CollectResources, so one prefix covers them. The collection is deferred, not skipped: it runs the moment the last player disconnects, or after AssetUnloadMaxDeferMinutes (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 on Heightmap.RebuildRenderMesh that returns false on a dedicated server. The server regenerates a heightmap for every ghost zone a player explores (ZoneSystem.SpawnZone -> the zone prefab's Heightmap.OnEnable) and for every terrain edit that loads with one (TerrainComp.Poke); the render half is (m_width+1)^2 vertices, colours, UVs and indices plus RecalculateNormals/Tangents/Bounds on a -nographics process. The collision mesh, paint mask and material instance are untouched, and every other read of m_renderMesh is null-guarded. New meshSkips counter 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.Deserialize postfix 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

  • TopKSort default 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 vanilla m_tempSortValue leak as a side effect. One GetZDO per id in the dirty-set drain.

0.3.1 — 2026-09-07

  • SlicedSave default 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; drained on the stats line; ZDOID.None ignored in Mark. AllPeersPerRound default 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.
  • DirtySets default 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; ConstSwap IL constant swap; Tier C knobs (send window, headroom, all-peers round, Steam send rate); per-peer packet cap; TargetFrameRate knob. Final review fixes: restart grace 150 s, config ranges, per-peer try/catch, measurement hygiene.