

A library mod for networking specifically with Steam with other platform compatibility.
PEAKNetworkingLibrary has its own NuGet package, you can find it here.
Update Info • FAQ • Features • Examples • Install • Security • Efficiency Guidance • Support

Contains information related to the current update.
Contains commonly asked questions.
Contains information related to features that do function.
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.
INetworkingService provides one surface mods use. Swap Steam vs Offline without code changes.SetLobbyData. Clients read with GetLobbyData and LobbyDataChanged event.[CustomRPC] with RegisterNetworkObject and call RPC, RPCTarget, or RPCToHost.Message class supports: byte, int, uint, long, ulong, float, bool, string, byte[], Vector3, Quaternion, CSteamID.ReliableType enum supports Reliable, Unreliable, UnreliableNoDelay semantics.IncomingValidator lets consumers drop or accept messages before handler invocation.PollReceive() is required for Steam adapter; Offline shim is immediate.| Key | Type | Meaning |
|---|---|---|
| NetworkingPhotonExtensions | Method | Dictionary<int, ulong> MapPhotonActorsToSteam |
| ModId | Method | FromGuid(string guid) |
| NetworkingServiceFactory | Method | CreateDefaultService() – auto-chosen by runtime |
| INetworkingService | Info | Service is created & initialized automatically by the library. Mods should access via Net.Service and must not call Initialize() except for test harnesses. |
| Method | bool IsHost() |
|
| Method | ulong GetLocalSteam64() |
|
| Method | ulong[] GetLobbyMemberSteamIds() |
|
| Method | Initialize() |
|
| Method | Shutdown() |
|
| Method | CreateLobby(maxPlayers) |
|
| Method | JoinLobby(lobbySteamId64) |
|
| Method | LeaveLobby() |
|
| Method | RegisterLobbyDataKey(string key) |
|
| Method | SetLobbyData(string key, object value) |
|
| Method | T GetLobbyData<T>(string key) |
|
| Method | RegisterPlayerDataKey(string key) |
|
| Method | SetPlayerData(string key, object value) |
|
| Method | T GetPlayerData<T>(ulong steam64, string key) |
|
| Method | IDisposable RegisterNetworkObject(object instance, uint modId, int mask = 0) |
|
| Method | void DeregisterNetworkObject(object instance, uint modId, int mask = 0) |
|
| Method | void RPC(uint modId, string methodName, ReliableType reliable, params object[] parameters) |
|
| Method | void RPCTarget(uint modId, string methodName, ulong targetSteamId64, ReliableType reliable, params object[] parameters) |
|
| Method | void RPCToHost(uint modId, string methodName, ReliableType reliable, params object[] parameters) |
|
| Method | void PollReceive() |
|
| Events | LobbyCreated, LobbyEntered, LobbyLeft, PlayerEntered(ulong), PlayerLeft(ulong), LobbyDataChanged(string[] keys), PlayerDataChanged(ulong steam64, string[] keys) |
|
| Property | Func<Message, ulong, bool>? IncomingValidator { get; set; } |
Contains examples of features.
- All examples assume the networking service is already present (Steam or Offline). They show the smallest usable code for each feature.
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}");
}
}
}
}
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:
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:
PollReceive() each frame for Steam adapter.RegisterNetworkObject discovers methods tagged [CustomRPC].svc.RPCTarget(MOD, "PrivateMessage", targetSteamId64, ReliableType.Reliable, "hello");
Handler:
[CustomRPC]
void PrivateMessage(string text) { Debug.Log(text); }
svc.IncomingValidator = (msg, fromSteam64) =>
{
// drop messages with a specific method name
if (msg.MethodName == "DropMe") return false;
return true;
};
// 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.
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.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().
Contains information related to installing the mod.
Mod Manager
Manual install
- See Usage: for information related to configuration.
Contains information related to security.
SetSharedSecret, RegisterModPublicKey, RegisterModSigner are privileged/global. Misuse affects all mods.
Contains information related to best practices.
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.
Contains information about where to find support.
BepInEx logs.
Contains information related to debugging issues.
PollReceive() in Update() when using Steam adapter.Get/Set to avoid warnings.RegisterNetworkObject and keep the returned IDisposable for safe deregistration.