
PEAKNetworkingLibrary
Network library for PEAK using Steam instead of PUN.Details
PEAKNetworkingLibrary v1.0.4: Patches! 
- Backend-agnostic and auto-initialising: the library auto-selects Steam or Offline at runtime, auto-instantiates the poller so you do not need to call Initialize() or PollReceive().
- PEAKNetworkingLibrary has it's own NuGet package, you can find it here.
FAQ
Q: Do I need to create or initialize the networking service?
- No. The library auto-instantiates and initializes the correct service (Steam or Offline) on load and auto-creates a poller. Mod authors should access the service via Net.Service and do not call Initialize() or PollReceive() themselves.
Update Details & Plans & Features:
Update Details
- Patched a few issues with the library.
- Implemented IsHost() bool in INetworkingService.
Plans
- Add chunked RPC transfer for very large payloads.
- Improve documentation.
Features [Not All]
AS OF VERSION 1.0.1:
- Automatic initialization & poller. Library auto-creates service and runs receive poller; mod authors only consume Net.Service.
- ModId helpers. ModId.FromGuid(string).
- Wire-efficient keys. API accepts human-friendly strings but sends small 32-bit keys on the wire (local map + fallback).
- All old functions are still usable and can be used, though it is recommended to swap over to new system.
-
Backend
INetworkingServiceprovides one surface mods use. Swap Steam vs Offline without code changes. -
Lobby key sync (host -> clients)
Host sets small authoritative strings viaSetLobbyData. Clients read withGetLobbyDataandLobbyDataChangedevent. -
RPC discovery & invocation
Discover methods marked[CustomRPC]withRegisterNetworkObjectand callRPC,RPCTarget, orRPCToHost. -
Message serialization
Messageclass supports: byte, int, uint, long, ulong, float, bool, string, byte[], Vector3, Quaternion, CSteamID. -
Reliable vs Unreliable
ReliableTypeenum supports Reliable, Unreliable, UnreliableNoDelay semantics. -
Security (optional)
Optional HMAC signing, per-mod signer hooks, sequence numbers, replay protection. -
Framing & priority
Per-message flags, msg-id, sequence, fragment metadata, priority queues. -
Offline shim for CI
Full in-process simulator to run tests without Steam. -
Incoming validation hook
IncomingValidatorlets consumers drop or accept messages before handler invocation. -
Poll-based receive loop
PollReceive()is required for Steam adapter; Offline shim is immediate.
API Refrences [Not All]
ModId
- FromGuid(string guid)
NetworkingServiceFactory
- CreateDefaultService() // auto-chosen by runtime
INetworkingService
> Service is created & initialized automatically by the library, mods should access it via `Net.Service` and must not call Initialize() except for test harnesses.
- bool IsHost()
- ulong GetLocalSteam64()
- ulong[] GetLobbyMemberSteamIds()
- Initialize()
- Shutdown()
- CreateLobby(maxPlayers)
- JoinLobby(lobbySteamId64)
- LeaveLobby()
- RegisterLobbyDataKey(string key)
- SetLobbyData(string key, object value)
- T GetLobbyData<T>(string key)
- RegisterPlayerDataKey(string key)
- SetPlayerData(string key, object value)
- T GetPlayerData<T>(ulong steam64, string key)
- IDisposable RegisterNetworkObject(object instance, uint modId, int mask = 0)
- void DeregisterNetworkObject(object instance, uint modId, int mask = 0)
- void RPC(uint modId, string methodName, ReliableType reliable, params object[] parameters)
- void RPCTarget(uint modId, string methodName, ulong targetSteamId64, ReliableType reliable, params object[] parameters)
- void RPCToHost(uint modId, string methodName, ReliableType reliable, params object[] parameters)
- void PollReceive()
- Events: LobbyCreated, LobbyEntered, LobbyLeft, PlayerEntered(ulong), PlayerLeft(ulong),
LobbyDataChanged(string[] keys), PlayerDataChanged(ulong steam64, string[] keys)
- Func<Message, ulong, bool>? IncomingValidator { get; set; }
Examples [Not All]
All examples assume the networking service is already present (Steam or Offline). They show the smallest usable code for each feature.
[FULL EXAMPLE] PEAKTest by off_grid
using System;
using System.Linq;
using BepInEx;
using BepInEx.Logging;
using UnityEngine;
using NetworkingLibrary.Services;
using NetworkingLibrary.Modules;
using NetworkingLibrary;
using Steamworks;
namespace PEAKTest
{
[BepInDependency("off_grid.NetworkingLibrary")]
[BepInPlugin(MyPluginInfo.PLUGIN_GUID, MyPluginInfo.PLUGIN_NAME, MyPluginInfo.PLUGIN_VERSION)]
public class Plugin : BaseUnityPlugin
{
readonly static uint MOD_ID = ModId.FromGuid(MyPluginInfo.PLUGIN_GUID); // Use this when you do not want to compute bytes.
const string LOBBY_KEY_PACK_COUNT = "peak_test.pack_count";
const string PLAYER_KEY_STATUS = "peak_test.player_status";
static ManualLogSource Log => Instance.Logger;
public static Plugin Instance { get; private set; } = null!;
IDisposable? registrationToken;
internal static INetworkingService Service = Net.Service!;
void Awake()
{
Instance = this;
Log.LogInfo($"{MyPluginInfo.PLUGIN_NAME} Awake");
// Register keys
Service.RegisterLobbyDataKey(LOBBY_KEY_PACK_COUNT);
Service.RegisterPlayerDataKey(PLAYER_KEY_STATUS);
// Register RPC handlers by reflecting this instance's [CustomRPC] methods.
registrationToken = Service.RegisterNetworkObject(this, MOD_ID);
// Subscribe to a few events, none of these are required.
Service.LobbyEntered += OnLobbyEntered;
Service.LobbyCreated += () => Log.LogInfo("LobbyCreated event");
Service.PlayerEntered += id => Log.LogInfo($"PlayerEntered: {id}");
Service.PlayerLeft += id => Log.LogInfo($"PlayerLeft: {id}");
Service.LobbyDataChanged += keys => Log.LogInfo("LobbyDataChanged: " + string.Join(",", keys));
Service.PlayerDataChanged += (steam, keys) => Log.LogInfo($"PlayerDataChanged: {steam} -> {string.Join(',', keys)}");
// If already in a lobby at load time, run quick checks
if (Service.InLobby) OnLobbyEntered();
}
void OnDestroy()
{
Service.LobbyEntered -= OnLobbyEntered;
Service.LobbyCreated -= () => { }; //
registrationToken?.Dispose();
Log.LogInfo($"{MyPluginInfo.PLUGIN_NAME} destroyed");
}
// Called when we enter a lobby (host or client).
void OnLobbyEntered()
{
Log.LogInfo($"OnLobbyEntered: InLobby={Service.InLobby}, HostSteamId64={Service.HostSteamId64}");
// Host will announce a small package list via RPC to all clients.
// Non-host clients will request the package list from host (RPCToHost).
if (Service.IsHost)
{
Log.LogInfo("We are host. Announcing packages to clients.");
// pretend-loaded packages, fill with your actual data.
string[] loaded = new[] { "pivo1", "pivo2" };
// set a lobby value
Service.SetLobbyData(LOBBY_KEY_PACK_COUNT, loaded.Length);
// set player data (host status)
Service.SetPlayerData(PLAYER_KEY_STATUS, "host_ready");
// send package list as a single joined string
string payload = string.Join("|", loaded);
Service.RPC(MOD_ID, nameof(HandlePackagesRpc), ReliableType.Reliable, payload);
Log.LogInfo($"Host RPC broadcast sent with {loaded.Length} packages.");
}
else
{
Log.LogInfo("We are client. Requesting package list from host.");
// ask host to send packages to everyone (host will handle RequestPackagesRpc)
Service.RPCToHost(MOD_ID, nameof(RequestPackagesRpc), ReliableType.Reliable);
}
}
// Host broadcasts packages with this RPC. Clients receive here.
// Signature shows a single string parameter.
[CustomRPC]
void HandlePackagesRpc(string joined)
{
try
{
var list = string.IsNullOrEmpty(joined) ? Array.Empty<string>() : joined.Split('|');
Log.LogInfo($"HandlePackagesRpc: received {list.Length} packages: {string.Join(", ", list)}");
// Set player key to indicate we received packages, not required.
var svc = Net.Service;
svc?.SetPlayerData(PLAYER_KEY_STATUS, "packages_received");
}
catch (Exception ex)
{
Log.LogError($"HandlePackagesRpc exception: {ex}");
}
}
// Clients call this (RPCToHost) to request the host's package list.
// Host will respond by performing an RPC broadcast (see OnLobbyEntered path).
[CustomRPC]
void RequestPackagesRpc()
{
try
{
// Only the host should act on this. We do a host check.
if (!Service.IsHost)
{
Log.LogInfo("RequestPackagesRpc called on non-host.");
return;
}
Log.LogInfo("Host handling RequestPackagesRpc; responding with package list.");
// Example data
var loaded = new[] { "pivo1", "pivo2" };
string payload = string.Join("|", loaded);
Service.RPC(MOD_ID, nameof(HandlePackagesRpc), ReliableType.Reliable, payload);
}
catch (Exception ex)
{
Log.LogError($"RequestPackagesRpc exception: {ex}");
}
}
}
}
[LEGACY] 1) Minimal host -> clients: Lobby key (recommended for package lists)
Host sets one string key. Clients read it on change.
Host: broadcast package list
// host only
void BroadcastLoadedPackages(INetworkingService svc, IEnumerable<string> packages)
{
const string KEY = "PEAK_PACKAGES_V1";
svc.RegisterLobbyDataKey(KEY); // safe to call on all peers
// serialize: escape '|' then join and compress+base64 option
string joined = string.Join("|", packages.Select(p => p.Replace("|","||")));
string payload = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(joined));
svc.SetLobbyData(KEY, payload); // host-only action
}
Client: receive update
void SubscribeToPackageUpdates(INetworkingService svc)
{
const string KEY = "PEAK_PACKAGES_V1";
svc.RegisterLobbyDataKey(KEY); // register the key used by host
svc.LobbyDataChanged += keys =>
{
if (!keys.Contains(KEY)) return;
var payload = svc.GetLobbyData<string>(KEY);
if (string.IsNullOrEmpty(payload)) return;
var joined = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(payload));
var packages = joined.Length == 0 ? Array.Empty<string>() :
joined.Split('|').Select(s => s.Replace("||","|")).ToArray();
// now packages[] contains the host list
};
}
Notes:
- Lobby metadata is small and best for modest lists (tens of items).
- If payload grows large, compress it (gzip) before base64.
2) Minimal RPC broadcast (general notifications)
Use RPC when you need immediate notification or structured params.
Register and broadcast
const uint MOD = 0xDEADBEEF;
// on plugin init
IDisposable token = svc.RegisterNetworkObject(this, MOD);
// broadcast
svc.RPC(MOD, "NotifyPackages", ReliableType.Reliable, "GAME_PACKAGES_READY");
In same class: RPC handler
[CustomRPC]
void NotifyPackages(string tag)
{
// runs on all peers (including host by loopback)
Logger.LogInfo($"NotifyPackages received: {tag}");
}
Notes:
- [LEGACY] Must call
PollReceive()each frame for Steam adapter. RegisterNetworkObjectdiscovers methods tagged[CustomRPC].
3) Targeted RPC to specific player
svc.RPCTarget(MOD, "PrivateMessage", targetSteamId64, ReliableType.Reliable, "hello");
Handler:
[CustomRPC]
void PrivateMessage(string text) { Debug.Log(text); }
4) IncomingValidator usage (drop unwanted messages)
svc.IncomingValidator = (msg, fromSteam64) =>
{
// drop messages with a specific method name
if (msg.MethodName == "DropMe") return false;
return true;
};
5) Offline shim for local tests
// Use this for unit tests or local dev when Steam not available.
INetworkingService svc = new OfflineNetworkingService();
svc.Initialize();
// Offline shim delivers messages immediately. PollReceive() is a no-op.
6) ModId.FromGuid & mapping
> Use uint MOD = ModId.FromGuid("<your-mod-guid>"); it produces a stable 32-bit id from your mod GUID so you do not hand-pick hex values.
7) Getting local & lobby Steam IDs + Photon mapping guidance
var svc = Net.Service;
ulong local = svc.GetLocalSteam64();
ulong[] members = svc.GetLobbyMemberSteamIds(); // empty when not in a lobby
If you use Photon and need actor -> steam mappings, then you can use svc.GetLobbyMemberSteamIds().
Efficiency guidance
-
Lobby data: cheap for small text. Keep per-key payload < ~2–4 KB.
-
RPC: use for immediate messages and structured params. Use Unreliable for high-rate telemetry.
String keys are readable in code but cost bytes on the wire. The library maps strings to a stable 32-bit hash locally and sends the 32-bit value, the full string is sent only when the peer does not have the mapping. If your payloads are large, compress or use chunked RPCs.
-
Compress large payloads (gzip) before base64. Or use chunked RPC transfer.
-
HMAC/signing is optional. Enable when you need tamper detection.
Security / Signing / SharedSecret
SetSharedSecret,RegisterModPublicKey,RegisterModSignerare privileged/global. Misuse affects all mods.
Troubleshooting & tips
-
[LEGACY] Always call
PollReceive()inUpdate()when using Steam adapter. -
Register lobby/player keys before calling
Get/Setto avoid warnings. -
Use
RegisterNetworkObjectand keep the returnedIDisposablefor safe deregistration. -
For very large lists prefer chunked RPC or a request-on-join RPC rather than putting everything in lobby metadata.
-
If the library or poller stops, check BepInEx logs for an uncaught exception in NetworkingPoller or signer delegate. The library will now disable a failing signer delegate and log a warning.
-
If you see duplicate / colliding RPCs, verify you used ModId.FromGuid() and that no two mods share the same GUID.
-
If RPC handlers never run for a disposed object, ensure you keep the returned IDisposable registration token and Dispose() it in OnDestroy, the library also prunes destroyed Unity objects periodically.

