Skip to content

Peer-to-peer network connection library for your next R.E.P.O. clone.

Notifications You must be signed in to change notification settings

nikitaclicks/peersack

Repository files navigation

PeersAck

A lightweight, transport-agnostic networking library for C# games with support for:

  • LAN/Local connections via LiteNetLib
  • Steam P2P via Facepunch.Steamworks
  • Room/Lobby abstraction for multiplayer game sessions
  • Fluent builder API for easy setup
  • Logging integration (Microsoft.Extensions.Logging)
  • Connection statistics (latency, bytes sent/received)

Requirements

  • .NET 8.0+ (supports net8.0, net9.0, net10.0)
  • For Steam support: Steam client must be running
  • For Godot: Godot 4.3+ with .NET support

Installation

NuGet:

# Core library
dotnet add package PeersAck

# For Godot 4 integration
dotnet add package PeersAck.Godot

From source:

dotnet restore
dotnet build

Godot 4 Integration

PeersAck provides first-class Godot 4 support with Node-based wrappers and signals.

Quick Start (Godot C#)

using Godot;
using PeersAck.Godot;

public partial class Game : Node
{
    [Export] public NetworkManagerNode Network { get; set; }
    
    public override void _Ready()
    {
        Network.PeerConnected += OnPeerConnected;
        Network.DataReceived += OnDataReceived;
        Network.StartServer(7777);
    }
    
    private void OnPeerConnected(long connectionId)
    {
        GD.Print($"Peer connected: {connectionId}");
    }
    
    private void OnDataReceived(long connectionId, byte[] data)
    {
        GD.Print($"Received {data.Length} bytes");
    }
}

Using from GDScript

extends Node

@onready var network = $NetworkManagerNode

func _ready():
    network.peer_connected.connect(_on_peer_connected)
    network.data_received.connect(_on_data_received)
    network.start_server(7777)

func _on_peer_connected(connection_id: int):
    print("Peer connected: ", connection_id)

func _on_data_received(connection_id: int, data: PackedByteArray):
    print("Received ", data.size(), " bytes")

Available Nodes

  • NetworkManagerNode - Low-level transport wrapper with auto-polling
  • RoomManagerNode - High-level room/lobby system wrapper

See PeersAck.Godot/README.md for full documentation.

Quick Start

Using the Builder (Recommended)

using PeersAck;
using PeersAck.Rooms;
using Microsoft.Extensions.Logging;

// Server with fluent configuration
var (transport, roomServer) = new PeersAckBuilder()
    .UseLanTransport()
    .WithLogging(loggerFactory)      // Optional: add logging
    .WithConnectionKey("MyGame_v1")
    .WithMaxConnections(16)
    .WithMaxPlayersPerRoom(4)
    .BuildServer();

await transport.StartServerAsync(7777);

// Run game loop
await transport.RunLoopAsync(tickRate: 60, onTick: () =>
{
    // Your game logic here
});
// Client with fluent configuration
var (transport, roomClient) = new PeersAckBuilder()
    .UseLanTransport()
    .WithConnectionKey("MyGame_v1")
    .WithOperationTimeout(TimeSpan.FromSeconds(10))
    .BuildClient();

await roomClient.ConnectAsync("127.0.0.1", 7777);
await roomClient.JoinRoomAsync("game-room");

LAN Server (Manual Setup)

using PeersAck.Transport;
using PeersAck.Rooms;

await using var transport = new LanTransport();
using var roomServer = new RoomServer(transport);

roomServer.OnPlayerJoinedRoom += (roomId, playerId) =>
    Console.WriteLine($"Player {playerId} joined {roomId}");

await transport.StartServerAsync(7777);

while (true)
{
    transport.Poll();
    await Task.Delay(16);
}

LAN Client (Manual Setup)

await using var transport = new LanTransport();
using var roomClient = new RoomClient(transport);

await roomClient.ConnectAsync("127.0.0.1", 7777);
await roomClient.JoinRoomAsync("game-room");

roomClient.SendToRoom("game-room", myDataBytes, DeliveryMode.Reliable);

while (roomClient.IsConnected)
{
    transport.Poll();
    await Task.Delay(16);
}

Steam Host

using PeersAck.Steam;
using Steamworks;

SteamClient.Init(480); // Your App ID

// Create lobby for matchmaking
using var lobbyManager = new SteamLobbyManager();
var lobby = await lobbyManager.CreateLobbyAsync(maxPlayers: 4);
lobbyManager.SetLobbyData("game", "MyGame");

// Create P2P server
var (transport, roomServer) = new PeersAckBuilder()
    .UseSteamTransport()
    .BuildServer();

await transport.StartServerAsync();

while (true)
{
    transport.Poll();
    await Task.Delay(16);
}

SteamClient.Shutdown();

Steam Client

SteamClient.Init(480);

using var lobbyManager = new SteamLobbyManager();

var lobbies = await lobbyManager.FindLobbiesAsync(
    new Dictionary<string, string> { ["game"] = "MyGame" });
await lobbyManager.JoinLobbyAsync(lobbies[0]);

var hostId = lobbyManager.GetHostSteamId()!.Value;

var (transport, roomClient) = new PeersAckBuilder()
    .UseSteamTransport()
    .BuildClient();

await ((SteamTransport)transport).ConnectAsync(hostId);
await roomClient.JoinRoomAsync("main");

SteamClient.Shutdown();

Logging

PeersAck integrates with Microsoft.Extensions.Logging:

using Microsoft.Extensions.Logging;

// Create a logger factory (e.g., from DI container or manually)
using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder.AddConsole();
    builder.SetMinimumLevel(LogLevel.Debug);
});

