PEAK

Details

Last Updated
2 days ago
First Uploaded
2 days ago
Downloads
1.3K
Likes
2
Size
122KB
Dependency string
DAa-PEAKNetworkingLibrary-1.0.4
Dependants

Discord

PEAKNetworkingLibrary v1.0.4: Patches!

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


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

Thunderstore development is made possible with ads. Please consider making an exception to your adblock.
Thunderstore development is made possible with ads. Please consider making an exception to your adblock.
Thunderstore development is made possible with ads. Please consider making an exception to your adblock.