Confirm with your password

SpreadTheLoad

Server-side performance optimization for multiplayer: moves object ownership off a struggling client so other players stop inheriting its lag and desync. Valheim never rebalances who simulates what; this does. No client install needed.

Client-only

· Website · 💗 Donate

Stars
0
Downloads
10
Version
0.2.1
Updated
Author
DeathMonger
Virus scan
✓ Scan successful
Runs on
Client-only

Description

SpreadTheLoad

Server-side performance optimization for Valheim multiplayer. It moves object ownership off a struggling client so that everybody else stops inheriting that machine's lag.

Install on the dedicated server only. No client needs it, including players running no mods at all. There is no prefab, no RPC and no version check, so nobody is locked out.

The problem

Valheim simulates each creature, ship and workbench on exactly one machine: whichever client owns it. Ownership goes to whoever was in range first, and vanilla never rebalances it — whoever loads a zone keeps it until they walk away.

That is fine until the owner is the slowest machine in the group. Then every arrow you fire at a creature it owns, and every swing at a tree it loaded, travels to that machine and back before anything happens. Its frame time becomes your input delay, and the lag you feel has nothing to do with your own hardware or your ping to the server.

Measured on a three-player server: two clients averaging 18 ms a frame, one at 63 ms, and a round trip through the slow one of 144 ms against an 18 ms ping to the server.

What it does

Three things, all server-side:

  1. Objects follow whoever is using them — a tree or ore vein moves to the player chopping it
  2. Ships follow whoever is steering — vanilla picks an arbitrary passenger instead
  3. Named or detected struggling machines are steered away from shared work

For the third: name the players whose machines should not be handed shared work, and the server stops giving them objects that somebody else is also standing near — and hands back the ones they are already holding, which vanilla will not do on its own.

They still own anything only they are near, so nothing is ever left unsimulated and no creature freezes. When they are the only player in a zone, nothing changes at all.

What it does not do

It will not improve the frame rate of the machine it steers away from. Ownership costs that machine CPU, and a struggling client is usually short of something else — on the server this was built for, the slow client was using less CPU than the healthy ones while rendering a third as many frames. Measurement there found ownership made no difference to its frame time either way.

This mod protects the other players. If you want the slow machine itself to run better, that is a graphics settings and hardware question, and DiagnoseServerLag will tell you which.

Configuration

BepInEx/config/DeathMonger.SpreadTheLoad.cfg, generated on first run.

Setting Default Meaning
Enabled true Turn off for stock behaviour without removing the mod.
Yield Players (empty) Who to steer work away from. Empty means the mod does nothing.
Remember Ids true Learn and remember network ids, so names keep working.
Log Activity false Occasional summary line; never one line per object.
Ownership Follows Attacker true Give a tree, rock or ore vein to whoever is hitting it.
Attacker Dwell Seconds 5 How long it stays put afterwards.
Auto Detect Struggling Players false Find struggling machines without naming anyone.
Stalls Per Minute 6 How many stalls a minute before flagging someone.
Assign Ship To Captain true Give a ship to whoever is steering it.
Yielding Player Retains Boat Helm Ownership true Whether a yielding player keeps the helm when they steer.

Naming players

Yield Players takes Steam ids or character names, comma separated.

A character name is not an identity — the same person on a second character stops matching, and somebody else picking that name starts matching. So a name is used once, to find the player; their network id is then written to SpreadTheLoad-known-ids.txt beside the config, and from then on they are matched by id whatever they call their character. Delete a line from that file to forget someone.

Steam ids are the reliable thing to enter. The server logs them as Got connection SteamID 7656119... whenever somebody joins.

Detecting struggling players automatically

Yield Players requires you to know who is struggling. Auto Detect Struggling Players works it out instead — off by default, because deciding on its own to move work away from someone is a judgement you should opt into.

It does not use ping or connection quality, and that matters. The client this mod was written for had a 24 ms ping — better than one of the healthy players — a clean connection, and 16 frames a second. Every network-level measurement the server can take said it was fine.

What the server can see instead is timing. A client's ZDO updates come from its own update loop, so they arrive at whatever pace that machine manages. The rate is useless — the sender is gated at about 20 Hz, so 30 fps and 200 fps look identical — but the gaps are not. A 479 ms frame, the worst measured on that machine, is a 479 ms hole in the stream, and no healthy client produces one.

So a stall is a gap over 0.3 s, and a player is flagged above Stalls Per Minute averaged over two minutes. Clearing the flag takes five clean minutes — deliberately harder than earning it, so a borderline machine does not flap ownership back and forth.

