You are viewing a potentially older version of this package. View all versions.
DeathMonger-SpreadTheLoad-0.1.4 icon

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.

Date uploaded 13 hours ago
Version 0.1.4
Download link DeathMonger-SpreadTheLoad-0.1.4.zip
Downloads 30
Dependency string DeathMonger-SpreadTheLoad-0.1.4

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

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

Changelog

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.

0.1.3

  • A tree, rock or ore vein now goes to whoever is hitting it. Vanilla never does this - 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 there and back, for that tree and the next one. A chopping session is hundreds of interactions against a handful of objects, which makes this the commonest way a group feels somebody else's frame time.

    Resources only. A creature carries live AI state that is not all replicated, so moving one mid-fight can make it re-acquire its target or re-path - a real hitch in the least welcome moment, left alone until it can be measured rather than reasoned about.

    Three details that matter: the transfer waits 0.4s so the swing that triggered it lands first (the old owner's handler checks ownership, and changing it immediately drops that hit); a dwell time stops two players bouncing one tree between them; and it never hands an object to a player work is being steered away from, which would only churn.

  • Ship prefabs are found in the right registry. The scan read ZNetScene.m_prefabs, the list serialised with the scene, and found the five vanilla hulls and none of TheGreatestShips'. Mods register into m_namedPrefabs, which is what GetPrefab reads. Now 13 hulls including every DM_* ship.

0.1.2

  • The yield list is switched off with the mod. IsYielding did not check Enabled, so with the mod turned off but names still configured it answered yes about a rule that was not in force. The ship pass consults it, so this was a real bug rather than only a display one.
  • A small public API, SpreadTheLoadApi, so DiagnoseServerLag can grey the rows of players work is being steered away from. This mod stays server-only and gains no client half; DSL's server half reads it and sends the answer down with its own report.

0.1.1

Both fixes come from the first run on a real server.

  • The "NOT WORKING" alarm was a false positive. It fired on a healthy server with no conflicting mod installed. IsInPeerActiveArea is only reached from the second half of (!zdo.HasOwner() || !IsInPeerActiveArea(...)), so an ownerless object short-circuits past it and an object owned by the peer being processed takes the other branch entirely. It is consulted only when one player's active area contains an object somebody else owns - which never happens while everyone is off in their own corner. Silence now only corroborates the Harmony inspection instead of accusing on its own.
  • Modded ships were missed. The prefab scan ran once at startup and found the five vanilla hulls and none of TheGreatestShips', because mods register their prefabs after ZNetScene exists. It now re-scans whenever the prefab list grows, which self-corrects however late a mod registers.

0.1.0

  • First build. Server-side only; nothing to install on clients.
  • Steers object ownership away from named players. Valheim simulates each object on the one machine that owns it, gives ownership to whoever was in range first, and never rebalances - so one struggling client's frame time becomes everybody else's input delay. This hands those objects to another player who is already in range, and hands back the ones already held. Anything only the named player is near stays theirs, so nothing is ever left unsimulated.
  • Players are identified by network id, not by character name. A name is used once to find somebody; their id is remembered in SpreadTheLoad-known-ids.txt and used from then on, so the setting survives them playing a different character.
  • Optionally finds struggling machines by itself, off by default. Not by ping or connection quality - the machine this was written for had a 24 ms ping and ran at 16 fps - but by timing the gaps between the updates each client sends. Rate saturates around 20 Hz and tells you nothing; a 479 ms hole in the stream tells you plenty. Clearing a flag is deliberately harder than earning one, and the last healthy player is never flagged.
  • Ships follow the helm. Vanilla only reassigns a ship when its owner is not aboard, and then picks an arbitrary passenger instead of the captain, while steering is batched to the owner every 0.2s and the physics runs only there - so the person steering often waits a quarter second for every turn. Ship prefabs are found by component, so modded hulls are covered without a list. Whether a yielding player keeps the helm is a setting, defaulting to yes.
  • Config changes take effect without a restart. BepInEx keeps the value parsed at startup, so without a file watcher an edited setting would do nothing until the process restarted - which on a dedicated server means disconnecting everybody to change who is being steered away from. The file is watched and reloaded, and every setting is read fresh each pass, so a change lands within a second.
  • Says so when it is not working. ValheimPerformanceOptimizations replaces the vanilla method this mod works through, which would otherwise leave it silently inert. It checks both by asking Harmony who else patched that method and by noticing its own decision was never consulted while players were connected.