Skip to content

Instantly share code, notes, and snippets.

@Pieeer1
Created January 6, 2024 01:28
Show Gist options
  • Save Pieeer1/559c1de3a8c7110249a0f45b630e8b43 to your computer and use it in GitHub Desktop.
Save Pieeer1/559c1de3a8c7110249a0f45b630e8b43 to your computer and use it in GitHub Desktop.
Steam Multiplayer Peer with Facepunch Steamworks (C# API)

Steam Multiplayer Peer With Facepunch Steamworks 0.1

Introduction

  • This is a very simple rudimentary and inneficient way to do some of these things. This is just a base functioning version. It works.
  • It works with RPC's, Multiplayer Spawners, you can pull the MultiplayerApi.PeerConnected and disconnected events, it just works
  • Does it throw errors sometimes? Unsure still in development. But it functions.
  • Joining Lobbies through the steam ui is not implemented but the setup is really trivial (sub to one of those events)

Setup.

  • Take this Code. Put it in your project.
  • Get a Steam Id. Don't want to spend $100? use 480
  • Download a couple DLL's from https://github.com/finepointcgi/Facepunch.Steamworks/releases/tag/1.0 (you specifically want the following): steam_api.dll steam_api64.dll Facepunch.Steamworks.Win64.dll Facepunch.Steamworks.Posix.dll (pretty sure this is the mac dll) the linux dll from one of the zips.
  • Add the references to your csproj: Example: <ItemGroup> <None Include="DLL\Facepunch.Steamworks.Win64.dll" /> <None Include="steam_api.dll"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> <None Include="steam_api64.dll"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </None> </ItemGroup> <ItemGroup> <Reference Include="Facepunch.Steamworks.Win64"> <HintPath>./DLL/Facepunch.Steamworks.Win64.dll</HintPath> </Reference> </ItemGroup>

Feedback

  • Let me know if something doesn't work, it should be pretty plug and play!
  • This is the intial functioning version so if there are some errors or issues let me know there are probably going to be some updates made to make it faster/better.

Credit

Lobby.Join(); // you are going to have to pull the lobby from somewhere. Run the callback for getting the lobby list somewhere
// holy shit terrible implementation eventually fix this
//this is due to the fact that a second connection does not have access to the lobby owner id until it is joined.
//fucking why???
Timer timer = new Timer();
AddChild(timer);
timer.OneShot = true;
timer.WaitTime = 1.0d;
timer.Timeout += () =>
{
SteamMultiplayerPeer peer = new SteamMultiplayerPeer();
peer.CreateClient(this.SteamManager().PlayerSteamID, Lobby.Owner.Id);
Multiplayer.MultiplayerPeer = peer;
//this.JoinGame().IsOpen = false; // closing ui
//this.Startup().StartGame(); // again arbitrary
};
timer.Start();
using Godot;
using Steamworks;
using Steamworks.Data;
using System;
using System.Collections.Generic;
public partial class SteamConnection : RefCounted
{
public bool IsActive { get; private set; }
public ulong SteamId { get; set; }
public Connection Connection { get; set; }
public long TickCountSinceLastData { get; private set; }
public int PeerId { get; set; } = -1;
public DateTime LastMessageTimeStamp { get; private set; }
public List<SteamPacketPeer> PendingRetryPackets { get; private set; } = new List<SteamPacketPeer>();
public struct SetupPeerPayload
{
public int PeerId = -1;
public SetupPeerPayload()
{
}
}
public Error Send(SteamPacketPeer packet)
{
AddPacket(packet);
return SendPending();
}
private void AddPacket(SteamPacketPeer packet)
{
PendingRetryPackets.Add(packet);
}
private Error SendPending()
{
while(PendingRetryPackets.Count > 0)
{
SteamPacketPeer packet = PendingRetryPackets[0];
Error errorCode = RawSend(packet);
PendingRetryPackets.RemoveAt(0);
if(errorCode != Error.Ok)
{
return errorCode;
}
}
return Error.Ok;
}
private Error RawSend(SteamPacketPeer packet)
{
return GetErrorFromResult(Connection.SendMessage(packet.Data, SendType.Reliable));
}
private Error GetErrorFromResult(Result result) => result switch
{
//TODO - IMPLEMENT OTHER ERROR MESSAGES
Result.OK => Error.Ok,
Result.Fail => Error.Failed,
Result.NoConnection => Error.ConnectionError,
Result.InvalidParam => Error.InvalidParameter,
_ => Error.Bug
};
public Error SendPeer(int uniqueId)
{
SetupPeerPayload payload = new SetupPeerPayload();
payload.PeerId = uniqueId;
return SendSetupPeer(payload);
}
private Error SendSetupPeer(SetupPeerPayload payload)
{
return Send(new SteamPacketPeer(payload.ToBytes(), MultiplayerPeer.TransferModeEnum.Reliable));
}
}
using Godot;
using Steamworks.Data;
using Steamworks;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Linq;
public class SteamConnectionManager : ConnectionManager
{
public event Action<ConnectionInfo>? OnConnectionEstablished;
public event Action<ConnectionInfo>? OnConnectionLost;
private List<SteamNetworkingMessage> _pendingMessages { get; } = new List<SteamNetworkingMessage>();
public override void OnConnectionChanged(ConnectionInfo info)
{
base.OnConnectionChanged(info);
}
public override void OnConnected(ConnectionInfo info)
{
base.OnConnected(info);
OnConnectionEstablished?.Invoke(info);
}
public override void OnConnecting(ConnectionInfo info)
{
base.OnConnecting(info);
}
public override void OnDisconnected(ConnectionInfo info)
{
base.OnDisconnected(info);
OnConnectionLost?.Invoke(info);
}
public override void OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel)
{
base.OnMessage(data, size, messageNum, recvTime, channel);
byte[] managedArray = new byte[size];
Marshal.Copy(data, managedArray, 0, size);
_pendingMessages.Add(new SteamNetworkingMessage(managedArray, ConnectionInfo.Identity.SteamId, MultiplayerPeer.TransferModeEnum.Reliable));
}
public IEnumerable<SteamNetworkingMessage> GetPendingMessages(int count)
{
IEnumerable<SteamNetworkingMessage> messagesToSend = _pendingMessages.Take(count).ToList();
for (int i = 0; i < count; i++)
{
if (_pendingMessages.Any())
{
_pendingMessages.RemoveAt(0);
}
}
return messagesToSend;
}
}
using Godot;
using Steamworks;
using Steamworks.Data;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public partial class SteamManager : Node
{
private const uint _steamAppId = /*YOUR STEAM ID HERE*/;
public event Action<Friend>? OnPlayerJoinLobby;
public event Action<Friend>? OnPlayerLeftLobby;
public event Action<List<Lobby>>? OnLobbyRefreshCompleted;
public string PlayerName => SteamClient.Name;
public SteamId PlayerSteamID => SteamClient.SteamId;
public bool IsHost { get; private set; }
private List<Lobby> _availableLobbies = new List<Lobby>();
private Lobby _hostedLobby;
public override void _Ready()
{
InstantiateSteam();
SteamMatchmaking.OnLobbyGameCreated += OnLobbyGameCreated;
SteamMatchmaking.OnLobbyCreated += OnLobbyCreated;
SteamMatchmaking.OnLobbyMemberJoined += OnLobbyMemberJoined;
SteamMatchmaking.OnLobbyMemberDisconnected += OnLobbyMemberDisconnected;
SteamMatchmaking.OnLobbyMemberLeave += OnLobbyMemberLeave;
SteamMatchmaking.OnLobbyEntered += OnLobbyEntered;
SteamFriends.OnGameLobbyJoinRequested += OnGameLobbyJoinRequested;
}
public override void _Process(double delta)
{
if (SteamClient.IsValid)
{
SteamClient.RunCallbacks();
}
}
public override void _ExitTree()
{
SteamClient.Shutdown();
}
private void OnLobbyMemberDisconnected(Lobby lobby, Friend friend)
{
OnPlayerLeftLobby?.Invoke(friend);
}
private void OnLobbyMemberLeave(Lobby lobby, Friend friend)
{
OnPlayerLeftLobby?.Invoke(friend);
}
private void OnLobbyMemberJoined(Lobby lobby, Friend friend)
{
OnPlayerJoinLobby?.Invoke(friend);
}
private void OnLobbyCreated(Result result, Lobby lobby)
{
SteamMultiplayerPeer peer = new SteamMultiplayerPeer();
peer.CreateHost(PlayerSteamID);
Multiplayer.MultiplayerPeer = peer;
//this.Startup().StartGame(); /*This is really just a callback to start your game. I call a global function that extends Node here.*/
}
private void OnLobbyGameCreated(Lobby lobby, uint id, ushort port, SteamId steamId)
{
}
public async Task CreateLobby()
{
Lobby? createLobbyOutput = await SteamMatchmaking.CreateLobbyAsync(20);
if (!createLobbyOutput.HasValue)
{
throw new Exception();
}
_hostedLobby = createLobbyOutput.Value;
_hostedLobby.SetPublic();
_hostedLobby.SetJoinable(true);
_hostedLobby.SetData("ownerNameDataString", PlayerName);
IsHost = true;
}
public async Task GetMultiplayerLobbies()
{
_availableLobbies = (await SteamMatchmaking.LobbyList.WithMaxResults(10).RequestAsync()).ToList();
OnLobbyRefreshCompleted?.Invoke(_availableLobbies);
}
private void OnLobbyEntered(Lobby lobby)
{
if (lobby.MemberCount > 0)
{
_hostedLobby = lobby;
foreach (var item in lobby.Members)
{
OnPlayerJoinLobby?.Invoke(item);
}
lobby.SetGameServer(lobby.Owner.Id);
}
}
private async void OnGameLobbyJoinRequested(Lobby lobby, SteamId id)
{
RoomEnter joinSuccessful = await lobby.Join();
if (joinSuccessful != RoomEnter.Success)
{
throw new InvalidOperationException("Failed to Join Lobby");
}
else
{
_hostedLobby = lobby;
foreach (var item in lobby.Members)
{
OnPlayerJoinLobby?.Invoke(item);
}
}
}
public void OpenFriendOverlayForInvite()
{
SteamFriends.OpenGameInviteOverlay(_hostedLobby.Id);
}
private void InstantiateSteam()
{
try
{
SteamClient.Init(_steamAppId, asyncCallbacks: false);
}
catch (Exception ex)
{
GD.PrintErr(ex.Message);
}
}
}
using Godot;
using Steamworks;
using Steamworks.Data;
using System;
using System.Collections.Generic;
using System.Linq;
public partial class SteamMultiplayerPeer : MultiplayerPeerExtension
{
public enum Mode
{
None,
Server,
Client
}
private const int _maxPacketSize = 524288; // abritrary value from steam. Fun.
private SteamConnectionManager? _steamConnectionManager;
private SteamSocketManager? _steamSocketManager;
private System.Collections.Generic.Dictionary<int, SteamConnection> _peerIdToConnection = new System.Collections.Generic.Dictionary<int, SteamConnection>();
private System.Collections.Generic.Dictionary<ulong, SteamConnection> _connectionsBySteamId = new System.Collections.Generic.Dictionary<ulong, SteamConnection>();
private int _targetPeer = -1;
private int _uniqueId = 0;
private Mode _mode;
private bool _isActive => _mode != Mode.None;
private ConnectionStatus _connectionStatus = ConnectionStatus.Disconnected;
private TransferModeEnum _transferMode = TransferModeEnum.Reliable;
private List<SteamPacketPeer> _incomingPackets = new List<SteamPacketPeer>();
private SteamPacketPeer? _nextReceivedPacket;
private SteamId _steamId;
private int _transferChannel = 0;
private bool _refuseNewConnections = false;
public Error CreateHost(SteamId playerId)
{
_steamId = playerId;
if (_isActive)
{
return Error.AlreadyInUse;
}
_steamSocketManager = SteamNetworkingSockets.CreateRelaySocket<SteamSocketManager>();
_steamConnectionManager = SteamNetworkingSockets.ConnectRelay<SteamConnectionManager>(playerId);
_steamSocketManager.OnConnectionEstablished += (c) =>
{
if (c.Item2.Identity.SteamId != _steamId)
{
AddConnection(c.Item2.Identity.SteamId, c.Item1);
}
};
_steamSocketManager.OnConnectionLost += (c) =>
{
if (c.Item2.Identity.SteamId != _steamId)
{
EmitSignal(SignalName.PeerDisconnected, _connectionsBySteamId[c.Item2.Identity.SteamId].PeerId);
_peerIdToConnection.Remove(_connectionsBySteamId[c.Item2.Identity.SteamId].PeerId);
_connectionsBySteamId.Remove(c.Item2.Identity.SteamId);
}
};
_uniqueId = 1;
_mode = Mode.Server;
_connectionStatus = ConnectionStatus.Connected;
return Error.Ok;
}
public Error CreateClient(SteamId playerId, SteamId hostId)
{
_steamId = playerId;
if (_isActive)
{
return Error.AlreadyInUse;
}
_uniqueId = (int)GenerateUniqueId();
_steamConnectionManager = SteamNetworkingSockets.ConnectRelay<SteamConnectionManager>(hostId);
_steamConnectionManager.OnConnectionEstablished += (connection) =>
{
if (connection.Identity.SteamId != _steamId)
{
AddConnection(connection.Identity.SteamId, _steamConnectionManager.Connection);
_connectionStatus = ConnectionStatus.Connected;
_connectionsBySteamId[connection.Identity.SteamId].SendPeer(_uniqueId);
}
};
_steamConnectionManager.OnConnectionLost += (connection) =>
{
if (connection.Identity.SteamId != _steamId)
{
EmitSignal(SignalName.PeerDisconnected, _connectionsBySteamId[connection.Identity.SteamId].PeerId);
_peerIdToConnection.Remove(_connectionsBySteamId[connection.Identity.SteamId].PeerId);
_connectionsBySteamId.Remove(connection.Identity.SteamId);
}
};
_mode = Mode.Client;
_connectionStatus = ConnectionStatus.Connecting;
return Error.Ok;
}
public override void _Close()
{
if(!_isActive || _connectionStatus != ConnectionStatus.Connected) { return; }
foreach (var connection in _steamSocketManager?.Connected ?? Enumerable.Empty<Connection>())
{
connection.Close();
}
if(_IsServer())
{
_steamConnectionManager?.Close();
}
_peerIdToConnection.Clear();
_connectionsBySteamId.Clear();
_mode = Mode.None;
_uniqueId = 0;
_connectionStatus = ConnectionStatus.Disconnected;
}
public override void _DisconnectPeer(int pPeer, bool pForce)
{
SteamConnection? connection = GetConnectionFromPeer(pPeer);
if(connection == null) { return; }
bool res = connection.Connection.Close();
if(!res)
{
return;
}
connection.Connection.Flush();
_connectionsBySteamId.Remove(connection.SteamId);
_peerIdToConnection.Remove(pPeer);
if(_mode == Mode.Client || _mode == Mode.Server)
{
GetConnectionFromPeer(0)?.Connection.Flush();
}
if(pForce && _mode == Mode.Client)
{
_connectionsBySteamId.Clear();
Close();
}
}
public override int _GetAvailablePacketCount()
{
return _incomingPackets.Count;
}
public override ConnectionStatus _GetConnectionStatus()
{
return _connectionStatus;
}
public override int _GetMaxPacketSize()
{
return _maxPacketSize;
}
public override int _GetPacketChannel()
{
return 0; // todo - implement more channels
}
public override TransferModeEnum _GetPacketMode()
{
return _incomingPackets.FirstOrDefault()?.TransferMode ?? TransferModeEnum.Reliable;
}
public override int _GetPacketPeer()
{
return _connectionsBySteamId[_incomingPackets.FirstOrDefault()?.SenderSteamId ?? 0].PeerId;
}
public override byte[] _GetPacketScript()
{
if (_incomingPackets.Any())
{
_nextReceivedPacket = _incomingPackets.First();
_incomingPackets.RemoveAt(0);
return _nextReceivedPacket.Data;
}
return System.Array.Empty<byte>();
}
public override int _GetTransferChannel()
{
return _transferChannel;
}
public override TransferModeEnum _GetTransferMode()
{
return _transferMode;
}
public override int _GetUniqueId()
{
return _uniqueId;
}
public override bool _IsRefusingNewConnections()
{
return _refuseNewConnections;
}
public override bool _IsServer()
{
return _uniqueId == 1;
}
public override bool _IsServerRelaySupported()
{
return _mode == Mode.Server || _mode == Mode.Client;
}
public override void _Poll()
{
if (_steamSocketManager != null)
{
_steamSocketManager.Receive();
}
if (_steamConnectionManager != null && _steamConnectionManager.Connected)
{
_steamConnectionManager.Receive();
}
foreach (SteamConnection connection in _connectionsBySteamId.Values)
{
IEnumerable<SteamNetworkingMessage> steamNetworkingMessages = (_steamConnectionManager?.GetPendingMessages(255) ??
Enumerable.Empty<SteamNetworkingMessage>()).Union(_steamSocketManager?.ReceiveMessagesOnConnection(connection.Connection, 255) ?? Enumerable.Empty<SteamNetworkingMessage>());
foreach (SteamNetworkingMessage message in steamNetworkingMessages)
{
if(GetPeerIdFromSteamId(message.Sender) != -1)
{
ProcessMesssage(message.Data, message.Sender);
}
else
{
SteamConnection.SetupPeerPayload? receive = message.Data.ToStruct<SteamConnection.SetupPeerPayload>();
ProcessPing(receive.Value, message.Sender);
}
}
}
}
private void ProcessPing(SteamConnection.SetupPeerPayload receive, ulong sender)
{
SteamConnection connection = _connectionsBySteamId[sender]; // potentially might have to change to check if it exists first, if not then set the steamid peer
if (receive.PeerId != -1)
{
if(connection.PeerId == -1)
{
SetSteamIdPeer(sender, receive.PeerId);
}
if (_IsServer())
{
connection.SendPeer(_uniqueId);
}
EmitSignal(SignalName.PeerConnected, receive.PeerId);
}
}
private void SetSteamIdPeer(ulong steamId, int peerId)
{
SteamConnection steamConnection = _connectionsBySteamId[steamId];
if(steamConnection.PeerId == -1)
{
steamConnection.PeerId = peerId;
_peerIdToConnection.Add(peerId, steamConnection);
}
}
private void ProcessMesssage(byte[] message, ulong senderId)
{
SteamPacketPeer packet = new SteamPacketPeer(message, TransferModeEnum.Reliable);
packet.SenderSteamId = senderId;
_incomingPackets.Add(packet);
}
public override Error _PutPacketScript(byte[] pBuffer)
{
if (!_isActive || _connectionStatus != ConnectionStatus.Connected) { return Error.Unconfigured; }
if (_targetPeer != 0 && !_peerIdToConnection.ContainsKey(Mathf.Abs(_targetPeer))) // CONTINUE HERE // https://github.com/expressobits/steam-multiplayer-peer/blob/main/steam-multiplayer-peer/steam_multiplayer_peer.cpp
{
return Error.InvalidParameter;
}
if (_mode == Mode.Client && !_peerIdToConnection.ContainsKey(1))
{
return Error.Bug;
}
SteamPacketPeer packet = new SteamPacketPeer(pBuffer, _transferMode);
if (_targetPeer == 0)
{
Error error = Error.Ok;
foreach (SteamConnection connection in _connectionsBySteamId.Values)
{
Error packetSendingError = connection.Send(packet);
if (packetSendingError != Error.Ok)
{
return packetSendingError;
}
}
return error;
}
else
{
return GetConnectionFromPeer(_targetPeer)?.Send(packet) ?? Error.Unavailable;
}
}
public override void _SetRefuseNewConnections(bool pEnable)
{
_refuseNewConnections = pEnable;
}
public override void _SetTargetPeer(int pPeer)
{
_targetPeer = pPeer;
}
public override void _SetTransferChannel(int pChannel)
{
_transferChannel = pChannel;
}
public override void _SetTransferMode(TransferModeEnum pMode)
{
_transferMode = pMode;
}
private SteamConnection? GetConnectionFromPeer(int peerId)
{
if(_peerIdToConnection.ContainsKey(peerId))
{
return _peerIdToConnection[peerId];
}
return null;
}
private int GetPeerIdFromSteamId(ulong steamId)
{
if (steamId == _steamId.Value)
{
return _uniqueId;
}
else if (_connectionsBySteamId.ContainsKey(steamId))
{
return _connectionsBySteamId[steamId].PeerId;
}
else
{
return -1;
}
}
private void AddConnection(ulong steamId, Connection connection)
{
if(steamId == _steamId.Value)
{
throw new InvalidOperationException("Cannot add Self as Peer");
}
SteamConnection connectionData = new SteamConnection();
connectionData.Connection = connection;
connectionData.SteamId = steamId;
_connectionsBySteamId.Add(steamId, connectionData);
}
}
using Steamworks;
using static Godot.MultiplayerPeer;
public class SteamNetworkingMessage
{
public SteamNetworkingMessage(byte[] data, SteamId sender, TransferModeEnum transferMode)
{
Data = data;
Sender = sender;
TransferMode = transferMode;
}
public byte[] Data { get; private set; }
public SteamId Sender { get; private set; }
public TransferModeEnum TransferMode { get; private set; }
public int Size => Data.Length;
}
using Godot;
using static Godot.MultiplayerPeer;
public partial class SteamPacketPeer : RefCounted
{
public SteamPacketPeer(byte[] data, TransferModeEnum transferMode = TransferModeEnum.Reliable)
{
Data = data;
TransferMode = transferMode;
}
public byte[] Data { get; private set; }
public ulong SenderSteamId { get; set; }
public TransferModeEnum TransferMode { get; private set; }
}
using Godot;
using Steamworks.Data;
using Steamworks;
using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Linq;
public class SteamSocketManager : SocketManager
{
private Dictionary<Connection, List<SteamNetworkingMessage>> _connectionMessages = new Dictionary<Connection, List<SteamNetworkingMessage>>();
private int _maxEnqueuedMessages = 1000;
public event Action<(Connection, ConnectionInfo)>? OnConnectionEstablished;
public event Action<(Connection, ConnectionInfo)>? OnConnectionLost;
public event Action<(Connection, ConnectionInfo)>? OnConnectionChange;
public override void OnConnectionChanged(Connection connection, ConnectionInfo info)
{
base.OnConnectionChanged(connection, info);
OnConnectionChange?.Invoke((connection, info));
}
public override void OnConnected(Connection connection, ConnectionInfo info)
{
base.OnConnected(connection, info);
_connectionMessages.Add(connection, new List<SteamNetworkingMessage>());
OnConnectionEstablished?.Invoke((connection, info));
}
public override void OnConnecting(Connection connection, ConnectionInfo info)
{
base.OnConnecting(connection, info);
}
public override void OnDisconnected(Connection connection, ConnectionInfo info)
{
base.OnDisconnected(connection, info);
_connectionMessages.Remove(connection);
OnConnectionLost?.Invoke((connection, info));
}
public override void OnMessage(Connection connection, NetIdentity identity, IntPtr data, int size, long messageNum, long recvTime, int channel)
{
base.OnMessage(connection, identity, data, size, messageNum, recvTime, channel);
if (_connectionMessages[connection].Count > _maxEnqueuedMessages)
{
_connectionMessages[connection].RemoveAt(0);
}
byte[] managedArray = new byte[size];
Marshal.Copy(data, managedArray, 0, size);
_connectionMessages[connection].Add(new SteamNetworkingMessage(managedArray, identity.SteamId, MultiplayerPeer.TransferModeEnum.Reliable));
}
public IEnumerable<SteamNetworkingMessage> ReceiveMessagesOnConnection(Connection connection, int maxMessageCount)
{
IEnumerable<SteamNetworkingMessage> steamNetworkingMessages = _connectionMessages[connection].Take(maxMessageCount).ToList();
for (int i = 0; i < maxMessageCount; i++)
{
if (_connectionMessages[connection].Any())
{
_connectionMessages[connection].RemoveAt(0);
}
}
return steamNetworkingMessages;
}
}
@Pieeer1
Copy link
Author

Pieeer1 commented Feb 17, 2024

Hey this is really cool! im going to have to dig though this!

Thanks!! I actually made this a bit cleaner and a fully functional example is included in this repository:

https://github.com/Pieeer1/SteamMultiplayerPeer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment