You are viewing a potentially older version of this package. View all versions.
DAa-PEAKNetworkingLibrary-1.0.1 icon

PEAKNetworkingLibrary

Network library for PEAK using Steam instead of PUN.

By DAa
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-5.4.75301 icon
BepInEx-BepInExPack_PEAK

BepInEx pack for PEAK. Preconfigured and ready to use.

Preferred version: 5.4.75301

README


Discord

PEAKNetworkingLibrary v1.0.1: Quality of Life Update!

Latest Version Total Downloads

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


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, RegisterModSigner are privileged/global. Misuse affects all mods.

Troubleshooting & tips

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


More details & Help

⠀ (!) If you encounter any issues with the mod, join the discord and message here.


Discord

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