Honest limits:

  • A gap says that machine stopped sending, not why. A frozen client and a hiccuping connection look the same. That's acceptable, since routing other players' work through either is a bad idea.
  • The last healthy player on the server is never flagged. If everyone is struggling there is nobody to hand work to, and flagging everyone would only churn ownership.
  • Flags live in memory only. They are never written to the known-ids file and are dropped when the player disconnects — that file is for identities you chose, not guesses the mod made.

Chopping, mining and anything else you hit

Vanilla never gives a resource to whoever is hitting it. TreeBase, TreeLog, Destructible and MineRock5 all open their damage handler with if (!m_nview.IsOwner()) return;, and nothing anywhere calls ClaimOwnership — so the machine that loaded a tree keeps it, and every swing anyone else makes travels to that machine and back. For that tree, and the next one, indefinitely.

A chopping session is hundreds of interactions against a handful of objects, which makes this the commonest way a group ends up feeling one person's frame time. Ownership Follows Attacker moves the object to whoever is working it, so the first swing lands remotely and the rest are local.

The server sees the swings because it already relays them — it forwards every client-to-client RPC — so nothing is installed on any client.

Three details worth knowing:

  • The transfer waits 0.4 s. The current owner's damage handler begins by checking it still owns the object, so changing ownership the instant the swing arrives would make it drop that hit.
  • A dwell time (Attacker Dwell Seconds) stops two players working the same tree from bouncing it between them.
  • A yielding player is never given the object. The yield pass would take it back within two seconds and the next swing would move it again, which is worse than leaving it alone.

Resources only. A tree's entire state is its health in the ZDO, so handing it over costs one owner revision and loses nothing. A creature carries live AI state — its target, its path, its alert timers — that is not all replicated, so moving one mid-fight can make it re-acquire or re-path. That is a real hitch in the least welcome moment, so creatures are left alone until it can be measured rather than reasoned about.

Ships

A ship is simulated by its owner, and steering is sent to that owner in 0.2 s batches, so a captain who does not own the hull waits roughly a quarter second for every turn. Vanilla only reassigns a ship when its owner is not aboard, and then hands it to an arbitrary passenger rather than to the captain - so the wrong person often owns it.

With Assign Ship To Captain, the ship follows the helm.

Note this can give a yielding player an object the rest of the mod would take away, whenever they are the one steering. That is deliberate, and switchable. Yielding Player Retains Boat Helm Ownership decides it:

  • On (default) - they keep the helm. Their steering is responsive; the hull lurches for everyone aboard whenever their machine stalls past Unity's catch-up limit.
  • Off - the ship goes to a capable player. Smoother for passengers, but the captain steers through a delay, and a captain fighting a mushy helm is the one who puts the boat into a rock.

Which is better is an open question - reasoned about, not measured. Try both.

An unattended ship has no captain, so it falls back to the ordinary yield rules and moves off a struggling machine like anything else.

Open chests

A container someone has open stays with them, for the same reason a helm does. Vanilla's container code assumes whoever has the window open owns it: only the owner writes the chest back, and the panel is repainted from the network copy whenever the data revision moves. Move the chest to another machine mid-session and the stack they just dragged out reappears in the chest while the one they took sits in their inventory — nothing is duplicated, and closing the chest clears it, but it is not a thing anyone should have to see.

There is no setting for this. A chest is released as soon as it is closed.

Compatibility

Known conflict: ValheimPerformanceOptimizations replaces ZDOMan.ReleaseNearbyZDOS with its own implementation, which is the vanilla method the yielding half of this mod works through. With it installed, Yield Players has no effect. Helm ownership is unaffected, because the ship pass runs on its own rather than through that method.

Rather than fail quietly, it checks twice and says so in the server log — once by asking Harmony who else has patched that method, and once by noticing that its own decision was never consulted while players were connected. If you see a NOT WORKING line, believe it.

ServersideQoL_MultiplayerTweaks also reassigns ownership, but by proximity — it gives objects to the closest player. That directly contradicts this mod whenever the closest player is the one you are steering away from. Run one or the other.

Credits

The conflict-detection approach is borrowed from balrond_core_optimizer, which checks Harmony ownership of its targets and stands down rather than trusting patch order.

Changelog

