Confirm with your password

ValheimNetProbe

Diagnostic-only measurements of Valheim 1.0 RTT, combat RPCs, pickups, and ZDO send queues. Does not change networking.

Client & Server

· Website

Stars
0
Downloads
19
Version
0.1.2
Updated
Author
cjayride
Virus scan
✓ Scan successful
Runs on
Client & Server

Description

ValheimNetProbe

Diagnostic-only BepInEx 5 plugin for Valheim 1.0. It measures Taiwan ↔ Australia combat and pickup delay. It does not change send rates, ownership, ZDO scheduling, interpolation, RPCs, combat, or pickups.

Install on the dedicated server and every client in the test. Clients show the overlay. The server writes the peer and event log.

Console

  • netprobe / netprobe status — print the current snapshot
  • netprobe export — write BepInEx/ValheimNetProbe/events-*.csv
  • netprobe clear — empty the in-memory buffer

Config: BepInEx/config/cjayride.valheimnetprobe.cfg

What was measured (Valheim 1.0)

Inspected in assembly_valheim.dll on this machine. These are the live types.

Network / RTT

  • ISocket.GetConnectionQuality via ZNet.GetNetStats / each ZNetPeer.m_socket — this is the game's ping (Steam/PlayFab). This is the RTT number.
  • ZRpc.GetTimeSinceLastPing is not RTT. ZRpc.UpdatePing sends a keepalive (method == 0) every 1s. The reply only resets a timeout timer. Logged as keepalive.
  • ISocket.GetSendQueueSize — bytes waiting on that peer. ZDOMan.SendZDOs skips a peer when this is above 10240, or remaining budget is under 2048.

No extra ping RPC was added. The socket quality value is what the game already uses.

Combat path (do not guess — this is the 1.0 path)

  1. Player.StartAttack — local swing starts.
  2. Attack.OnAttackTrigger — melee/area/projectile runs on the attacker.
  3. Character.Damage → ZNetView.InvokeRPC("RPC_Damage") → ZRoutedRpc.InvokeRoutedRPC(owner, zdo, "RPC_Damage").
  4. The dedicated server forwards that RPC to the ZDO owner. It does not apply damage unless the server owns the creature.
  5. Owner runs Character.RPC_Damage (early-out if not owner) then ApplyDamage / SetHealth on the ZDO.

Remote HP is a ZDO float (ZDOVars.s_health), not a second damage RPC. Observers wait for ZDOMan.RPC_ZDOData.

Pickup path

  1. ItemDrop.Interact → Pickup.
  2. CanPickup is m_nview.IsOwner() (plus a 0.5s spawn delay).
  3. If not owner: RequestOwn → InvokeRPC("RPC_RequestOwn") to the current owner, then PickupUpdate every 0.05s.
  4. Owner RPC_RequestOwn calls ZDO.SetOwner(requester) only if that machine is the owner.
  5. Requester becomes owner after the owner-change ZDO arrives, then Player.Pickup runs.

Pickup delay is often one RTT for the ownership RPC plus whatever ZDO send wait (SendZDOs is one peer per ZDOMan tick, and ticks start every 0.05s).

ZDO scheduling (read-only counters)

ZDOMan.SendZDOToPeers2: after 0.05s, send one peer per Update, then wrap. SendZDOs builds m_tempToSync, sorts with ServerSortSendZDOS (distance, age since last sync, ZDO.ObjectType.Prioritized first). Combat HP and item ownership ride this same queue. They are not a separate reliable channel.

Harmony patches (why each exists)

There is no public “on damage sent” or “on ZDO skipped” event. Prefix/postfix only record. Nothing is skipped, rewritten, or reordered.

Patch Why
Player.StartAttack postfix Local attack start
Attack.OnAttackTrigger postfix Hit window on attacker
Character.Damage prefix Attacker queued RPC_Damage
Character.RPC_Damage postfix Owner applied path
Character.ApplyDamage postfix Detailed HP apply (off unless DetailedLogging)
ItemDrop.Pickup / RequestOwn / RPC_RequestOwn / PickupUpdate Pickup ownership wait
ZRoutedRpc.InvokeRoutedRPC / HandleRoutedRPC prefix Combat/pickup RPC only (RPC_Damage, RPC_RequestOwn)
ZDOMan.SendZDOs prefix/postfix Queue size, skip/defer, pending list count
ZDOMan.RPC_ZDOData postfix Receive counter

