DAa-PEAKNetworkingLibrary icon

PEAKNetworkingLibrary

Network library for PEAK using Steam instead of PUN.

By DAa
Last updated 2 weeks ago
Total downloads 8916
Total rating 3 
Categories Mods Libraries Misc Client Side Host Only All Clients
Dependency string DAa-PEAKNetworkingLibrary-1.0.7
Dependants 3 other packages depend on this package

This mod requires the following mods to function

BepInEx-BepInExPack_PEAK-5.4.75301 icon
BepInEx-BepInExPack_PEAK

BepInEx pack for PEAK. Preconfigured and ready to use.

Preferred version: 5.4.75301

README

Discord

PEAKNetworkingLibrary -> Update Thunderstore VersionNuGet Version

logo

Discord GitHub X YouTube
Buy Me A Coffee Donate

  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

 


Update

Contains information related to the current update.

Details → (click to expand)

 

  • Implemented support for tables in the messages.
  • Patched issues with RPC not being sent or received.

 


FAQ

Contains commonly asked questions.

FAQ → (click to expand)

 

  • 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.

 


Features

Contains information related to features that do function.

Feature list [DOES NOT INCLUDE EVERYTHING] → (click to expand)

 

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
    INetworkingService provides one surface mods use. Swap Steam vs Offline without code changes.
  • Lobby key sync (host -> clients)
    Host sets small authoritative strings via SetLobbyData. Clients read with GetLobbyData and LobbyDataChanged event.
  • RPC discovery & invocation
    Discover methods marked [CustomRPC] with RegisterNetworkObject and call RPC, RPCTarget, or RPCToHost.
  • Message serialization
    Message class supports: byte, int, uint, long, ulong, float, bool, string, byte[], Vector3, Quaternion, CSteamID.
  • Reliable vs Unreliable
    ReliableType enum 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
    IncomingValidator lets consumers drop or accept messages before handler invocation.
  • Poll-based receive loop
    PollReceive() is required for Steam adapter; Offline shim is immediate.
API References [DOES NOT INCLUDE EVERYTHING] → (click to expand)
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; }

 


Examples

Contains examples of features.

Examples [DOES NOT INCLUDE EVERYTHING] → (click to expand)

 

  • 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.
  • RegisterNetworkObject discovers 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().

 



 


Install

Contains information related to installing the mod.

Install instructions → (click to expand)

  Using Mod Manager

Mod Manager
  1. Install from Thunderstore or add via your mod manager.
Manual install
  1. Copy the release files into the game directory.
  • See Usage: for information related to configuration.

 


Security

Contains information related to security.

Security → (click to expand)

 

  • SetSharedSecret, RegisterModPublicKey, RegisterModSigner are privileged/global. Misuse affects all mods.

 


Efficiency Guidance

Contains information related to best practices.

Efficiency Guide → (click to expand)

 

  • 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.

 


Support

Contains information about where to find support.

Get help → (click to expand)

 

 


 

Contains information related to debugging issues.

Troubleshooting → (click to expand)

 

  • [LEGACY] Always call PollReceive() in Update() when using Steam adapter.
  • Register lobby/player keys before calling Get/Set to avoid warnings.
  • Use RegisterNetworkObject and keep the returned IDisposable for 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.

 



Discord