// Pass to builder
var (transport, server) = new PeersAckBuilder()
    .UseLanTransport()
    .WithLogging(loggerFactory)
    .BuildServer();

Or pass a logger directly:

var logger = loggerFactory.CreateLogger<LanTransport>();
var transport = new LanTransport(logger);

Connection Statistics

Monitor connection health and bandwidth:

var transport = new LanTransport();

// Get stats for a specific connection
var stats = transport.GetConnectionStats(connectionId);
if (stats != null)
{
    Console.WriteLine($"Latency: {stats.Latency}ms");
    Console.WriteLine($"Bytes sent: {stats.BytesSent}");
    Console.WriteLine($"Bytes received: {stats.BytesReceived}");
    Console.WriteLine($"Packets sent: {stats.PacketsSent}");
    Console.WriteLine($"Connected for: {stats.ConnectionDuration}");
}

// Get aggregate transport stats
var transportStats = transport.Stats;
Console.WriteLine($"Active connections: {transportStats.ActiveConnections}");
Console.WriteLine($"Total bytes sent: {transportStats.TotalBytesSent}");

Architecture

┌─────────────────────────────────────────────────────┐
│                    Your Game                        │
├─────────────────────────────────────────────────────┤
│                 PeersAckBuilder                     │
│            (Fluent configuration)                   │
├─────────────────────────────────────────────────────┤
│              RoomServer / RoomClient                │
│         (Room management, player tracking)          │
├─────────────────────────────────────────────────────┤
│                INetworkTransport                    │
├──────────────────────┬──────────────────────────────┤
│    LanTransport      │       SteamTransport         │
│    (LiteNetLib)      │  (Facepunch.Steamworks)      │
└──────────────────────┴──────────────────────────────┘

Key Types

Transport Layer

Type Description
INetworkTransport Core transport interface
LanTransport LAN/local network transport
SteamTransport Steam P2P transport
ConnectionId Unique connection identifier
DeliveryMode Reliable or Unreliable
IConnectionStats Connection statistics interface
TransportStats Aggregate transport statistics

Room Layer

Type Description
RoomServer Server-side room management
RoomClient Client-side room handling
RoomId Room identifier (string-based)
PlayerId Player identifier

Builder & Utilities

Type Description
PeersAckBuilder Fluent builder for configuration
TransportType Enum: Lan or Steam

Steam Extras

Type Description
SteamLobbyManager Steam lobby create/join/search

Builder Options

new PeersAckBuilder()
    // Transport selection
    .UseLanTransport()              // Use LiteNetLib (default)
    .UseSteamTransport()            // Use Steam P2P
    
    // Logging
    .WithLogging(loggerFactory)     // Add logging support
    
    // Connection settings
    .WithConnectionKey("key")       // LAN connection key
    .WithMaxConnections(32)         // Max server connections
    .WithDisconnectTimeout(5000)    // Timeout in ms
    .WithDisconnectTimeout(TimeSpan.FromSeconds(5))
    
    // Room settings
    .WithAutoCreateRooms(true)      // Auto-create rooms on join
    .WithMaxPlayersPerRoom(8)       // Max players per room
    
    // Client settings
    .WithOperationTimeout(10000)    // Async operation timeout
    
    // Build
    .BuildTransport()               // Just the transport
    .BuildServer()                  // (transport, roomServer)
    .BuildClient()                  // (transport, roomClient)

Delivery Modes

  • Reliable: Guaranteed delivery, ordered. Use for important events (chat, state changes).
  • Unreliable: Fast, no guarantees. Use for frequently-updated data (positions, inputs).

How to Extend

Adding a New Transport

  1. Implement INetworkTransport
  2. Map your transport's connection IDs to ConnectionId
  3. Raise events: OnPeerConnected, OnPeerDisconnected, OnDataReceived
public class MyTransport : INetworkTransport
{
    // Implement all interface members...
}

Adding Room Features

Extend RoomServer/RoomClient or create wrapper classes:

public class GameRoomServer : RoomServer
{
    public Dictionary<RoomId, GameState> RoomStates { get; } = new();
    
    // Add game-specific room logic...
}

Custom Room Messages

Extend RoomProtocol with new message types:

public static class GameProtocol
{
    public const byte SpawnPlayer = 0x20;
    public const byte UpdatePosition = 0x21;
    
    public static byte[] CreateSpawnPlayer(PlayerId id, Vector3 position) { ... }
}

Thread Safety

The library is designed to be called from a single thread (your game loop). Call Poll() regularly to process network events.

Running Tests

dotnet test PeersAck.Tests/PeersAck.Tests.csproj

License

MIT

About

Peer-to-peer network connection library for your next R.E.P.O. clone.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages