Networking in the doubles
Reference for ZPackage, routed and per-object RPCs, console commands, the join handshake and peer identities in Valheim.Testing.Doubles. The core page is Valheim.Testing.Doubles.
Purpose
The doubles run a mod's network code in-process. A handler receives the bytes the game would send, a peer connects only through a linked socket double, and routing follows the game's rules without real timing, loss or reordering. Each table says what the doubles do and where they throw or differ from the game. Members the game lacks are marked in the member index; use them in tests only. Setup and ValheimWorldScope are on the core page, world state (ZDOs, global keys, terrain) on doubles-world, Unity objects, config and Harmony on doubles-unity.
ZPackage
| Area | The doubles do | Fails or differs when |
|---|---|---|
| Write/Read pairs | Every pair of the game, with its byte encoding | — |
| Numbers | Little-endian | — |
| Strings | 7-bit length prefix, UTF-8 | — |
| Byte array, nested package | Int length prefix | — |
ZDOID |
Creator's session id, then a uint | — |
| Vectors, quaternions, small rotations, item counts | The game's layout | — |
| Stream position | One stream, written and read at the same position | Reading without SetPos(0) after a write reads past your data |
| Other members | GetArray, GetPos, Size, Clear, Load, byte[] and base64 constructors |
— |
| Compression | The game's gzip level (Fastest); each side reads the other's stream | Compressed bytes can differ by header bytes; compare what is read back |
ZRpc.Serialize(args, ref pkg) |
Writes arguments with the routed-RPC type table | — |
| Version strings | ZNet.VersionString and ZNet.NetworkVersion default to Valheim.Testing.Doubles.DoubledGame.Version and NetworkVersion |
— |
Routed RPCs
ZRoutedRpc carries arguments through a ZPackage, so a handler gets copies.
| Area | The doubles do | Fails or differs when |
|---|---|---|
| Argument types | int, uint, long, float, double, bool, string, ZPackage, List<string>, Vector3, Quaternion, ZDOID, HitData, ISerializableParameter |
Any other type (an enum, a byte, a byte[], a Vector2i): ArgumentException naming the argument; the game skips it silently |
| Handler parameters | Read back by the handler's own parameter types | An int sent to a long parameter, fewer arguments than parameters, or an unreadable parameter type: InvalidOperationException naming the parameter |
| Type check | Exact | An int read as a uint, or a subclass sent to a base-class parameter: refused, where the game misreads |
| Extra arguments | Trailing extras are ignored, as in the game | — |
HitData |
The game's fields, defaults and layout (a flags word, then non-default values) | Damage is not calculated; Skills has only its SkillType ids |
Everybody or this peer |
Runs the local handler at once, before anything is sent | A "client-only" broadcast handler also runs on the server; on a listen-server host its client branch runs too |
| Unregistered name | Dropped without an error, recorded in ZRoutedRpc.Dropped |
— |
| Registering twice | Throws, as in the game | A mod registering on the default static ZRoutedRpc.instance more than once: install a fresh one per test with ValheimWorldScope.WithNetwork() |
| Handler arity | Up to six arguments after the sender | — |
IsRegistered(name) |
On ZRoutedRpc and ZNetView; test-only |
Mod code that calls them fails against the game |
| No target | The server calls itself; a client calls its first ready peer, or Everybody with none |
— |
GetServerPeerID() |
Returns that target; public here | Private in the game |
PeerId |
ZNet.GetUID() unless SetUID sets another |
— |
ZNet.GetUID() |
The session id (ZDOMan.GetSessionID(), 1 by default and with no ZDOMan), or the side's ZNet.Uid in a two-sided test |
Never 0, which is Everybody |
| Sending | Server: to the target, or a broadcast to every ready peer except its sender. Client: to its ready peers | A peer with Ready = false has m_uid 0 and receives nothing |
Sent |
Each peer's message as that peer decodes it | — |
Invoked |
Every call as made | — |
Deliver(sender, method, args) |
Handles a call arriving at this peer | — |
Receive(sender, target, zdo, method, args) |
Handles any arriving call, including one the server relays | — |
| Message size | Steam's 512 KiB limit on Steam only | PlayFab's own send limits are not modelled |
Deliver a server broadcast to a client by hand. serverId and keys are illustrative:
ZRoutedRpc.instance.Deliver(serverId, "GlobalKeys", keys);
The global-key handlers themselves are on doubles-world.
Per-object and Jotunn RPCs
| Area | The doubles do | Fails or differs when |
|---|---|---|
ZNetView.Register, Unregister, InvokeRPC |
As in the game, through the routed RPCs with the object's ZDO id | Register takes up to six arguments |
InvokeRPC(method, ...) |
Goes to the object's owner | An object without an owner broadcasts |
InvokeRPC(ZNetView.Everybody, ...) |
Goes to everyone | — |
| Target view | The view whose ZDO is in ZDOMan.instance and, with a scene installed, whose object is live in it |
A view around a bare ZDO, with no game object, is found through its ZDO |
| Name the view did not register | Logs Failed to find rpc method <hash> through ZLog, lands in ZRoutedRpc.Dropped |
— |
| Missing object | Dropped silently, lands in ZRoutedRpc.Dropped |
— |
| Jotunn RPCs | Kept by name in NetworkManager.Instance.Rpcs; record what they send |
— |
Console commands
| Area | The doubles do | Fails or differs when |
|---|---|---|
Terminal.ConsoleCommand constructor |
Registers the command under its lower-case name | — |
Terminal.TryRunCommand |
Runs it with that terminal as args.Context; prints an unknown command or a failed failable action |
— |
Terminal.Output |
Holds what was printed | — |
| Cheat command | Needs devcommands (Terminal.m_cheat) on the server, then acknowledged cheats (Achievements.CheatedAtAll, the game's confirmcheats) |
Otherwise prints 'x' is not valid in the current context. or That command is a cheat, please enter 'confirmcheats' ... and does not run |
| Cheat gates | Start closed, as on a fresh dedicated server; ValheimWorldScope.WithCheats() opens both, restored on dispose |
A command marked isCheat: true is refused in a plain scope |
| Server-only command | Needs the server | — |
| Invalid remote command on a client | Recorded in ZNet.instance.RemoteCommands as sent to the server |
— |
| Admin checks | Not modelled | — |
An empty command table per test is WithCommands on the core page.
Peer connections and the join handshake
These doubles add global types (ISocket, SocketDouble, ZSteamSocket, ZPlayFabSocket, ZRpc, SyncedList, OnlineBackendType) and namespaced ones (Steamworks.CSteamID, Splatform.Platform, Splatform.PlatformUserID). If your test project has its own stubs with these names, delete them or add members through partial.
| Area | The doubles do | Fails or differs when |
|---|---|---|
ZNetPeer.m_rpc, ZNetPeer.m_socket |
A ZRpc and an ISocket a mod's handshake or admin check reads |
— |
SocketDouble.Link(a, b) |
Joins two ends in-process; a message arrives when the receiving side's ZRpc.Update(dt) or ZNet.UpdatePeers(dt) runs |
— |
ZRpc.Invoke |
Writes the method's hash and the arguments (routed type table) and sends them | Nothing is sent once the connection is closed |
ZRpc.Register |
Replaces an earlier handler of the same name | — |
| Call without a handler | Ignored, kept in Dropped |
— |
| Short package | A package ending before the parameters are read stops Update with ErrorCode.IncompatibleVersion; ZNet reports ErrorVersion |
A peer sending fewer fields than the handler reads |
| Exception in a handler | Logged only, as in the game; kept in Exceptions |
Includes reading past the end of a ZPackage argument |
ZRpc.Deliver(method, args) |
Handles one call as if it had arrived | — |
| Pings, timeout | Not modelled; a connection sends only what its side invokes | — |
The handshake runs between two ZNet objects: the server is ZNet.instance, the client a second ZNet with its own Uid. Call OnNewConnection(peer) on each side, then UpdatePeers(0f) on each in turn.
| Area | The doubles do | Fails or differs when |
|---|---|---|
| Refusals | The game's Error codes; the client's Status becomes the matching ZNet.ConnectionStatus |
— |
| Network version | 3 ErrorVersion for another NetworkVersion |
— |
| Ban or permit list | 8 ErrorBanned |
— |
| Password | 6 ErrorPassword when the test sets the server's RefusePassword |
Salt, hash and the password dialog are not modelled |
| Already connected | 7 ErrorAlreadyConnected |
— |
| Player limit | Not modelled | 9 ErrorFull never occurs |
| A joined peer | Ready under its id | — |
OnNewConnection, RPC_PeerInfo |
Public, so a test applies a mod's patch as Harmony would: call the prefix before OnNewConnection; re-register "PeerInfo" on m_rpc with a handler that runs the prefix, then RPC_PeerInfo |
— |
Version message from the client's OnNewConnection prefix |
Goes out before the game's ServerHandshake on the same ordered connection, so it reaches the server before the peer info | A mod that sends its version later (in reply to the server's) is turned away by a check that refuses on arrival; a check that holds the peer info passes it |
| Connection status | Per ZNet |
Static in the game |
HandshakeTests has a mod version check that refuses a peer whose version message has not arrived by the time its peer info does.
Steam and crossplay identities
| Area | The doubles do | Fails or differs when |
|---|---|---|
ZSteamSocket(steamId) |
GetHostName() is the bare SteamID64 (76561198000000002) |
— |
ZPlayFabSocket(platformUserId, remotePlayerId) |
GetHostName() is the platform-prefixed id (Steam_76561198000000002, Xbox_2535…); end point playfab/ plus the PlayFab entity id |
— |
peer.m_socket cast to ZSteamSocket |
Throws InvalidCastException for a crossplay peer |
Inside an RPC handler the game only logs it |
ZNet.instance.IsAdmin(rpc.GetSocket().GetHostName()) |
A Steam id matches Steam_<id> or the bare id; another platform only <Platform>_<id>; the display form V_<id> matches too |
Comparing the host name with the list's lines misses a Steam peer listed as Steam_<id> |
m_adminList, m_bannedList, m_permittedList |
SyncedLists (new SyncedList(entries)), compared as exact strings |
List files are not modelled; nothing is trimmed |
ZNet.OnlineBackend |
The game's static m_onlineBackend; selects Steam or PlayFab for the handshake |
— |
Splatform.PlatformUserID |
Parses and formats the ids | — |
| Message sizes | The sockets check none | — |
Limits
- Networking is in-process. Peers connect only through linked socket doubles; there is no real timing, loss or reordering.
- Not modelled: Steam session tickets and PlayFab authentication (every peer passes), invite keys, the world data in the server's peer info, the game's other connection RPCs, pings, the timeout and the player limit.
HitDatadamage, console admin checks and PlayFab send limits are not modelled.- Test real networking with Valheim.Testing.Game against a real server.