What cannot be measured accurately

  • One number for “client → server ms” across Taiwan and the dedicated server. Different OS clocks. MonoMs is per process. NetTimeSec is Valheim's net clock and is useful on the server. Do not subtract a client MonoMs from a server MonoMs.
  • Observer “I saw the swing” frame without watching animation bones. The overlay does not invent that. Compare attacker DamageRpcSend on one client with DamageRpcOwner on the owner and RoutedRpcRecv on the server.
  • True packet loss. Socket quality is what Steam/PlayFab expose, not a custom ping series.
  • ZRpc keepalive as RTT. It is not RTT.

What each metric means

Metric Meaning
RTT GetConnectionQuality ping for that peer, milliseconds
jitter / min / max / avg Rolling stats of that ping
queue GetSendQueueSize bytes on that peer
keepalive Seconds since ZRpc ping reply (timeout detector, not RTT)
zdoDefer Times SendZDOs would skip because queue > 10 KB or budget < 2 KB
pending Last m_tempToSync.Count after a send attempt
frame Time.unscaledDeltaTime at the event (server lag vs network)
BecameOwner waitMs Time on the picking client from Pickup to owner (same clock)

Taiwan / Australia test

  1. Install the same DLL on the dedicated server, Taiwan client, and Australia client.
  2. netprobe clear on all three.
  3. Note which machine owns the target (creature/item owner= in the log). Dedicated-server ownership vs player ownership changes the hop count.
  4. TEST 1: Taiwan hits a mob 20 times. TEST 2: Australia hits the same kind of mob 20 times.
  5. TEST 3/4: each player picks up 20 item drops they do not already own (drop from the other player if needed).
  6. netprobe export on server and both clients. Compare CSV Phase rows. Same-process intervals only.

How to read the cause

  • Raw RTT: BecameOwner waitMs ≈ RTT (or ~2× RTT if owner is the other player and the server only forwards). Combat DamageRpcSend on attacker vs DamageRpcOwner on owner separated by about that same RTT. frame stays small (a few ms). zdoDefer stays near 0.
  • ZDO scheduling: RTT is modest but BecameOwner waitMs or remote HP is much larger. queue high, zdoDefer climbing, pending large on the far peer. SendZDOs is one peer per tick — a busy TW peer can delay AU's packet by extra 50ms steps.
  • RPC delay: RoutedRpcRecv on the server is late vs the attacker's send, while ZDO counters are quiet. Forward-only: server RoutedRpcRecv then owner DamageRpcOwner.
  • Server CPU: frame / ServerFrameMs spikes at the same timestamp as the event (tens of ms+). Processing itself is still the Damage/RequestOwn postfix; if those are 1–2 ms and frame is 30 ms, the server is busy, not the RPC body.
  • Ownership: PickupAttempt can=0 then RequestOwnSend then long BecameOwner. Combat owner= is the other player, so damage RPC is TW→server→AU (or the reverse), not TW→server apply.
  • Client interpolation: attack animation feels late but DamageRpcOwner and HP ZDO already happened. This probe does not hook animation interpolation; if network intervals match RTT and the feel is worse, look at client presentation next.
  • Queue starvation: AU queue and zdoDefer grow while TW stays low. ServerSortSendZDOS still shares the same 10 KB window per peer, but only one peer is served per ZDOMan update.

Possible Network Optimizer

Do not build this until the CSV shows which row is true.

A. Prioritization — Worth it if pending is large and combat/pickup ZDOs sit behind scenery. ObjectType.Prioritized already exists; a later mod could mark hit/item-owner ZDOs higher without raising global send rate.

B. Per-peer scheduling — Worth it if one peer's send uses the 0.05s slot and the other waits. Today m_nextSendPeer is strictly round-robin, one peer per Update.

C. High-latency compensation — Worth it if AU's zdoDefer is high because GetSendQueueSize stays above 10240 (bytes in flight ≈ bandwidth-delay). Raising the 10 KB cap is a last resort and is not done here.

D. Combat — Worth it if RPC_Damage is fast on the owner but observers wait on SendZDOs. Then prioritize that ZDO, do not “predict” hits.

E. Pickups — Worth it if BecameOwner waitMs >> RTT because of owner hop + ZDO wait. Safe client prediction is a later design; first try prioritizing the item ZDO after SetOwner.

F. Adaptive — Only after A–E. Use RTT, queue, and zdoDefer from this probe as inputs.

The next mod should change one of those, then re-run the same 20-hit / 20-pickup tests.

Changelog

0.1.2 Latest
  • Fix startup Harmony error: patch Humanoid.StartAttack instead of Player.StartAttack.
0.1.1
  • Logging is on from launch. Session CSV writes itself. No console command required.

Full version history & older 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 ValheimNetProbe

This mod must be installed on both the client and the server.

Download ValheimNetProbe 0.1.2

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