0.2.1 Latest
  • An object goes to the player standing on it when its owner is nowhere near. Greg's rule, and it fixes the case people actually feel: shoving a boar toward its pen. Every push travels to whoever owns the boar and the corrected position comes back, so the animal lurches - and the owner may be a hundred metres away with no interest in it. The same goes for opening somebody's chest, or anything you are standing over.

    Take at 2 m, give up only at 5 m. That gap is what stops an object flitting between two people: once the near player owns it they are at zero distance, so the rule cannot fire again until somebody else is nearer than 2 m while they are further than 5. It settles by construction rather than by a cooldown.

    Nothing to do with anyone's machine being slow, unlike everything else here - the object is simply in the wrong hands. Distances come from each player's character ZDO, which syncs every 67 ms since the pacing change, rather than the zone-grained peer reference position.

    The active-area predicate is only told an object's position and its owner, never which player is being offered it, so a small patch on ReleaseNearbyZDOS records whose pass is running. Without it the rule could tell that the owner was far away but not that this player was close, and vanilla would have handed the object to whichever peer happened to be iterating.

  • The guards now apply to every rule, not just the yield one. The open-container and combat checks sat inside the yield branch, so the rule above would have bypassed both - handing away a chest somebody had open, or a creature mid-fight.

  • Ownership is frozen near fighting. The attacker rule already refused to move anything living, because a creature's target, path and alert timers are not all replicated and the new owner restarts its AI from whatever the ZDO holds - which in play is a boss that stops attacking. The yield rule had no such guard, which was an inconsistency rather than a decision: it works through a predicate that is handed a position and an owner and never learns which object it is being asked about, so it cannot tell a troll from a fence post.

    Greg reported exactly that symptom after a Gerhaffa fight, and it does not need the yielded player to have arrived first - ownership moves whenever an owner leaves an object's active area, so simply walking through was enough to acquire the boss and have it taken away again.

    Since the object cannot be identified, the guard is spatial: every damage RPC the server relays marks a place and a time, and ownership stops moving within 40 m of recent blows until 15 quiet seconds have passed. Coarse - it protects the fence posts in the fight too - but it errs the safe way, and a fight is the one moment when rebalancing has nothing to offer.

0.2.0
  • Every player is sent their world update each cycle, instead of one player per frame. Vanilla serves exactly one peer per server frame, so each player hears from the server every (players + 1) frames. Measured on bahnsheim at its 30 Hz cap, and the model is exact at both ends: one player (1+1) x 33.3 = 67 ms, measured 67; five players (5+1) x 33.3 = 200 ms, measured 199, 200, 201 and 204.

    Five updates a second, with four other people in the world. Everything a player does not own - where everyone else is, what their creatures are doing, the ship they are standing on - arrives at that rate and is interpolated in between. It is the one cost that grows with the size of the group, and it is invisible to every other measurement: in that same capture the server held a perfect 33.3 ms tick on 17% of one core and stalled 3 times in 1800 seconds.

    The 50 ms gate in the vanilla loop never signifies, because m_sendTimer keeps accumulating during the serving frames and is always long past 0.05 by the end of a cycle. But it reads like an intended 20 updates a second, which the per-frame loop quietly turns into 20/N. This restores that intent: Updates Per Second, default 20, no longer divided by the player count.

    Safe because the real flow control is untouched. SendZDOs refuses outright when the socket's send queue is backed up and caps each package at what is left of 10 KB, so the socket's capacity still decides and the gain is self-limiting rather than a flood. On bahnsheim that queue sits at a median 3.7 KB against a refusal threshold near 8 KB, so expect real improvement rather than a clean six times.

0.1.4
  • A player being hit is no longer treated like a tree. The attacker rule skipped creatures by looking for BaseAI, and a player does not have one - so when something hit a player, the damage RPC named that player's own ZDO and their character was handed to whoever swung. The receiving client then found it owned a Player that was not its local one and did what vanilla does in Player.FixedUpdate: logged "Destroying old local player" and destroyed it. The player's screen went black and they had to rejoin. The test is now Character, which every creature has too, so nothing living is ever moved.

  • A chest stays with the player who has it open. Vanilla's container code assumes whoever has the window open owns the ZDO - OnContainerChanged saves only if (IsOwner()), and Load() repaints the panel whenever the data revision moves. Steering a yielded player's chest away mid-session broke both: the stack they dragged out reappeared in the chest a moment later while the one they took sat in their inventory. Nothing was ever duplicated and closing the chest cleared it, but it looked alarming. Open containers are now held with their user, the way a ship is held with its captain. The open request names the chest - it is routed through the server, and only travels at all when somebody else owns it - and the container's own InUse flag says when to let go.

4 older entries, full version history & downloads →

Manual installation instructions
1

Install BepInExPack Valheim

BepInExPack Valheim is required to run mods in Valheim.

Download BepInExPack Valheim 5.4.2351 · View mod page

Check out the mod page for detailed installation instructions.

2

Install SpreadTheLoad

This mod only needs to be installed on the client.

Download SpreadTheLoad 0.2.1

Extract the ZIP and place the file(s) into the BepInEx/plugins/ folder inside your Valheim game folder.