| Date uploaded | 8 months ago |
| Version | 1.0.1 |
| Download link | DAa-PEAKNetworkingLibrary-1.0.1.zip |
| Downloads | 76 |
| Dependency string | DAa-PEAKNetworkingLibrary-1.0.1 |
This mod requires the following mods to function
BepInEx-BepInExPack_PEAK
BepInEx pack for PEAK. Preconfigured and ready to use.
Preferred version: 5.4.75301README
PEAKNetworkingLibrary v1.0.1: Quality of Life Update! 
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().
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
- Automatic service/poller creation (no per-mod init).
- ModId.FromGuid(string) helper to derive numeric IDs from GUIDs.
- On-wire optimization: string keys mapped locally -> 32-bit key sent over the wire; collision detection and fallback to full-string metadata.
- Simplified examples for common tasks.
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) and ModId.FromString(string) (document whichever you implemented).
- 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
- static uint FromGuid(string guid)
NetworkingServiceFactory
- static INetworkingService 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.
- 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] Minimal plugin (host -> clients, uses lobby data + RPC)
using BepInEx;
using BepInEx.Logging;
using NetworkingLibrary.Services;
using NetworkingLibrary;
[BepInDependency("off_grid.NetworkingLibrary")]
[BepInPlugin("com.example.peaktest", "PEAKTest", "1.0.0")]
public class Plugin : BaseUnityPlugin
{
static readonly uint MOD = ModId.FromGuid("com.example.peaktest");
const string LOBBY_KEY_PACK_COUNT = "peak_test.pack_count";
static ManualLogSource Log => Instance.Logger!;
public static Plugin Instance { get; private set; } = null!;
IDisposable? registrationToken;
void Awake()
{
Instance = this;
Log.LogInfo("PEAKTest Awake");
var svc = Net.Service;
if (svc == null)
{
Log.LogError("Networking service not available.");
return;
}
svc.RegisterLobbyDataKey(LOBBY_KEY_PACK_COUNT);
registrationToken = svc.RegisterNetworkObject(this, MOD);
svc.LobbyEntered += OnLobbyEntered;
if (svc.InLobby) OnLobbyEntered();
}
void OnDestroy()
{
registrationToken?.Dispose();
}
void OnLobbyEntered()
{
var svc = Net.Service;
if (svc == null) return;
bool amHost = svc.HostSteamId64 != 0 && svc.HostSteamId64 == GetLocalSteam64();
if (amHost)
{
var packages = new[] { "pivo1", "pivo2" };
svc.SetLobbyData(LOBBY_KEY_PACK_COUNT, packages.Length);
string payload = string.Join("|", packages);
svc.RPC(MOD, nameof(HandlePackagesRpc), ReliableType.Reliable, payload);
}
else
{
svc.RPCToHost(MOD, nameof(RequestPackagesRpc), ReliableType.Reliable);
}
}
[CustomRPC]
void HandlePackagesRpc(string joined)
{
var list = string.IsNullOrEmpty(joined) ? Array.Empty<string>() : joined.Split('|');
Log.LogInfo($"Got {list.Length} packages: {string.Join(',', list)}");
}
[CustomRPC]
void RequestPackagesRpc()
{
var svc = Net.Service;
if (svc == null) return;
if (svc.HostSteamId64 != GetLocalSteam64()) return; // only host handles
var packages = new[] { "pivo1", "pivo2" };
string payload = string.Join("|", packages);
svc.RPC(MOD, nameof(HandlePackagesRpc), ReliableType.Reliable, payload);
}
static ulong GetLocalSteam64()
{
try { return Steamworks.SteamUser.GetSteamID().m_SteamID; } catch { return 0UL; }
}
}
[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;
void Awake()
{
Instance = this;
Log.LogInfo($"{MyPluginInfo.PLUGIN_NAME} Awake");
var svc = Net.Service;
if (svc == null)
{
Log.LogError("Networking service not found. Ensure PEAKNetworkingLibrary is installed and loaded.");
return;
}
// Register keys
svc.RegisterLobbyDataKey(LOBBY_KEY_PACK_COUNT);
svc.RegisterPlayerDataKey(PLAYER_KEY_STATUS);
// Register RPC handlers by reflecting this instance's [CustomRPC] methods.
registrationToken = svc.RegisterNetworkObject(this, MOD_ID);
// Subscribe to a few events, none of these are required.
svc.LobbyEntered += OnLobbyEntered;
svc.LobbyCreated += () => Log.LogInfo("LobbyCreated event");
svc.PlayerEntered += id => Log.LogInfo($"PlayerEntered: {id}");
svc.PlayerLeft += id => Log.LogInfo($"PlayerLeft: {id}");
svc.LobbyDataChanged += keys => Log.LogInfo("LobbyDataChanged: " + string.Join(",", keys));
svc.PlayerDataChanged += (steam, keys) => Log.LogInfo($"PlayerDataChanged: {steam} -> {string.Join(',', keys)}");
// If already in a lobby at load time, run quick checks
if (svc.InLobby) OnLobbyEntered();
}
void OnDestroy()
{
var svc = Net.Service;
if (svc != null)
{
svc.LobbyEntered -= OnLobbyEntered;
svc.LobbyCreated -= () => { }; //
}
registrationToken?.Dispose();
Log.LogInfo($"{MyPluginInfo.PLUGIN_NAME} destroyed");
}
// Called when we enter a lobby (host or client).
void OnLobbyEntered()
{
var svc = Net.Service;
if (svc == null) return;
Log.LogInfo($"OnLobbyEntered: InLobby={svc.InLobby}, HostSteamId64={svc.HostSteamId64}");
// Host will announce a small package list via RPC to all clients.
// Non-host clients will request the package list from host (RPCToHost).
ulong localSteam64 = GetLocalSteam64();
bool amHost = localSteam64 != 0 && svc.HostSteamId64 == localSteam64;
if (amHost)
{
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
svc.SetLobbyData(LOBBY_KEY_PACK_COUNT, loaded.Length);
// set player data (host status)
svc.SetPlayerData(PLAYER_KEY_STATUS, "host_ready");
// send package list as a single joined string
string payload = string.Join("|", loaded);
svc.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)
svc.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
{
var svc = Net.Service;
if (svc == null) return;
// Only the host should act on this. We do a host check.
ulong local = GetLocalSteam64();
if (local == 0 || svc.HostSteamId64 != local)
{
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);
svc.RPC(MOD_ID, nameof(HandlePackagesRpc), ReliableType.Reliable, payload);
}
catch (Exception ex)
{
Log.LogError($"RequestPackagesRpc exception: {ex}");
}
}
//
static ulong GetLocalSteam64()
{
try
{
return SteamUser.GetSteamID().m_SteamID;
}
catch
{
return 0UL;
}
}
}
}
[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:
- 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.
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.
More details & Help
⠀ (!) If you encounter any issues with the mod, join the discord and message here.
CHANGELOG
Changelog
The format is based on Keep a Changelog.
1.0.7 23/11/2025
Added
- Table value support for messages.
Changed
- null
Fixed
- Patched issues with RPC events.
1.0.6 23/11/2025
Added
- null.
Changed
- null
Fixed
- I forgot to change reference link to the actual one.
1.0.5 22/11/2025
Added
- null.
Changed
- Simplified RPC structure for sending back messages to the caller / fetching the caller.
Fixed
- Made photon user to steam user mapping usable.
v1.0.4
- Patched something.
- Implemented IsHost bool.
v1.0.3
- Implemented helper functions to simplify usage of Library.
v1.0.2
- Fixed the README
v1.0.1
- Made it mote consumer friendly.
v1.0.0
- Release

