diff --git a/Basis/Assets/Basis/link.xml b/Basis/Assets/Basis/link.xml
index e64eb733a2..4d87e675d2 100644
--- a/Basis/Assets/Basis/link.xml
+++ b/Basis/Assets/Basis/link.xml
@@ -27,12 +27,16 @@
+
+
+
+
@@ -129,6 +133,8 @@
+
+
diff --git a/Basis/Packages/com.basis.eventdriver/BasisEventDriver.cs b/Basis/Packages/com.basis.eventdriver/BasisEventDriver.cs
index 65e24df29c..3e5d823969 100644
--- a/Basis/Packages/com.basis.eventdriver/BasisEventDriver.cs
+++ b/Basis/Packages/com.basis.eventdriver/BasisEventDriver.cs
@@ -123,6 +123,7 @@ public partial class BasisEventDriver : MonoBehaviour
public static BasisEventDriver Instance;
public static bool StateOfOnRenderBefore = false;
+ public static Action OnUpdate;
private int _volumeFrameworkFrameCounter;
@@ -200,6 +201,7 @@ public void Update()
InputSystem.Update();
OSCAcquisitionServer.Simulate();
timeSinceLastUpdate += DeltaTime;
+ OnUpdate?.Invoke();
}
///
diff --git a/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/ServersProvider.cs b/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/ServersProvider.cs
index d9b21d06e5..aafcbc5a95 100644
--- a/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/ServersProvider.cs
+++ b/Basis/Packages/com.basis.framework/BasisUI/Menus/Main Menu Providers/ServersProvider.cs
@@ -3,6 +3,7 @@
using Basis.Scripts.Device_Management.Devices.Desktop;
using Basis.Scripts.Drivers;
using Basis.Scripts.Networking;
+using Basis.Network.Core;
using System;
using System.Collections.Generic;
using System.Threading;
@@ -739,6 +740,9 @@ public static async Task PerformConnectionAsync(SavedServerEntry entry, string u
BasisNetworkManagement.Instance.Ip = entry.Address;
BasisNetworkManagement.Instance.Password = entry.HasPassword ? entry.Password : string.Empty;
BasisNetworkManagement.Instance.IsHostMode = isHostMode;
+ BasisNetworkManagement.Instance.Transport = NetworkTransportType.LiteNetLib;
+ BasisNetworkManagement.Instance.ClearPendingSteamWorld();
+ BasisNetworkManagement.Instance.ClearSteamLobbyState();
ReportConnectionProgress(60f, BasisLocalization.Get("menu.servers.status.loadingBundle"));
await LoadDefaultAssetBundleAsync();
diff --git a/Basis/Packages/com.basis.framework/Networking/BasisNetworkConnection.cs b/Basis/Packages/com.basis.framework/Networking/BasisNetworkConnection.cs
index 810c020b42..bcb61cd971 100644
--- a/Basis/Packages/com.basis.framework/Networking/BasisNetworkConnection.cs
+++ b/Basis/Packages/com.basis.framework/Networking/BasisNetworkConnection.cs
@@ -1,5 +1,5 @@
-using Basis.BasisUI;
using Basis.Network.Core;
+using Basis.BasisUI;
using Basis.Scripts.BasisSdk.Players;
using Basis.Scripts.Device_Management;
using Basis.Scripts.Drivers;
@@ -16,22 +16,44 @@
namespace Basis.Scripts.Networking
{
- ///
- /// Connection/session management, server runner, time utilities, and send helpers.
- ///
public static class BasisNetworkConnection
{
+ private static readonly object BnlSubscriptionLock = new object();
public static NetPeer LocalPlayerPeer { get; set; }
public static NetworkClient NetworkClient { get; set; } = new NetworkClient();
public static bool LocalPlayerIsConnected { get; set; }
+ public static bool SuppressNextDisconnectUi { get; set; }
+ public static event Action OnConnectedToServer;
+ public static event Action OnDisconnectedFromServer;
public static BasisNetworkServerRunner BasisNetworkServerRunner = null;
#if UNITY_SERVER
public static bool HeadlessReconnectSuppressed { get; set; }
public static Action OnDisconnectedAfterReboot;
#endif
+ private static bool HasRegisteredBnlLogging;
private static void LogErrorOutput(string msg) => BasisDebug.LogError(msg, BasisDebug.LogTag.Networking);
private static void LogWarningOutput(string msg) => BasisDebug.LogWarning(msg);
private static void LogOutput(string msg) => BasisDebug.Log(msg, BasisDebug.LogTag.Networking);
+ private static void EnsureBnlLoggingRegistered()
+ {
+ if (HasRegisteredBnlLogging)
+ {
+ return;
+ }
+
+ lock (BnlSubscriptionLock)
+ {
+ if (HasRegisteredBnlLogging)
+ {
+ return;
+ }
+
+ BNL.LogOutput += LogOutput;
+ BNL.LogWarningOutput += LogWarningOutput;
+ BNL.LogErrorOutput += LogErrorOutput;
+ HasRegisteredBnlLogging = true;
+ }
+ }
public static bool TryGetLocalPlayerID(out ushort localId)
{
localId = 0;
@@ -39,11 +61,73 @@ public static bool TryGetLocalPlayerID(out ushort localId)
localId = (ushort)LocalPlayerPeer.RemoteId;
return true;
}
+ public static void Connect(BasisNetworkManagement networkManagement)
+ {
+ if (networkManagement == null)
+ {
+ BasisDebug.LogError("Missing BasisNetworkManagement during connect.", BasisDebug.LogTag.Networking);
+ return;
+ }
+
+ Connect(
+ networkManagement.Port,
+ networkManagement.Ip,
+ networkManagement.Password,
+ networkManagement.IsHostMode,
+ networkManagement.Transport,
+ networkManagement.UseSteamRelay,
+ networkManagement.CurrentSteamLobbyId,
+ networkManagement.CurrentHostSteamId,
+ networkManagement.CurrentSteamVirtualPort
+ );
+ }
+ public static bool HasActiveClient() => NetworkClient != null && NetworkClient.HasActiveClient;
+ public static void DisconnectActiveClient()
+ {
+ NetworkClient?.Disconnect();
+ LocalPlayerPeer = null;
+ LocalPlayerIsConnected = false;
+ }
+ public static async Task ResetConnectionStateAsync(BasisNetworkManagement management)
+ {
+ if (!LocalPlayerIsConnected && !HasActiveClient())
+ {
+ return;
+ }
+
+ if (management == null)
+ {
+ SuppressNextDisconnectUi = true;
+ DisconnectActiveClient();
+ return;
+ }
+
+ if (!LocalPlayerIsConnected)
+ {
+ SuppressNextDisconnectUi = true;
+ DisconnectActiveClient();
+ return;
+ }
+
+ using var cts = new CancellationTokenSource();
+ Task rebootWait = WaitForRebootCompleteAsync(cts.Token);
+
+ SuppressNextDisconnectUi = true;
+ NetworkClient?.Disconnect();
+ await rebootWait;
+
+ if (management != null)
+ {
+ BasisNetworkLifeCycle.Initalize(management);
+ }
+ }
public static void Connect(ushort port, string ipString, string primitivePassword, bool isHostMode)
{
- BNL.LogOutput += LogOutput;
- BNL.LogWarningOutput += LogWarningOutput;
- BNL.LogErrorOutput += LogErrorOutput;
+ Connect(port, ipString, primitivePassword, isHostMode, NetworkTransportType.LiteNetLib, true, 0, 0, 0);
+ }
+ public static void Connect(ushort port, string ipString, string primitivePassword, bool isHostMode, NetworkTransportType transportType, bool useSteamRelay, ulong steamLobbyId, ulong steamHostSteamId, int steamVirtualPort)
+ {
+ EnsureBnlLoggingRegistered();
var uuid = BasisDIDAuthIdentityClient.GetOrSaveDID();
@@ -59,8 +143,14 @@ public static void Connect(ushort port, string ipString, string primitivePasswor
UseAuthIdentity = true,
UseAuth = true,
Password = primitivePassword,
- EnableStatistics = BasisSettingsDefaults.EnableStatistics.RawValue
+ EnableStatistics = BasisSettingsDefaults.EnableStatistics.RawValue,
+ TransportType = transportType,
+ UseSteamRelay = useSteamRelay,
+ SteamLobbyId = steamLobbyId,
+ SteamHostSteamId = steamHostSteamId,
+ SteamVirtualPort = steamVirtualPort
};
+ BasisDebug.Log($"Initializing host server with transport {transportType} relay={useSteamRelay} virtualPort={steamVirtualPort}", BasisDebug.LogTag.Networking);
BasisNetworkServerRunner.Initalize(serverConfig, string.Empty, uuid);
}
@@ -92,7 +182,7 @@ public static void Connect(ushort port, string ipString, string primitivePasswor
BasisDebug.Log("Network Starting Client");
- _ = Task.Run(() =>
+ void StartClientConnection()
{
try
{
@@ -104,17 +194,21 @@ public static void Connect(ushort port, string ipString, string primitivePasswor
UseAuthIdentity = true,
UseAuth = true,
Password = primitivePassword,
- EnableStatistics = BasisSettingsDefaults.EnableStatistics.RawValue
+ EnableStatistics = BasisSettingsDefaults.EnableStatistics.RawValue,
+ TransportType = transportType,
+ UseSteamRelay = useSteamRelay,
+ SteamLobbyId = steamLobbyId,
+ SteamHostSteamId = steamHostSteamId,
+ SteamVirtualPort = steamVirtualPort
};
- // Pass the token into anything that supports cancellation
+ NetworkClient.OnPeerConnected = PeerConnectedEvent;
+ NetworkClient.OnPeerDisconnected = BasisNetworkConnection.HandleDisconnection;
+ NetworkClient.OnNetworkReceive = BasisNetworkEvents.NetworkReceiveEvent;
+
LocalPlayerPeer = NetworkClient.StartClient(
ipString, port, readyMessage,
Encoding.UTF8.GetBytes(primitivePassword), serverConfig);
- NetworkClient.listener.PeerConnectedEvent += PeerConnectedEvent;
- NetworkClient.listener.PeerDisconnectedEvent += BasisNetworkConnection.HandleDisconnection;
- NetworkClient.listener.NetworkReceiveEvent += BasisNetworkEvents.NetworkReceiveEvent;
-
if (LocalPlayerPeer != null)
{
BasisDebug.Log("Network Client Started " + LocalPlayerPeer.RemoteId);
@@ -136,7 +230,16 @@ public static void Connect(ushort port, string ipString, string primitivePasswor
Reason = DisconnectReason.UnknownHost
});
}
- });
+ }
+
+ if (transportType == NetworkTransportType.Steam)
+ {
+ StartClientConnection();
+ }
+ else
+ {
+ _ = Task.Run(StartClientConnection);
+ }
}
public static void OnDestroy()
{
@@ -190,6 +293,7 @@ private static void PeerConnectedEvent(NetPeer peer)
LocalPlayerIsConnected = true;
+ OnConnectedToServer?.Invoke(peer);
BasisNetworkPlayer.OnLocalPlayerJoined?.Invoke(transmitter, BasisLocalPlayer.Instance);
BasisNetworkPlayer.OnPlayerJoined?.Invoke(transmitter);
}
@@ -204,6 +308,7 @@ public static void HandleDisconnection(NetPeer peer, DisconnectInfo disconnectIn
{
BasisDeviceManagement.EnqueueOnMainThread(async () =>
{
+ OnDisconnectedFromServer?.Invoke(peer, disconnectInfo);
#if UNITY_SERVER
if (disconnectInfo.Reason == DisconnectReason.Timeout)
{
@@ -213,7 +318,9 @@ public static void HandleDisconnection(NetPeer peer, DisconnectInfo disconnectIn
Basis.Scripts.Device_Management.Devices.Headless.BasisHeadlessInput.Instance?.StopMovement();
#endif
BasisNetworkAvatarCompressor.Dispose();
- await BasisNetworkLifeCycle.RebootManagement(BasisNetworkManagement.Instance, true, peer, disconnectInfo);
+ bool displayReason = !SuppressNextDisconnectUi;
+ SuppressNextDisconnectUi = false;
+ await BasisNetworkLifeCycle.RebootManagement(BasisNetworkManagement.Instance, displayReason, peer, disconnectInfo);
#if UNITY_SERVER
if (!HeadlessReconnectSuppressed)
{
diff --git a/Basis/Packages/com.basis.framework/Networking/BasisNetworkManagement.cs b/Basis/Packages/com.basis.framework/Networking/BasisNetworkManagement.cs
index 540d4f0c94..05d5181a2e 100644
--- a/Basis/Packages/com.basis.framework/Networking/BasisNetworkManagement.cs
+++ b/Basis/Packages/com.basis.framework/Networking/BasisNetworkManagement.cs
@@ -16,10 +16,6 @@
namespace Basis.Scripts.Networking
{
- ///
- /// Centralized network manager for Basis. Handles connection lifecycle, transmitters,
- /// simulation ticks, time synchronization, and server/client messaging.
- ///
[DefaultExecutionOrder(15001)]
public class BasisNetworkManagement : MonoBehaviour
{
@@ -48,7 +44,53 @@ public class BasisNetworkManagement : MonoBehaviour
public bool IsHostMode = false;
///
- /// Singleton instance of .
+ /// Transport selection.
+ ///
+ public NetworkTransportType Transport = NetworkTransportType.LiteNetLib;
+
+ ///
+ /// Prefer Steam relay over direct peer addressing.
+ ///
+ public bool UseSteamRelay = true;
+
+ ///
+ /// Active Steam lobby ID.
+ ///
+ [HideInInspector]
+ public ulong CurrentSteamLobbyId = 0;
+
+ ///
+ /// Current host Steam ID for Steam transport.
+ ///
+ [HideInInspector]
+ public ulong CurrentHostSteamId = 0;
+
+ ///
+ /// Steam virtual port for relay sockets.
+ ///
+ [HideInInspector]
+ public int CurrentSteamVirtualPort = 0;
+
+ ///
+ /// Pending world BEE URL for Steam lobby creation.
+ ///
+ [HideInInspector]
+ public string PendingSteamWorldUrl = string.Empty;
+
+ ///
+ /// Pending world password for Steam lobby creation.
+ ///
+ [HideInInspector]
+ public string PendingSteamWorldPassword = string.Empty;
+
+ ///
+ /// Pending world name from BEE metadata.
+ ///
+ [HideInInspector]
+ public string PendingSteamWorldName = string.Empty;
+
+ ///
+ /// Singleton instance.
///
public static BasisNetworkManagement Instance;
@@ -140,9 +182,48 @@ public static bool IsMainThread()
#region Connection Control
///
- /// Connects to the server using the configured , , and .
+ /// Connects to the server with current settings.
///
- public void Connect() => BasisNetworkConnection.Connect(Port, Ip, Password, IsHostMode);
+ public void Connect() => BasisNetworkConnection.Connect(this);
+
+ public bool HasPendingSteamWorld()
+ {
+ return !string.IsNullOrWhiteSpace(PendingSteamWorldUrl) && !string.IsNullOrWhiteSpace(PendingSteamWorldPassword);
+ }
+
+ public void UpdateSteamLobbyState(ulong lobbyId, ulong hostSteamId, bool useSteamRelay, int steamVirtualPort = 0)
+ {
+ CurrentSteamLobbyId = lobbyId;
+ CurrentHostSteamId = hostSteamId;
+ UseSteamRelay = useSteamRelay;
+ CurrentSteamVirtualPort = steamVirtualPort;
+ }
+
+ public void SetPendingSteamWorld(string worldUrl, string worldPassword, string worldName)
+ {
+ PendingSteamWorldUrl = worldUrl ?? string.Empty;
+ PendingSteamWorldPassword = worldPassword ?? string.Empty;
+ PendingSteamWorldName = worldName ?? string.Empty;
+ }
+
+ public void ClearPendingSteamWorld()
+ {
+ PendingSteamWorldUrl = string.Empty;
+ PendingSteamWorldPassword = string.Empty;
+ PendingSteamWorldName = string.Empty;
+ }
+
+ public void ClearSteamLobbyState()
+ {
+ CurrentSteamLobbyId = 0;
+ CurrentHostSteamId = 0;
+ UseSteamRelay = true;
+ CurrentSteamVirtualPort = 0;
+ if (Transport == NetworkTransportType.Steam)
+ {
+ Transport = NetworkTransportType.LiteNetLib;
+ }
+ }
#endregion
diff --git a/Basis/Packages/com.basis.framework/Networking/BasisNetworkPlayer.cs b/Basis/Packages/com.basis.framework/Networking/BasisNetworkPlayer.cs
index 6b939855f3..6ff7de9655 100644
--- a/Basis/Packages/com.basis.framework/Networking/BasisNetworkPlayer.cs
+++ b/Basis/Packages/com.basis.framework/Networking/BasisNetworkPlayer.cs
@@ -216,6 +216,11 @@ public bool CheckForAvatar()
}
public void OnAvatarServerReductionSystemMessageSend(byte MessageIndex, byte[] buffer = null)
{
+ if (!BasisNetworkManagement.NetworkRunning || !BasisNetworkConnection.LocalPlayerIsConnected)
+ {
+ return;
+ }
+
if (BasisNetworkManagement.Transmitter != null)
{
AdditionalAvatarData AAD = new AdditionalAvatarData
diff --git a/Basis/Packages/com.basis.framework/Networking/BasisNetworkServerRunner.cs b/Basis/Packages/com.basis.framework/Networking/BasisNetworkServerRunner.cs
index 1996895386..4eb8716c0b 100644
--- a/Basis/Packages/com.basis.framework/Networking/BasisNetworkServerRunner.cs
+++ b/Basis/Packages/com.basis.framework/Networking/BasisNetworkServerRunner.cs
@@ -1,4 +1,5 @@
using Basis.Network;
+using Basis.Network.Core;
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -16,26 +17,39 @@ public void Initalize(Configuration configuration, string LogPath,string UUIDTom
{
Configuration = configuration;
BasisServerSideLogging.Initialize(Configuration, LogPath);
+
+ if (configuration.TransportType == NetworkTransportType.Steam)
+ {
+ StartServer(UUIDTomarkAsAdmin);
+ return;
+ }
+
cancellationTokenSource = new CancellationTokenSource();
var cancellationToken = cancellationTokenSource.Token;
serverTask = Task.Run(() =>
{
- try
- {
- NetworkServer.StartServer(Configuration);
-
- PermissionIntegration.Manager.AddUserNode(UUIDTomarkAsAdmin,"*");
- PermissionIntegration.Manager.AddUserToGroup(UUIDTomarkAsAdmin, "admin");
- }
- catch (Exception ex)
- {
- BNL.LogError($"Server encountered an error: {ex.Message} {ex.StackTrace}");
- // Optionally, handle server restart or log critical errors
- }
+ StartServer(UUIDTomarkAsAdmin);
}, cancellationToken);
}
+
+ private void StartServer(string UUIDTomarkAsAdmin)
+ {
+ try
+ {
+ NetworkServer.StartServer(Configuration);
+
+ PermissionIntegration.Manager.AddUserNode(UUIDTomarkAsAdmin,"*");
+ PermissionIntegration.Manager.AddUserToGroup(UUIDTomarkAsAdmin, "admin");
+ }
+ catch (Exception ex)
+ {
+ BNL.LogError($"Server encountered an error: {ex.Message} {ex.StackTrace}");
+ }
+ }
+
public void Stop()
{
- cancellationTokenSource.Cancel();
+ cancellationTokenSource?.Cancel();
+ NetworkServer.StopServer();
}
}
diff --git a/Basis/Packages/com.basis.server/BasisNetworkClient/BasisNetworkClient.asmdef b/Basis/Packages/com.basis.server/BasisNetworkClient/BasisNetworkClient.asmdef
index 3b890eea44..15b2873db7 100644
--- a/Basis/Packages/com.basis.server/BasisNetworkClient/BasisNetworkClient.asmdef
+++ b/Basis/Packages/com.basis.server/BasisNetworkClient/BasisNetworkClient.asmdef
@@ -4,6 +4,7 @@
"references": [
"LiteNetLib",
"BasisNetworkCore",
+ "BasisSteamTransportCore",
"Did",
"Crypto"
],
@@ -16,4 +17,4 @@
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
-}
\ No newline at end of file
+}
diff --git a/Basis/Packages/com.basis.server/BasisNetworkClient/NetworkClient.cs b/Basis/Packages/com.basis.server/BasisNetworkClient/NetworkClient.cs
index 444d8cc478..bdfd5e3bf1 100644
--- a/Basis/Packages/com.basis.server/BasisNetworkClient/NetworkClient.cs
+++ b/Basis/Packages/com.basis.server/BasisNetworkClient/NetworkClient.cs
@@ -1,41 +1,47 @@
using Basis.Network.Core;
-
+using Basis.Scripts.Networking.Steam;
+using System;
using static Basis.Network.Core.Serializable.SerializableBasis;
using static SerializableBasis;
public class NetworkClient
{
- public NetManager client;
+ public NetManager client;
public EventBasedNetListener listener;
+ public Action OnPeerConnected;
+ public Action OnPeerDisconnected;
+ public Action OnNetworkReceive;
private NetPeer peer;
private bool IsInUse;
+ public bool HasActiveClient => IsInUse;
///
- /// inital data is typically the
+ /// Initial data is typically the ready/auth payload used during connection setup.
///
///
///
///
public NetPeer StartClient(string IP, int port, ReadyMessage ReadyMessage, byte[] AuthenticationMessage, Configuration Configuration)
{
- if (IsInUse == false)
- {
- listener = new EventBasedNetListener();
- client = new LNLNetManager(listener, Configuration);
- client.Start();
- NetDataWriter Writer = new NetDataWriter(true,12);
- //this is the only time we dont put key!
- Writer.Put(BasisNetworkVersion.ServerVersion);
- BytesMessage AuthBytes = new BytesMessage();
- AuthBytes.Serialize(Writer, AuthenticationMessage);
- ReadyMessage.Serialize(Writer);
- peer = client.Connect(IP, port, Writer);
- IsInUse = true;
- return peer;
- }
- else
+ if (IsInUse)
{
- BNL.LogError("Call Shutdown First!");
- return null;
+ BNL.LogError("NetworkClient.StartClient called while previous client still active. Forcing disconnect before reconnect.");
+ Disconnect();
}
+
+ listener = new EventBasedNetListener();
+ listener.PeerConnectedEvent += peer => OnPeerConnected?.Invoke(peer);
+ listener.PeerDisconnectedEvent += (peer, info) => OnPeerDisconnected?.Invoke(peer, info);
+ listener.NetworkReceiveEvent += (peer, reader, channel, method) => OnNetworkReceive?.Invoke(peer, reader, channel, method);
+ client = BasisTransportFactory.Create(listener, Configuration);
+ client.Start();
+ NetDataWriter Writer = new NetDataWriter(true, 12);
+ // This is the only connect path that writes the version directly before auth and ready payloads.
+ Writer.Put(BasisNetworkVersion.ServerVersion);
+ BytesMessage AuthBytes = new BytesMessage();
+ AuthBytes.Serialize(Writer, AuthenticationMessage);
+ ReadyMessage.Serialize(Writer);
+ peer = client.Connect(IP, port, Writer);
+ IsInUse = true;
+ return peer;
}
public void Disconnect()
{
diff --git a/Basis/Packages/com.basis.server/BasisNetworkCore/BasisNetworkShell.cs b/Basis/Packages/com.basis.server/BasisNetworkCore/BasisNetworkShell.cs
index 017964c02a..1af04b459a 100644
--- a/Basis/Packages/com.basis.server/BasisNetworkCore/BasisNetworkShell.cs
+++ b/Basis/Packages/com.basis.server/BasisNetworkCore/BasisNetworkShell.cs
@@ -43,6 +43,35 @@ public partial class EventBasedNetListener
public event OnNetworkError NetworkErrorEvent;
public event OnPeerConnected PeerConnectedEvent;
public event OnNetworkReceiveUnconnected NetworkReceiveUnconnectedEvent;
+
+ public void RaiseConnectionRequest(ConnectionRequest request)
+ {
+ ConnectionRequestEvent?.Invoke(request);
+ }
+
+ public void RaisePeerDisconnected(NetPeer peer, DisconnectInfo disconnectInfo)
+ {
+ PeerDisconnectedEvent?.Invoke(peer, disconnectInfo);
+ }
+
+ public void RaiseNetworkReceive(NetPeer peer, NetPacketReader reader, byte channel, DeliveryMethod deliveryMethod)
+ {
+#if UNITY_EDITOR || DEVELOPMENT_BUILD
+ reader.channel = channel;
+ reader.method = deliveryMethod;
+#endif
+ NetworkReceiveEvent?.Invoke(peer, reader, channel, deliveryMethod);
+ }
+
+ public void RaiseNetworkError(IPEndPoint endPoint, SocketError socketError)
+ {
+ NetworkErrorEvent?.Invoke(endPoint, socketError);
+ }
+
+ public void RaisePeerConnected(NetPeer peer)
+ {
+ PeerConnectedEvent?.Invoke(peer);
+ }
}
public interface ConnectionRequest
@@ -90,6 +119,9 @@ public void Start(int SetPort)
public void Start(IPAddress IPv4Address, IPAddress IPv6Address, int SetPort);
public void Stop();
public Basis.Network.Core.NetPeer Connect(string sIP, int port, NetDataWriter Writer);
+ public void PollEvents()
+ {
+ }
public bool SendUnconnectedMessage(NetDataWriter writer, IPEndPoint remoteEndPoint);
public NetStatistics Statistics { get; }
@@ -99,6 +131,10 @@ public void Start(int SetPort)
public sealed partial class NetStatistics
{
+ public NetStatistics()
+ {
+ }
+
public long PacketsSent;
public long PacketsReceived;
public long BytesSent;
@@ -110,6 +146,10 @@ public partial class NetPacketReader : NetDataReader
{
Action RecycleInternal;
+ public NetPacketReader()
+ {
+ }
+
#if UNITY_EDITOR || DEVELOPMENT_BUILD
internal byte channel;
internal DeliveryMethod method;
@@ -130,6 +170,14 @@ public void Recycle(bool IsOkTOHaveEmptyData = false)
RecycleInternal?.Invoke();
}
+
+ public static NetPacketReader Create(byte[] source, int offset, int maxSize, Action recycle = null)
+ {
+ var reader = new NetPacketReader();
+ reader.SetSource(source, offset, maxSize);
+ reader.RecycleInternal = recycle;
+ return reader;
+ }
}
// Lifted straight from litenetlib
diff --git a/Basis/Packages/com.basis.server/BasisNetworkCore/BasisServerConfiguration.cs b/Basis/Packages/com.basis.server/BasisNetworkCore/BasisServerConfiguration.cs
index e994adc3c5..d1d7153929 100644
--- a/Basis/Packages/com.basis.server/BasisNetworkCore/BasisServerConfiguration.cs
+++ b/Basis/Packages/com.basis.server/BasisNetworkCore/BasisServerConfiguration.cs
@@ -1,3 +1,4 @@
+using Basis.Network.Core;
using BasisNetworkCore.Security;
using System;
using System.IO;
@@ -92,6 +93,11 @@ public class Configuration
/// other content lockouts. Default off so existing deployments behave as before.
///
public bool ThirdPersonDisabled = false;
+ public NetworkTransportType TransportType = NetworkTransportType.LiteNetLib;
+ public bool UseSteamRelay = true;
+ public ulong SteamLobbyId = 0;
+ public ulong SteamHostSteamId = 0;
+ public int SteamVirtualPort = 0;
///
/// Read config from file. If no file is found create a default config file at filePath
///
diff --git a/Basis/Packages/com.basis.server/BasisNetworkCore/LNLNetworkImpl.cs b/Basis/Packages/com.basis.server/BasisNetworkCore/LNLNetworkImpl.cs
index aac54ab7c7..05602f9c5f 100644
--- a/Basis/Packages/com.basis.server/BasisNetworkCore/LNLNetworkImpl.cs
+++ b/Basis/Packages/com.basis.server/BasisNetworkCore/LNLNetworkImpl.cs
@@ -106,6 +106,8 @@ internal LNLConnectionRequest(LiteNetLib.ConnectionRequest request)
public IPEndPoint RemoteEndPoint => request.RemoteEndPoint;
+ public string Identity => request.RemoteEndPoint?.Address?.ToString() ?? string.Empty;
+
NetPeer ConnectionRequest.Accept()
{
return new LNLNetPeer(request.Accept());
@@ -235,6 +237,11 @@ public void Stop()
manager.Stop();
}
+ public void PollEvents()
+ {
+ manager.TriggerUpdate();
+ }
+
public Basis.Network.Core.NetPeer Connect(string sIP, int port, NetDataWriter Writer)
{
diff --git a/Basis/Packages/com.basis.server/BasisNetworkCore/NetworkTransportType.cs b/Basis/Packages/com.basis.server/BasisNetworkCore/NetworkTransportType.cs
new file mode 100644
index 0000000000..3798c13556
--- /dev/null
+++ b/Basis/Packages/com.basis.server/BasisNetworkCore/NetworkTransportType.cs
@@ -0,0 +1,8 @@
+namespace Basis.Network.Core
+{
+ public enum NetworkTransportType : byte
+ {
+ LiteNetLib = 0,
+ Steam = 1
+ }
+}
diff --git a/Basis/Packages/com.basis.server/BasisNetworkCore/NetworkTransportType.cs.meta b/Basis/Packages/com.basis.server/BasisNetworkCore/NetworkTransportType.cs.meta
new file mode 100644
index 0000000000..d9c95cf02a
--- /dev/null
+++ b/Basis/Packages/com.basis.server/BasisNetworkCore/NetworkTransportType.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: cfa3882424d6f4e4d934d89c280a8826
\ No newline at end of file
diff --git a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworkResourceManagement.cs b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworkResourceManagement.cs
index 4d94bfda16..1795cf9332 100644
--- a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworkResourceManagement.cs
+++ b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworkResourceManagement.cs
@@ -9,6 +9,11 @@
public static class BasisNetworkResourceManagement
{
public static ConcurrentDictionary UshortNetworkDatabase = new ConcurrentDictionary();
+
+ ///
+ /// Partial reset used for ordinary peer lifecycle.
+ /// Keeps persistent resources alive while removing non-persistent ones.
+ ///
public static void Reset()
{
LocalLoadResource[] resourceArray = UshortNetworkDatabase.Values.ToArray();
@@ -42,6 +47,16 @@ public static void Reset()
}
}
}
+
+ ///
+ /// Full reset used on complete server teardown / rehost.
+ /// Clears all resources, including persistent ones, so a new local host
+ /// session does not replay stale scene state from the previous session.
+ ///
+ public static void ResetAll()
+ {
+ UshortNetworkDatabase.Clear();
+ }
public static void SendOutAllResources(NetPeer NewConnection)
{
LocalLoadResource[] Resource = UshortNetworkDatabase.Values.ToArray();
diff --git a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworkServer.asmdef b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworkServer.asmdef
index c1f3c20c73..1adc46b2fd 100644
--- a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworkServer.asmdef
+++ b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworkServer.asmdef
@@ -4,6 +4,7 @@
"references": [
"LiteNetLib",
"BasisNetworkCore",
+ "BasisSteamTransportCore",
"Did",
"Crypto"
],
@@ -16,4 +17,4 @@
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
-}
\ No newline at end of file
+}
diff --git a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisSavedState.cs b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisSavedState.cs
index 48e3d34f69..8bb917ee8f 100644
--- a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisSavedState.cs
+++ b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworking/BasisSavedState.cs
@@ -47,6 +47,14 @@ public static void RemovePlayer(int id)
}
}
+ public static void Reset()
+ {
+ avatarChangeStates.Clear();
+ playerMetaDataMessages.Clear();
+ resolvedVoicePeers.Clear();
+ shoutModeStates.Clear();
+ }
+
///
/// Adds or updates the ReadyMessage for a player.
///
diff --git a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworkingReductionSystem/BasisServerReductionSystemEvents.cs b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworkingReductionSystem/BasisServerReductionSystemEvents.cs
index 1fd9f96f36..cfd1bc5a06 100644
--- a/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworkingReductionSystem/BasisServerReductionSystemEvents.cs
+++ b/Basis/Packages/com.basis.server/BasisNetworkServer/BasisNetworkingReductionSystem/BasisServerReductionSystemEvents.cs
@@ -367,9 +367,9 @@ private static void RunTick(long startTick)
//Phase 4: Network I/O
BasisNetworkPIPCamera.UpdatePIPPositions(now);
- if (NetworkServer.Server != null && NetworkServer.Server.manager != null)
+ if (NetworkServer.Server != null)
{
- NetworkServer.Server.manager.TriggerUpdate();
+ NetworkServer.Server.PollEvents();
}
if (profiling)
{
diff --git a/Basis/Packages/com.basis.server/BasisNetworkServer/NetworkServer.cs b/Basis/Packages/com.basis.server/BasisNetworkServer/NetworkServer.cs
index 07baccb7fe..23689b5ead 100644
--- a/Basis/Packages/com.basis.server/BasisNetworkServer/NetworkServer.cs
+++ b/Basis/Packages/com.basis.server/BasisNetworkServer/NetworkServer.cs
@@ -2,10 +2,15 @@
using Basis.Network.Core.Compression;
using Basis.Network.Server;
using Basis.Network.Server.Auth;
+using Basis.Network.Server.Generic;
+using Basis.Network.Server.Ownership;
+using Basis.Scripts.Networking.Steam;
using BasisDidLink;
+using BasisNetworkCore;
using BasisNetworkServer.BasisNetworking;
using BasisNetworkServer.BasisNetworkingReductionSystem;
using BasisNetworkServer.Security;
+using BasisNetworkServer;
using BasisServerHandle;
using System;
using System.Collections.Concurrent;
@@ -19,7 +24,7 @@
public static class NetworkServer
{
public static EventBasedNetListener Listener;
- public static LNLNetManager Server;
+ public static NetManager Server;
public static ConcurrentDictionary AuthenticatedPeers = new();
public static Configuration Configuration;
///
@@ -60,10 +65,17 @@ public static void ReturnWriter(NetDataWriter writer)
public static IAuth Auth;
public static IAuthIdentity AuthIdentity;
public static int HighQualityLength;
+ public static bool IsShuttingDown { get; private set; }
#region Server Entry Point
public static void StartServer(Configuration configuration)
{
+ if (Server != null || Listener != null || AuthIdentity != null || !AuthenticatedPeers.IsEmpty)
+ {
+ StopServer();
+ }
+
+ IsShuttingDown = false;
Configuration = configuration;
HighQualityLength = BasisAvatarBitPacking.ConvertToSize(BitQuality.High);
@@ -103,6 +115,8 @@ private static void InitializeAuth()
BasisPlayerModeration.UseFileOnDisc = HasFileSupport;
IAuthIdentity.HasFileSupport = HasFileSupport;
+ AuthIdentity?.DeInitialize();
+
Auth = new PasswordAuth(Configuration.Password ?? string.Empty);
AuthIdentity = new BasisDIDAuthIdentity();
@@ -139,7 +153,7 @@ private static void SubscribeEvents(Configuration Configuration)
public static void SetupServer(Configuration configuration)
{
Listener = new EventBasedNetListener();
- Server = new LNLNetManager(Listener, configuration);
+ Server = BasisTransportFactory.Create(Listener, configuration);
NetDebug.Logger = new BasisServerLogger();
StartListening(configuration);
@@ -171,6 +185,64 @@ public static void StartListening(Configuration configuration)
Server.Start(IPAddress.Any, IPAddress.IPv6Any, Configuration.SetPort);
}
}
+
+ public static void StopServer()
+ {
+ IsShuttingDown = true;
+
+ try
+ {
+ BasisServerHandleEvents.UnsubscribeServerEvents();
+ }
+ catch (Exception ex)
+ {
+ BNL.LogError($"StopServer unsubscribe failed: {ex.Message}");
+ }
+
+ try
+ {
+ AuthIdentity?.DeInitialize();
+ }
+ catch (Exception ex)
+ {
+ BNL.LogError($"StopServer auth deinitialize failed: {ex.Message}");
+ }
+
+ try
+ {
+ Server?.Stop();
+ }
+ catch (Exception ex)
+ {
+ BNL.LogError($"StopServer transport stop failed: {ex.Message}");
+ }
+
+ BasisStatistics.StopWorkerThread();
+ ResetRuntimeState();
+ }
+
+ private static void ResetRuntimeState()
+ {
+ Auth = null;
+ AuthIdentity = null;
+
+ AuthenticatedPeers.Clear();
+ RebuildPeerSnapshot();
+
+ BasisNetworkOwnership.ownershipByObjectId.Clear();
+ PermissionIntegration.Shutdown();
+ BasisSavedState.Reset();
+ BasisNetworkIDDatabase.Reset();
+ BasisNetworkResourceManagement.ResetAll();
+ BasisNetworkContentShare.Reset();
+ BasisNetworkPreloadResourceManagement.Reset();
+ BasisNetworkPIPCamera.Reset();
+ BasisNetworkStatistics.Clear();
+
+ Listener = null;
+ Server = null;
+ Configuration = null;
+ }
#endregion
public static void BroadcastMessageToClients(NetDataWriter writer, byte channel, NetPeer sender, ReadOnlySpan clients, DeliveryMethod deliveryMethod = DeliveryMethod.Sequenced, int maxMessages = 70)
{
diff --git a/Basis/Packages/com.basis.server/BasisNetworkServer/Security/BasisDIDAuthIdentity.cs b/Basis/Packages/com.basis.server/BasisNetworkServer/Security/BasisDIDAuthIdentity.cs
index 0800289259..6c2429df8c 100644
--- a/Basis/Packages/com.basis.server/BasisNetworkServer/Security/BasisDIDAuthIdentity.cs
+++ b/Basis/Packages/com.basis.server/BasisNetworkServer/Security/BasisDIDAuthIdentity.cs
@@ -55,6 +55,19 @@ public BasisDIDAuthIdentity()
public void DeInitialize()
{
BasisServerHandleEvents.OnAuthReceived -= OnAuthReceived;
+ foreach (KeyValuePair pair in _timeouts)
+ {
+ try
+ {
+ pair.Value.Cancel();
+ pair.Value.Dispose();
+ }
+ catch
+ {
+ }
+ }
+ _timeouts.Clear();
+ AuthIdentity.Clear();
BNL.Log("DidAuthIdentity deinitialized.");
}
diff --git a/Basis/Packages/com.basis.server/BasisNetworkServer/Security/PermissionManager.cs b/Basis/Packages/com.basis.server/BasisNetworkServer/Security/PermissionManager.cs
index 485b8fa559..70ea1cbc93 100644
--- a/Basis/Packages/com.basis.server/BasisNetworkServer/Security/PermissionManager.cs
+++ b/Basis/Packages/com.basis.server/BasisNetworkServer/Security/PermissionManager.cs
@@ -1044,6 +1044,7 @@ public static void Init(string xmlPath)
// Ensure saved
Manager.SaveToXmlDebounced();
+ Manager.OnPermissionsChanged -= HandlePermissionsChanged;
Manager.OnPermissionsChanged += HandlePermissionsChanged;
}
public static void InitWithoutDisc()
@@ -1051,9 +1052,16 @@ public static void InitWithoutDisc()
// Optional defaults if file was empty/nonexistent
Manager.EnsureDefaults();
+ Manager.OnPermissionsChanged -= HandlePermissionsChanged;
Manager.OnPermissionsChanged += HandlePermissionsChanged;
}
+ public static void Shutdown()
+ {
+ Manager.OnPermissionsChanged -= HandlePermissionsChanged;
+ _playerMeta.Clear();
+ }
+
///
/// Store player metadata when they connect so we can rebuild ServerMetaDataMessage later.
///
diff --git a/Basis/Packages/com.basis.steamtransport/BasisSteamTransport.asmdef b/Basis/Packages/com.basis.steamtransport/BasisSteamTransport.asmdef
new file mode 100644
index 0000000000..374d9a886b
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/BasisSteamTransport.asmdef
@@ -0,0 +1,23 @@
+{
+ "name": "BasisSteamTransport",
+ "rootNamespace": "",
+ "references": [
+ "BasisCommon",
+ "BasisDebug",
+ "BasisNetworkCore",
+ "BasisSteamTransportCore",
+ "BasisBundleManagement",
+ "Basis Framework",
+ "BasisEventDriver",
+ "BasisSDK"
+ ],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
diff --git a/Basis/Packages/com.basis.steamtransport/BasisSteamTransport.asmdef.meta b/Basis/Packages/com.basis.steamtransport/BasisSteamTransport.asmdef.meta
new file mode 100644
index 0000000000..1c07555073
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/BasisSteamTransport.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 74db547041184a144ba10a8518b7e490
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime.meta b/Basis/Packages/com.basis.steamtransport/Runtime.meta
new file mode 100644
index 0000000000..370ed04cbb
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 5804928eb8a5e234b8089d7d73ee6527
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Bootstrap.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Bootstrap.meta
new file mode 100644
index 0000000000..3772428e70
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Bootstrap.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: afee4ba3ef96e5344a0e098aa75ff2fe
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Bootstrap/BasisSteamBootstrap.cs b/Basis/Packages/com.basis.steamtransport/Runtime/Bootstrap/BasisSteamBootstrap.cs
new file mode 100644
index 0000000000..e6abe3f56b
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Bootstrap/BasisSteamBootstrap.cs
@@ -0,0 +1,165 @@
+using Steamworks;
+using Basis.EventDriver;
+using UnityEngine;
+
+namespace Basis.Scripts.Networking.Steam
+{
+ [DefaultExecutionOrder(-14950)]
+ public class BasisSteamBootstrap : MonoBehaviour
+ {
+ public BasisSteamSettings Settings;
+
+ public static BasisSteamBootstrap Instance;
+ public static BasisSteamSettings ActiveSettings { get; private set; }
+ public static bool IsInitialized => SteamClient.IsValid;
+ public static bool HasTriedInitialization { get; private set; }
+ public static bool HasRequestedRelayWarmup { get; private set; }
+
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
+ private static void EnsureRuntimeInstance()
+ {
+ if (Instance != null)
+ {
+ return;
+ }
+
+ GameObject bootstrapObject = new GameObject(nameof(BasisSteamBootstrap));
+ DontDestroyOnLoad(bootstrapObject);
+ bootstrapObject.hideFlags = HideFlags.DontSave;
+ bootstrapObject.AddComponent();
+ }
+
+ private void OnEnable()
+ {
+ if (Instance != null && Instance != this)
+ {
+ enabled = false;
+ return;
+ }
+
+ Instance = this;
+ ActiveSettings = ResolveSettings(Settings);
+ BasisSteamTransportMetrics.Reset();
+ BasisSteamTransportTrace.Configure(ActiveSettings != null && ActiveSettings.EnableTransportTrace);
+ BasisSteamTransportTrace.Clear();
+
+ if (SteamClient.IsValid)
+ {
+ EnsureRelayWarmup();
+ }
+
+ if (ActiveSettings != null && ActiveSettings.AutoInitialize)
+ {
+ EnsureInitialized(ActiveSettings);
+ }
+
+ BasisEventDriver.OnUpdate -= Tick;
+ BasisEventDriver.OnUpdate += Tick;
+ }
+
+ private static void Tick()
+ {
+ if (SteamClient.IsValid && ActiveSettings != null && ActiveSettings.RunCallbacksManually)
+ {
+ SteamClient.RunCallbacks();
+ }
+
+ BasisSteamTransportTrace.FlushPending();
+ SteamNetManager.PollActiveManagers();
+ }
+
+ private void OnDisable()
+ {
+ if (Instance == this)
+ {
+ Instance = null;
+ }
+
+ BasisEventDriver.OnUpdate -= Tick;
+ }
+
+ private void OnApplicationQuit()
+ {
+ BasisSteamTransportTrace.FlushPending(force: true);
+ Shutdown();
+ }
+
+ public static bool EnsureInitialized(BasisSteamSettings settings)
+ {
+ HasTriedInitialization = true;
+ settings = ResolveSettings(settings);
+
+ if (settings == null)
+ {
+ BasisDebug.LogError("Missing BasisSteamSettings asset. Cannot initialize Steam.", BasisDebug.LogTag.Networking);
+ return false;
+ }
+
+ ActiveSettings = settings;
+ BasisSteamTransportTrace.Configure(ActiveSettings != null && ActiveSettings.EnableTransportTrace);
+
+ if (SteamClient.IsValid)
+ {
+ return true;
+ }
+
+ try
+ {
+ if (settings.RestartAppIfNecessary && SteamClient.RestartAppIfNecessary(settings.AppId))
+ {
+ return false;
+ }
+
+ SteamClient.Init(settings.AppId, asyncCallbacks: !settings.RunCallbacksManually);
+ EnsureRelayWarmup();
+ return SteamClient.IsValid;
+ }
+ catch (System.Exception ex)
+ {
+ BasisDebug.LogError($"Steam init failed: {ex.Message}", BasisDebug.LogTag.Networking);
+ return false;
+ }
+ }
+
+ public static void Shutdown()
+ {
+ BasisSteamLobbyService.HandleSteamShutdown();
+
+ if (SteamClient.IsValid)
+ {
+ SteamClient.Shutdown();
+ }
+
+ HasRequestedRelayWarmup = false;
+ }
+
+ public static BasisSteamSettings ResolveSettings(BasisSteamSettings settings = null)
+ {
+ if (settings != null)
+ {
+ ActiveSettings = settings;
+ return ActiveSettings;
+ }
+
+ if (ActiveSettings != null)
+ {
+ return ActiveSettings;
+ }
+
+ ActiveSettings = ScriptableObject.CreateInstance();
+ ActiveSettings.hideFlags = HideFlags.DontSave;
+ return ActiveSettings;
+ }
+
+ private static void EnsureRelayWarmup()
+ {
+ if (HasRequestedRelayWarmup || !SteamClient.IsValid)
+ {
+ return;
+ }
+
+ SteamNetworkingUtils.InitRelayNetworkAccess();
+ HasRequestedRelayWarmup = true;
+ }
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Bootstrap/BasisSteamBootstrap.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Bootstrap/BasisSteamBootstrap.cs.meta
new file mode 100644
index 0000000000..a7d9630d5d
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Bootstrap/BasisSteamBootstrap.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 9a3b2b6f3955f7947a3b20c3a2512a6e
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Integration.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Integration.meta
new file mode 100644
index 0000000000..26488d9bcf
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Integration.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: f9a653159b928e845bf27bdbc596bd6d
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamBeeValidation.cs b/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamBeeValidation.cs
new file mode 100644
index 0000000000..0f8c7945ca
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamBeeValidation.cs
@@ -0,0 +1,85 @@
+using Basis.BasisUI;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using static Basis.Scripts.UI.UI_Panels.BasisDataStoreItemKeys;
+
+namespace Basis.Scripts.Networking.Steam
+{
+ public static class BasisSteamBeeValidation
+ {
+ public static async Task ValidateWorldAsync(string url, string password, CancellationToken cancellationToken = default)
+ {
+ BasisSteamBeeValidationResult result = new BasisSteamBeeValidationResult
+ {
+ WorldUrl = url ?? string.Empty,
+ WorldPassword = password ?? string.Empty,
+ };
+
+ InputValidation.EntryValidationResponse validationResponse = InputValidation.ValidateEntry(url, password, Array.Empty());
+ if (validationResponse.Result != InputValidation.EntryValidationResult.Success)
+ {
+ result.ErrorMessage = validationResponse.Result switch
+ {
+ InputValidation.EntryValidationResult.EmptyUrl => "URL cannot be empty.",
+ InputValidation.EntryValidationResult.InvalidUrlFormat => "URL format is invalid.",
+ InputValidation.EntryValidationResult.InvalidUrlScheme => "URL must start with http:// or https://",
+ InputValidation.EntryValidationResult.EmptyPassword => "Password cannot be empty.",
+ _ => "BEE validation input failed."
+ };
+ return result;
+ }
+
+ ItemKey tempItem = new ItemKey
+ {
+ Pass = validationResponse.Password,
+ Url = validationResponse.ProcessedUrl,
+ Mode = 0
+ };
+
+ var tempWrapper = LibraryProvider.CreateNewWrapperFromItem(tempItem);
+ BasisProgressReport report = new BasisProgressReport();
+
+ bool isValid = await BasisBeeManagement.HandleMetaOnlyLoad(tempWrapper.basisTrackedBundleWrapper, report, cancellationToken);
+ if (!isValid)
+ {
+ result.ErrorMessage = "The provided BEE file could not be validated.";
+ return result;
+ }
+
+ var loaded = await LibraryProvider.LoadWrapperFromDisc(tempItem, tempWrapper);
+ if (loaded?.BasisLoadableBundle?.BasisBundleConnector == null)
+ {
+ result.ErrorMessage = "Validated BEE file did not provide bundle connector metadata.";
+ return result;
+ }
+
+ BasisBundleConnector connector = loaded.BasisLoadableBundle.BasisBundleConnector;
+ result.WorldName = connector.BasisBundleDescription?.AssetBundleName ?? validationResponse.ProcessedUrl;
+
+ bool isWorld = false;
+ if (connector.MetaData.ComponentNames != null)
+ {
+ foreach (BasisBundleConnector.BasisComponentName component in connector.MetaData.ComponentNames)
+ {
+ if (string.Equals(component.Name, "BasisScene", StringComparison.OrdinalIgnoreCase))
+ {
+ isWorld = true;
+ break;
+ }
+ }
+ }
+
+ if (!isWorld)
+ {
+ result.ErrorMessage = "The provided BEE file is valid, but it is not a world scene bundle.";
+ return result;
+ }
+
+ result.WorldUrl = validationResponse.ProcessedUrl;
+ result.WorldPassword = validationResponse.Password;
+ result.IsValid = true;
+ return result;
+ }
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamBeeValidation.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamBeeValidation.cs.meta
new file mode 100644
index 0000000000..f0014ad9a5
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamBeeValidation.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 1fbf933d57f95d44b8d45d1d43a0061e
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamBeeValidationResult.cs b/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamBeeValidationResult.cs
new file mode 100644
index 0000000000..264620923b
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamBeeValidationResult.cs
@@ -0,0 +1,11 @@
+namespace Basis.Scripts.Networking.Steam
+{
+ public sealed class BasisSteamBeeValidationResult
+ {
+ public bool IsValid;
+ public string ErrorMessage = string.Empty;
+ public string WorldUrl = string.Empty;
+ public string WorldPassword = string.Empty;
+ public string WorldName = string.Empty;
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamBeeValidationResult.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamBeeValidationResult.cs.meta
new file mode 100644
index 0000000000..7ecc850608
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamBeeValidationResult.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a4909ea6b84cdbe4997e14e39ca90f91
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamNetworkIntegration.cs b/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamNetworkIntegration.cs
new file mode 100644
index 0000000000..76fe645dfa
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamNetworkIntegration.cs
@@ -0,0 +1,189 @@
+using Basis.Network.Core;
+using Basis.BasisUI;
+using Basis.Scripts.Device_Management;
+using Basis.Scripts.Device_Management.Devices.Desktop;
+using Basis.Scripts.Networking;
+using System;
+using System.Threading.Tasks;
+using UnityEngine;
+
+namespace Basis.Scripts.Networking.Steam
+{
+ public static class BasisSteamNetworkIntegration
+ {
+ private static bool isSubscribed;
+ private static ulong bootstrappedLobbyId;
+
+ [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)]
+ private static void Initialize()
+ {
+ Subscribe();
+ SyncLobbyStateToNetworkManagement(BasisSteamLobbyService.State);
+ }
+
+ private static void Subscribe()
+ {
+ if (isSubscribed)
+ {
+ return;
+ }
+
+ isSubscribed = true;
+ BasisSteamLobbyService.OnLobbyStateChanged += SyncLobbyStateToNetworkManagement;
+ BasisSteamLobbyService.OnLobbyJoinRequested += HandleLobbyJoinRequested;
+ BasisNetworkManagement.OnIstanceCreated += OnNetworkManagementCreated;
+ BasisNetworkConnection.OnConnectedToServer += HandleConnectedToServer;
+ BasisNetworkConnection.OnDisconnectedFromServer += HandleDisconnectedFromServer;
+ }
+
+ private static void OnNetworkManagementCreated()
+ {
+ SyncLobbyStateToNetworkManagement(BasisSteamLobbyService.State);
+ }
+
+ private static void SyncLobbyStateToNetworkManagement(BasisSteamLobbyState lobbyState)
+ {
+ BasisNetworkManagement management = BasisNetworkManagement.Instance;
+ if (management == null)
+ {
+ return;
+ }
+
+ if (lobbyState == null || lobbyState.LobbyId == 0)
+ {
+ management.ClearSteamLobbyState();
+ return;
+ }
+
+ management.UpdateSteamLobbyState(lobbyState.LobbyId, lobbyState.HostSteamId, lobbyState.UseRelay, lobbyState.VirtualPort);
+ }
+
+ private static void HandleConnectedToServer(NetPeer peer)
+ {
+ BasisDeviceManagement.EnqueueOnMainThread(() =>
+ {
+ BasisNetworkManagement management = BasisNetworkManagement.Instance;
+ if (management == null)
+ {
+ BasisDebug.LogError("Steam HandleConnectedToServer: BasisNetworkManagement.Instance is null", BasisDebug.LogTag.Networking);
+ return;
+ }
+
+ if (management.Transport != NetworkTransportType.Steam)
+ {
+ return;
+ }
+
+ if (BasisSteamLobbyService.State.IsHost == false || management.CurrentSteamLobbyId == 0)
+ {
+ return;
+ }
+
+ if (management.HasPendingSteamWorld() == false)
+ {
+ return;
+ }
+
+ if (bootstrappedLobbyId == management.CurrentSteamLobbyId)
+ {
+ return;
+ }
+
+ if (BasisNetworkSpawnItem.RequestSceneLoad(
+ management.PendingSteamWorldPassword,
+ management.PendingSteamWorldUrl,
+ true,
+ false,
+ out _,
+ 2))
+ {
+ bootstrappedLobbyId = management.CurrentSteamLobbyId;
+ BasisDebug.Log($"Steam host bootstrap queued world load for {management.PendingSteamWorldName}", BasisDebug.LogTag.Networking);
+ }
+ else
+ {
+ BasisDebug.LogError("Steam host bootstrap failed to queue world load request.", BasisDebug.LogTag.Networking);
+ }
+ });
+ }
+
+ private static void HandleDisconnectedFromServer(NetPeer peer, DisconnectInfo disconnectInfo)
+ {
+ bootstrappedLobbyId = 0;
+
+ BasisNetworkManagement management = BasisNetworkManagement.Instance;
+ if (management == null || management.Transport != NetworkTransportType.Steam)
+ {
+ return;
+ }
+
+ if (!BasisNetworkConnection.SuppressNextDisconnectUi)
+ {
+ BasisSteamLobbyService.LeaveLobby();
+ }
+ }
+
+ private static void HandleLobbyJoinRequested(ulong lobbyId)
+ {
+ BasisDeviceManagement.EnqueueOnMainThread(() => _ = JoinRequestedLobbyOnMainThreadAsync(lobbyId));
+ }
+
+ private static async Task JoinRequestedLobbyOnMainThreadAsync(ulong lobbyId)
+ {
+ if (lobbyId == 0)
+ {
+ BasisDebug.LogError("Steam JoinRequestedLobby called with lobbyId=0", BasisDebug.LogTag.Networking);
+ return;
+ }
+
+ BasisNetworkManagement management = BasisNetworkManagement.Instance;
+ if (management == null)
+ {
+ BasisDebug.LogError("Steam JoinRequestedLobby: BasisNetworkManagement.Instance is null", BasisDebug.LogTag.Networking);
+ return;
+ }
+
+ if (BasisSteamLobbyService.State.LobbyId == lobbyId &&
+ management.Transport == NetworkTransportType.Steam &&
+ (BasisNetworkConnection.LocalPlayerIsConnected || BasisNetworkConnection.HasActiveClient()))
+ {
+ return;
+ }
+
+ try
+ {
+ if (BasisSteamLobbyService.State.LobbyId != 0 && BasisSteamLobbyService.State.LobbyId != lobbyId)
+ {
+ BasisSteamLobbyService.LeaveLobby();
+ }
+
+ await BasisNetworkConnection.ResetConnectionStateAsync(management);
+
+ BasisSteamLobbyState joinedLobby = await BasisSteamLobbyService.JoinLobbyAsync(lobbyId);
+ if (joinedLobby == null)
+ {
+ BasisDebug.LogError($"Steam lobby invite join failed for lobby {lobbyId}.", BasisDebug.LogTag.Networking);
+ return;
+ }
+
+ management.Transport = NetworkTransportType.Steam;
+ management.IsHostMode = false;
+ management.UpdateSteamLobbyState(joinedLobby.LobbyId, joinedLobby.HostSteamId, joinedLobby.UseRelay, joinedLobby.VirtualPort);
+ management.ClearPendingSteamWorld();
+
+ BasisDebug.Log($"Joining Steam lobby from invite {joinedLobby.LobbyId}.", BasisDebug.LogTag.Networking);
+ BasisMainMenu.Close();
+ BasisCursorManagement.OnReset();
+ management.Connect();
+ if (BasisDesktopEye.Instance != null)
+ {
+ BasisDesktopEye.Instance.LockEye();
+ }
+ }
+ catch (Exception ex)
+ {
+ BasisDebug.LogError(ex.ToString(), BasisDebug.LogTag.Networking);
+ }
+ }
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamNetworkIntegration.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamNetworkIntegration.cs.meta
new file mode 100644
index 0000000000..d9911f0816
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamNetworkIntegration.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 60b3a2afc083c504390bfaa7cb0e9fca
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Lobby.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Lobby.meta
new file mode 100644
index 0000000000..bd18b3c8eb
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Lobby.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 524e3032a61fff0488277f3df176279e
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyMetadata.cs b/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyMetadata.cs
new file mode 100644
index 0000000000..62d71791e3
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyMetadata.cs
@@ -0,0 +1,14 @@
+namespace Basis.Scripts.Networking.Steam
+{
+ public static class BasisSteamLobbyMetadata
+ {
+ public const string Transport = "transport";
+ public const string Version = "version";
+ public const string WorldUrl = "world_url";
+ public const string WorldName = "world_name";
+ public const string HostSteamId = "host_steam_id";
+ public const string VirtualPort = "virtual_port";
+ public const string UseRelay = "use_relay";
+ public const string Name = "name";
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyMetadata.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyMetadata.cs.meta
new file mode 100644
index 0000000000..11796207d3
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyMetadata.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4c171f94e971d3c4e8b8a475037343a6
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyService.cs b/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyService.cs
new file mode 100644
index 0000000000..d273019c20
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyService.cs
@@ -0,0 +1,344 @@
+using Steamworks;
+using Steamworks.Data;
+using Basis.Network.Core;
+using Basis.Scripts.Networking;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading.Tasks;
+using UnityEngine;
+
+namespace Basis.Scripts.Networking.Steam
+{
+ public static class BasisSteamLobbyService
+ {
+ public static readonly BasisSteamLobbyState State = new BasisSteamLobbyState();
+ private static Lobby? currentLobby;
+ private static bool steamCallbacksSubscribed;
+
+ public static event Action OnLobbyStateChanged;
+ public static event Action OnLobbyError;
+ public static event Action OnLobbyJoinRequested;
+
+ public static bool EnsureReady()
+ {
+ if (!BasisSteamBootstrap.IsInitialized)
+ {
+ if (!BasisSteamBootstrap.EnsureInitialized(BasisSteamBootstrap.ActiveSettings))
+ {
+ OnLobbyError?.Invoke("Steam is not initialized.");
+ return false;
+ }
+ }
+
+ if (!SteamClient.IsLoggedOn)
+ {
+ OnLobbyError?.Invoke("Steam user is not logged in.");
+ return false;
+ }
+
+ SubscribeToSteamCallbacks();
+ return true;
+ }
+
+ public static async Task CreateLobbyAsync(string lobbyName, BasisSteamBeeValidationResult world, bool friendsOnly, bool isPrivate, bool useRelay)
+ {
+ if (!EnsureReady())
+ {
+ return null;
+ }
+
+ BasisSteamSettings settings = BasisSteamBootstrap.ActiveSettings;
+ int maxMembers = Mathf.Clamp(settings != null ? settings.DefaultMaxLobbyMembers : 32, 2, 250);
+ int virtualPort = settings != null ? settings.RelayVirtualPort : 0;
+
+ Lobby? created = await SteamMatchmaking.CreateLobbyAsync(maxMembers);
+ if (!created.HasValue)
+ {
+ OnLobbyError?.Invoke("Failed to create Steam lobby.");
+ return null;
+ }
+
+ Lobby lobby = created.Value;
+ lobby.SetData(BasisSteamLobbyMetadata.Transport, NetworkTransportType.Steam.ToString());
+ lobby.SetData(BasisSteamLobbyMetadata.Version, BasisNetworkVersion.ServerVersion.ToString(CultureInfo.InvariantCulture));
+ lobby.SetData(BasisSteamLobbyMetadata.WorldUrl, world.WorldUrl);
+ lobby.SetData(BasisSteamLobbyMetadata.WorldName, world.WorldName);
+ lobby.SetData(BasisSteamLobbyMetadata.HostSteamId, SteamClient.SteamId.ToString());
+ lobby.SetData(BasisSteamLobbyMetadata.VirtualPort, virtualPort.ToString(CultureInfo.InvariantCulture));
+ lobby.SetData(BasisSteamLobbyMetadata.UseRelay, useRelay ? "1" : "0");
+ lobby.SetData(BasisSteamLobbyMetadata.Name, string.IsNullOrWhiteSpace(lobbyName) ? SteamClient.Name : lobbyName);
+
+ if (isPrivate)
+ {
+ lobby.SetPrivate();
+ }
+ else if (friendsOnly)
+ {
+ lobby.SetFriendsOnly();
+ }
+ else
+ {
+ lobby.SetPublic();
+ }
+
+ lobby.SetJoinable(true);
+
+ ApplyState(lobby, true, useRelay);
+ return CloneState();
+ }
+
+ public static async Task JoinLobbyAsync(ulong lobbyId)
+ {
+ if (!EnsureReady())
+ {
+ return null;
+ }
+
+ Lobby? joined = await SteamMatchmaking.JoinLobbyAsync(lobbyId);
+ if (!joined.HasValue)
+ {
+ OnLobbyError?.Invoke("Failed to join Steam lobby.");
+ return null;
+ }
+
+ Lobby lobby = joined.Value;
+ bool useRelay = ReadBool(lobby.GetData(BasisSteamLobbyMetadata.UseRelay), BasisSteamBootstrap.ActiveSettings == null || BasisSteamBootstrap.ActiveSettings.UseRelayByDefault);
+ ApplyState(lobby, lobby.Owner.Id == SteamClient.SteamId, useRelay);
+ return CloneState();
+ }
+
+ public static async Task> QueryLobbiesAsync(int maxResults = 30)
+ {
+ if (!EnsureReady())
+ {
+ return Array.Empty();
+ }
+
+ Lobby[] lobbies = await SteamMatchmaking.LobbyList
+ .WithKeyValue(BasisSteamLobbyMetadata.Transport, NetworkTransportType.Steam.ToString())
+ .WithMaxResults(maxResults)
+ .RequestAsync();
+
+ if (lobbies == null || lobbies.Length == 0)
+ {
+ return Array.Empty();
+ }
+
+ List results = new List(lobbies.Length);
+ for (int index = 0; index < lobbies.Length; index++)
+ {
+ Lobby lobby = lobbies[index];
+ BasisSteamLobbyState item = new BasisSteamLobbyState
+ {
+ LobbyId = lobby.Id,
+ HostSteamId = ParseUlong(lobby.GetData(BasisSteamLobbyMetadata.HostSteamId)),
+ LobbyName = lobby.GetData(BasisSteamLobbyMetadata.Name),
+ WorldUrl = lobby.GetData(BasisSteamLobbyMetadata.WorldUrl),
+ WorldName = lobby.GetData(BasisSteamLobbyMetadata.WorldName),
+ VirtualPort = ParseInt(lobby.GetData(BasisSteamLobbyMetadata.VirtualPort)),
+ UseRelay = ReadBool(lobby.GetData(BasisSteamLobbyMetadata.UseRelay), true),
+ IsHost = false
+ };
+ results.Add(item);
+ }
+
+ return results;
+ }
+
+ public static void LeaveLobby()
+ {
+ if (State.LobbyId != 0)
+ {
+ if (currentLobby.HasValue)
+ {
+ Lobby lobby = currentLobby.Value;
+ lobby.Leave();
+ }
+ }
+
+ currentLobby = null;
+ BasisNetworkManagement.Instance?.ClearPendingSteamWorld();
+ State.Reset();
+ OnLobbyStateChanged?.Invoke(CloneState());
+ }
+
+ public static bool OpenInviteOverlay()
+ {
+ if (!EnsureReady())
+ {
+ return false;
+ }
+
+ if (State.LobbyId == 0)
+ {
+ OnLobbyError?.Invoke("Create or join a Steam lobby first.");
+ return false;
+ }
+
+ SteamFriends.OpenGameInviteOverlay((SteamId)State.LobbyId);
+ return true;
+ }
+
+ public static bool TrySetHostSteamId(ulong hostSteamId)
+ {
+ if (State.LobbyId == 0)
+ {
+ return false;
+ }
+
+ if (!currentLobby.HasValue)
+ {
+ return false;
+ }
+
+ Lobby lobby = currentLobby.Value;
+ lobby.SetData(BasisSteamLobbyMetadata.HostSteamId, hostSteamId.ToString(CultureInfo.InvariantCulture));
+ currentLobby = lobby;
+ State.HostSteamId = hostSteamId;
+ OnLobbyStateChanged?.Invoke(CloneState());
+ return true;
+ }
+
+ public static bool TrySetUseRelay(bool useRelay)
+ {
+ if (State.LobbyId == 0)
+ {
+ return false;
+ }
+
+ if (!currentLobby.HasValue)
+ {
+ return false;
+ }
+
+ Lobby lobby = currentLobby.Value;
+ lobby.SetData(BasisSteamLobbyMetadata.UseRelay, useRelay ? "1" : "0");
+ currentLobby = lobby;
+ State.UseRelay = useRelay;
+ OnLobbyStateChanged?.Invoke(CloneState());
+ return true;
+ }
+
+ public static void HandleSteamShutdown()
+ {
+ if (steamCallbacksSubscribed)
+ {
+ SteamFriends.OnGameLobbyJoinRequested -= HandleGameLobbyJoinRequested;
+ SteamMatchmaking.OnLobbyDataChanged -= HandleLobbyDataChanged;
+ SteamMatchmaking.OnLobbyMemberLeave -= HandleLobbyMemberLeave;
+ SteamMatchmaking.OnLobbyMemberDisconnected -= HandleLobbyMemberDisconnected;
+ steamCallbacksSubscribed = false;
+ }
+
+ currentLobby = null;
+ State.Reset();
+ }
+
+ private static void ApplyState(Lobby lobby, bool isHost, bool useRelay)
+ {
+ currentLobby = lobby;
+ State.LobbyId = lobby.Id;
+ State.HostSteamId = ParseUlong(lobby.GetData(BasisSteamLobbyMetadata.HostSteamId));
+ State.LobbyName = lobby.GetData(BasisSteamLobbyMetadata.Name);
+ State.WorldUrl = lobby.GetData(BasisSteamLobbyMetadata.WorldUrl);
+ State.WorldName = lobby.GetData(BasisSteamLobbyMetadata.WorldName);
+ State.VirtualPort = ParseInt(lobby.GetData(BasisSteamLobbyMetadata.VirtualPort));
+ State.UseRelay = useRelay;
+ State.IsHost = isHost;
+ OnLobbyStateChanged?.Invoke(CloneState());
+ }
+
+ private static BasisSteamLobbyState CloneState()
+ {
+ return new BasisSteamLobbyState
+ {
+ LobbyId = State.LobbyId,
+ HostSteamId = State.HostSteamId,
+ LobbyName = State.LobbyName,
+ WorldUrl = State.WorldUrl,
+ WorldName = State.WorldName,
+ VirtualPort = State.VirtualPort,
+ UseRelay = State.UseRelay,
+ IsHost = State.IsHost
+ };
+ }
+
+ private static int ParseInt(string value)
+ {
+ return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed) ? parsed : 0;
+ }
+
+ private static ulong ParseUlong(string value)
+ {
+ return ulong.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong parsed) ? parsed : 0;
+ }
+
+ private static bool ReadBool(string value, bool defaultValue)
+ {
+ if (string.IsNullOrEmpty(value))
+ {
+ return defaultValue;
+ }
+
+ return value == "1" || string.Equals(value, "true", StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static void SubscribeToSteamCallbacks()
+ {
+ if (steamCallbacksSubscribed)
+ {
+ return;
+ }
+
+ steamCallbacksSubscribed = true;
+ SteamFriends.OnGameLobbyJoinRequested += HandleGameLobbyJoinRequested;
+ SteamMatchmaking.OnLobbyDataChanged += HandleLobbyDataChanged;
+ SteamMatchmaking.OnLobbyMemberLeave += HandleLobbyMemberLeave;
+ SteamMatchmaking.OnLobbyMemberDisconnected += HandleLobbyMemberDisconnected;
+ }
+
+ private static void HandleGameLobbyJoinRequested(Lobby lobby, SteamId steamId)
+ {
+ OnLobbyJoinRequested?.Invoke(lobby.Id);
+ }
+
+ private static void HandleLobbyDataChanged(Lobby lobby)
+ {
+ if (State.LobbyId == 0 || lobby.Id != State.LobbyId)
+ {
+ return;
+ }
+
+ bool useRelay = ReadBool(lobby.GetData(BasisSteamLobbyMetadata.UseRelay), State.UseRelay);
+ bool isHost = lobby.Owner.Id == SteamClient.SteamId;
+ ApplyState(lobby, isHost, useRelay);
+ }
+
+ private static void HandleLobbyMemberLeave(Lobby lobby, Friend member)
+ {
+ HandleLobbyMemberRemoved(lobby, member);
+ }
+
+ private static void HandleLobbyMemberDisconnected(Lobby lobby, Friend member)
+ {
+ HandleLobbyMemberRemoved(lobby, member);
+ }
+
+ private static void HandleLobbyMemberRemoved(Lobby lobby, Friend member)
+ {
+ if (State.LobbyId == 0 || lobby.Id != State.LobbyId || State.IsHost)
+ {
+ return;
+ }
+
+ if ((ulong)member.Id != State.HostSteamId)
+ {
+ return;
+ }
+
+ OnLobbyError?.Invoke("Steam lobby host left the lobby.");
+ LeaveLobby();
+ }
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyService.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyService.cs.meta
new file mode 100644
index 0000000000..e17827defe
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyService.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 91aa6330d12333349b20c8fb5d6d1c69
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyState.cs b/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyState.cs
new file mode 100644
index 0000000000..37c622a76f
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyState.cs
@@ -0,0 +1,29 @@
+using System;
+
+namespace Basis.Scripts.Networking.Steam
+{
+ [Serializable]
+ public class BasisSteamLobbyState
+ {
+ public ulong LobbyId;
+ public ulong HostSteamId;
+ public string LobbyName = string.Empty;
+ public string WorldUrl = string.Empty;
+ public string WorldName = string.Empty;
+ public int VirtualPort;
+ public bool UseRelay = true;
+ public bool IsHost;
+
+ public void Reset()
+ {
+ LobbyId = 0;
+ HostSteamId = 0;
+ LobbyName = string.Empty;
+ WorldUrl = string.Empty;
+ WorldName = string.Empty;
+ VirtualPort = 0;
+ UseRelay = true;
+ IsHost = false;
+ }
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyState.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyState.cs.meta
new file mode 100644
index 0000000000..1f62770e18
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyState.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: ab24bbd9b3acfe042b89c05ea0dacf8f
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins.meta
new file mode 100644
index 0000000000..089271ad81
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins.meta
@@ -0,0 +1,3 @@
+fileFormatVersion: 2
+guid: 4adc4770ed774b759872a4022bd8481a
+folderAsset: yes
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks.meta
new file mode 100644
index 0000000000..45ac43d9c5
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 204da666754934a4fb320acf8cdacdbb
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Posix.dll b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Posix.dll
new file mode 100644
index 0000000000..a7dfeb153e
Binary files /dev/null and b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Posix.dll differ
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Posix.dll.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Posix.dll.meta
new file mode 100644
index 0000000000..5624e8a149
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Posix.dll.meta
@@ -0,0 +1,81 @@
+fileFormatVersion: 2
+guid: fc89a528dd38bd04a90af929e9c0f80e
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ '': Any
+ second:
+ enabled: 0
+ settings:
+ Exclude Editor: 0
+ Exclude Linux64: 0
+ Exclude OSXUniversal: 0
+ Exclude Win: 1
+ Exclude Win64: 1
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ DefaultValueInitialized: true
+ OS: OSX
+ - first:
+ Facebook: Win
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Facebook: Win64
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: Linux64
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ - first:
+ Standalone: OSXUniversal
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ - first:
+ Standalone: Win
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: Win64
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Windows Store Apps: WindowsStoreApps
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Win32.dll b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Win32.dll
new file mode 100644
index 0000000000..2a7061b2f2
Binary files /dev/null and b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Win32.dll differ
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Win32.dll.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Win32.dll.meta
new file mode 100644
index 0000000000..528dce97af
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Win32.dll.meta
@@ -0,0 +1,81 @@
+fileFormatVersion: 2
+guid: fb41692bc4208c0449c96c0576331408
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ '': Any
+ second:
+ enabled: 0
+ settings:
+ Exclude Editor: 0
+ Exclude Linux64: 1
+ Exclude OSXUniversal: 1
+ Exclude Win: 0
+ Exclude Win64: 1
+ - first:
+ Any:
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 1
+ settings:
+ CPU: x86
+ DefaultValueInitialized: true
+ OS: Windows
+ - first:
+ Facebook: Win
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ - first:
+ Facebook: Win64
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: Linux64
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: OSXUniversal
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: Win
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ - first:
+ Standalone: Win64
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Windows Store Apps: WindowsStoreApps
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Win64.dll b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Win64.dll
new file mode 100644
index 0000000000..87da6b0369
Binary files /dev/null and b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Win64.dll differ
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Win64.dll.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Win64.dll.meta
new file mode 100644
index 0000000000..1d4a147261
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/Facepunch.Steamworks.Win64.dll.meta
@@ -0,0 +1,95 @@
+fileFormatVersion: 2
+guid: b3ad7ccc15f481747842885a21b7b4ab
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ '': Any
+ second:
+ enabled: 0
+ settings:
+ Exclude Editor: 0
+ Exclude Linux: 1
+ Exclude Linux64: 1
+ Exclude LinuxUniversal: 1
+ Exclude OSXUniversal: 1
+ Exclude Win: 1
+ Exclude Win64: 0
+ - first:
+ Any:
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 1
+ settings:
+ CPU: x86_64
+ DefaultValueInitialized: true
+ OS: Windows
+ - first:
+ Facebook: Win
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Facebook: Win64
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ - first:
+ Standalone: Linux
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: Linux64
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: LinuxUniversal
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: OSXUniversal
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: Win
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: Win64
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ - first:
+ Windows Store Apps: WindowsStoreApps
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin.meta
new file mode 100644
index 0000000000..ff3620d725
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 9eb418beccc204946862a1a8f099ec39
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux32.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux32.meta
new file mode 100644
index 0000000000..3cb3eed16d
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux32.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ce9561d2de976e74684ab44c5fec0813
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux32/libsteam_api.so b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux32/libsteam_api.so
new file mode 100644
index 0000000000..e6a45351fa
Binary files /dev/null and b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux32/libsteam_api.so differ
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux32/libsteam_api.so.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux32/libsteam_api.so.meta
new file mode 100644
index 0000000000..06f72fcac2
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux32/libsteam_api.so.meta
@@ -0,0 +1,89 @@
+fileFormatVersion: 2
+guid: fd99b19e202e95a44ace17e10bac2feb
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ '': Any
+ second:
+ enabled: 0
+ settings:
+ Exclude Editor: 1
+ Exclude Linux: 1
+ Exclude Linux64: 1
+ Exclude LinuxUniversal: 1
+ Exclude OSXUniversal: 1
+ Exclude Win: 1
+ Exclude Win64: 1
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ DefaultValueInitialized: true
+ OS: AnyOS
+ - first:
+ Facebook: Win
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ - first:
+ Facebook: Win64
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ - first:
+ Standalone: Linux
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: Linux64
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: LinuxUniversal
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: OSXUniversal
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: Win
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ - first:
+ Standalone: Win64
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux64.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux64.meta
new file mode 100644
index 0000000000..379a114bac
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux64.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 2b478e6d3d1ef9848b43453c8e68cd0d
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux64/libsteam_api.so b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux64/libsteam_api.so
new file mode 100644
index 0000000000..59831c8f05
Binary files /dev/null and b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux64/libsteam_api.so differ
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux64/libsteam_api.so.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux64/libsteam_api.so.meta
new file mode 100644
index 0000000000..6f3929688e
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/linux64/libsteam_api.so.meta
@@ -0,0 +1,89 @@
+fileFormatVersion: 2
+guid: a3b75fd2a03fb3149b60c2040555c3fe
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ '': Any
+ second:
+ enabled: 0
+ settings:
+ Exclude Editor: 0
+ Exclude Linux: 1
+ Exclude Linux64: 0
+ Exclude LinuxUniversal: 0
+ Exclude OSXUniversal: 1
+ Exclude Win: 0
+ Exclude Win64: 0
+ - first:
+ Any:
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 1
+ settings:
+ CPU: x86_64
+ DefaultValueInitialized: true
+ OS: Linux
+ - first:
+ Facebook: Win
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ - first:
+ Facebook: Win64
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ - first:
+ Standalone: Linux
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: Linux64
+ second:
+ enabled: 1
+ settings:
+ CPU: x86_64
+ - first:
+ Standalone: LinuxUniversal
+ second:
+ enabled: 1
+ settings:
+ CPU: x86_64
+ - first:
+ Standalone: OSXUniversal
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: Win
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ - first:
+ Standalone: Win64
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/osx.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/osx.meta
new file mode 100644
index 0000000000..b7a374f550
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/osx.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 93319165ca0834f41b428adbdad19105
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/osx/libsteam_api.dylib b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/osx/libsteam_api.dylib
new file mode 100644
index 0000000000..a3df927328
Binary files /dev/null and b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/osx/libsteam_api.dylib differ
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/osx/libsteam_api.dylib.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/osx/libsteam_api.dylib.meta
new file mode 100644
index 0000000000..e89a0cf63f
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/osx/libsteam_api.dylib.meta
@@ -0,0 +1,63 @@
+fileFormatVersion: 2
+guid: 7d6647fb9d80f5b4f9b2ff1378756bee
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ : Any
+ second:
+ enabled: 0
+ settings:
+ Exclude Editor: 0
+ Exclude Linux64: 1
+ Exclude OSXUniversal: 0
+ Exclude Win: 1
+ Exclude Win64: 1
+ - first:
+ Any:
+ second:
+ enabled: 0
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ DefaultValueInitialized: true
+ OS: AnyOS
+ - first:
+ Standalone: Linux64
+ second:
+ enabled: 0
+ settings:
+ CPU: x86_64
+ - first:
+ Standalone: OSXUniversal
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ - first:
+ Standalone: Win
+ second:
+ enabled: 0
+ settings:
+ CPU: x86
+ - first:
+ Standalone: Win64
+ second:
+ enabled: 0
+ settings:
+ CPU: x86_64
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/steam_api.dll b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/steam_api.dll
new file mode 100644
index 0000000000..a05e4454a5
Binary files /dev/null and b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/steam_api.dll differ
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/steam_api.dll.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/steam_api.dll.meta
new file mode 100644
index 0000000000..01e2b52c15
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/steam_api.dll.meta
@@ -0,0 +1,89 @@
+fileFormatVersion: 2
+guid: f47308500f9b7734392a75ff281c7457
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ '': Any
+ second:
+ enabled: 0
+ settings:
+ Exclude Editor: 0
+ Exclude Linux: 0
+ Exclude Linux64: 0
+ Exclude LinuxUniversal: 0
+ Exclude OSXUniversal: 0
+ Exclude Win: 0
+ Exclude Win64: 1
+ - first:
+ Any:
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 1
+ settings:
+ CPU: x86
+ DefaultValueInitialized: true
+ OS: Windows
+ - first:
+ Facebook: Win
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ - first:
+ Facebook: Win64
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: Linux
+ second:
+ enabled: 1
+ settings:
+ CPU: x86
+ - first:
+ Standalone: Linux64
+ second:
+ enabled: 1
+ settings:
+ CPU: x86_64
+ - first:
+ Standalone: LinuxUniversal
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ - first:
+ Standalone: OSXUniversal
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ - first:
+ Standalone: Win
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ - first:
+ Standalone: Win64
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/steam_api.lib b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/steam_api.lib
new file mode 100644
index 0000000000..b7f71e369a
Binary files /dev/null and b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/steam_api.lib differ
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/steam_api.lib.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/steam_api.lib.meta
new file mode 100644
index 0000000000..03c736abc2
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/steam_api.lib.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 3ffd5813d91aefd459583d77d2e49ddd
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/win64.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/win64.meta
new file mode 100644
index 0000000000..8f9709dad6
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/win64.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 4080c4017456bde44a6f4b5915b8d27c
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/win64/steam_api64.dll b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/win64/steam_api64.dll
new file mode 100644
index 0000000000..0224579a13
Binary files /dev/null and b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/win64/steam_api64.dll differ
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/win64/steam_api64.dll.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/win64/steam_api64.dll.meta
new file mode 100644
index 0000000000..1aaa061d16
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/win64/steam_api64.dll.meta
@@ -0,0 +1,89 @@
+fileFormatVersion: 2
+guid: cf5718c4ee1c31e458f8a58a77f4eef0
+PluginImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ iconMap: {}
+ executionOrder: {}
+ defineConstraints: []
+ isPreloaded: 0
+ isOverridable: 0
+ isExplicitlyReferenced: 0
+ validateReferences: 1
+ platformData:
+ - first:
+ '': Any
+ second:
+ enabled: 0
+ settings:
+ Exclude Editor: 0
+ Exclude Linux: 0
+ Exclude Linux64: 0
+ Exclude LinuxUniversal: 0
+ Exclude OSXUniversal: 0
+ Exclude Win: 1
+ Exclude Win64: 0
+ - first:
+ Any:
+ second:
+ enabled: 1
+ settings: {}
+ - first:
+ Editor: Editor
+ second:
+ enabled: 1
+ settings:
+ CPU: x86_64
+ DefaultValueInitialized: true
+ OS: AnyOS
+ - first:
+ Facebook: Win
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Facebook: Win64
+ second:
+ enabled: 0
+ settings:
+ CPU: AnyCPU
+ - first:
+ Standalone: Linux
+ second:
+ enabled: 1
+ settings:
+ CPU: x86
+ - first:
+ Standalone: Linux64
+ second:
+ enabled: 1
+ settings:
+ CPU: x86_64
+ - first:
+ Standalone: LinuxUniversal
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ - first:
+ Standalone: OSXUniversal
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ - first:
+ Standalone: Win
+ second:
+ enabled: 0
+ settings:
+ CPU: None
+ - first:
+ Standalone: Win64
+ second:
+ enabled: 1
+ settings:
+ CPU: AnyCPU
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/win64/steam_api64.lib b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/win64/steam_api64.lib
new file mode 100644
index 0000000000..4ddda84c5f
Binary files /dev/null and b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/win64/steam_api64.lib differ
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/win64/steam_api64.lib.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/win64/steam_api64.lib.meta
new file mode 100644
index 0000000000..f8aa2b40aa
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Plugins/Facepunch.Steamworks/redistributable_bin/win64/steam_api64.lib.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: b7f47a56d1502a54aac85b9fadc6741e
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Settings.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Settings.meta
new file mode 100644
index 0000000000..0e2915dece
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Settings.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6637b26fb366ed243b64b5ec9db8bc36
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Settings/BasisSteamSettings.cs b/Basis/Packages/com.basis.steamtransport/Runtime/Settings/BasisSteamSettings.cs
new file mode 100644
index 0000000000..0d5bac7ead
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Settings/BasisSteamSettings.cs
@@ -0,0 +1,30 @@
+using UnityEngine;
+
+namespace Basis.Scripts.Networking.Steam
+{
+ [CreateAssetMenu(fileName = "BasisSteamSettings", menuName = "Basis/Steam Settings")]
+ public class BasisSteamSettings : ScriptableObject
+ {
+ public const string DefaultResourcesPath = "BasisSteamSettings";
+ public const int ValidatedDefaultMaxLobbyMembers = 32;
+
+ [Header("App")]
+ public uint AppId = 480;
+
+ [Header("Initialization")]
+ public bool RestartAppIfNecessary = false;
+ public bool AutoInitialize = true;
+ public bool RunCallbacksManually = true;
+
+ [Header("Lobby Defaults")]
+ [Tooltip("Validated lobby size. Increase only after load testing.")]
+ [Range(2, 250)]
+ public int DefaultMaxLobbyMembers = ValidatedDefaultMaxLobbyMembers;
+ public int RelayVirtualPort = 0;
+ public bool UseRelayByDefault = true;
+ public bool CreateFriendsOnlyByDefault = true;
+
+ [Header("Debug")]
+ public bool EnableTransportTrace = false;
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Settings/BasisSteamSettings.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Settings/BasisSteamSettings.cs.meta
new file mode 100644
index 0000000000..fda844ebd3
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Settings/BasisSteamSettings.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: af5c7611f1a59aa4c9d5fa9a73f0f90b
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Transport.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Transport.meta
new file mode 100644
index 0000000000..eb06c18dba
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Transport.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 785a7ee223db39743a8b03defa419f7f
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportCore.asmdef b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportCore.asmdef
new file mode 100644
index 0000000000..f01846bcd8
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportCore.asmdef
@@ -0,0 +1,17 @@
+{
+ "name": "BasisSteamTransportCore",
+ "rootNamespace": "",
+ "references": [
+ "BasisNetworkCore",
+ "BasisDebug"
+ ],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": true,
+ "defineConstraints": [],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportCore.asmdef.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportCore.asmdef.meta
new file mode 100644
index 0000000000..da7a57ae3b
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportCore.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: bd1d939e6c7500e49adaefc7fc924406
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportMetrics.cs b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportMetrics.cs
new file mode 100644
index 0000000000..2fbb9e9fb0
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportMetrics.cs
@@ -0,0 +1,235 @@
+using System.Diagnostics;
+using System.Threading;
+
+namespace Basis.Scripts.Networking.Steam
+{
+ public readonly struct BasisSteamTransportMetricsSnapshot
+ {
+ public readonly long ReceivePollCount;
+ public readonly long ReceiveMessageCount;
+ public readonly long ReceiveBudgetUsed;
+ public readonly long ReceiveBudgetCapacity;
+ public readonly long ReceiveMessageBudgetHits;
+ public readonly long ReceiveTimeBudgetHits;
+ public readonly int CurrentPendingConnections;
+ public readonly int PeakPendingConnections;
+ public readonly long SendFailureCount;
+ public readonly long SentPacketsTransient;
+ public readonly long SentPacketsControl;
+ public readonly long SentPacketsResource;
+ public readonly long SentBytesTransient;
+ public readonly long SentBytesControl;
+ public readonly long SentBytesResource;
+ public readonly long ReceivedPacketsTransient;
+ public readonly long ReceivedPacketsControl;
+ public readonly long ReceivedPacketsResource;
+ public readonly long ReceivedBytesTransient;
+ public readonly long ReceivedBytesControl;
+ public readonly long ReceivedBytesResource;
+
+ public BasisSteamTransportMetricsSnapshot(
+ long receivePollCount,
+ long receiveMessageCount,
+ long receiveBudgetUsed,
+ long receiveBudgetCapacity,
+ long receiveMessageBudgetHits,
+ long receiveTimeBudgetHits,
+ int currentPendingConnections,
+ int peakPendingConnections,
+ long sendFailureCount,
+ long sentPacketsTransient,
+ long sentPacketsControl,
+ long sentPacketsResource,
+ long sentBytesTransient,
+ long sentBytesControl,
+ long sentBytesResource,
+ long receivedPacketsTransient,
+ long receivedPacketsControl,
+ long receivedPacketsResource,
+ long receivedBytesTransient,
+ long receivedBytesControl,
+ long receivedBytesResource)
+ {
+ ReceivePollCount = receivePollCount;
+ ReceiveMessageCount = receiveMessageCount;
+ ReceiveBudgetUsed = receiveBudgetUsed;
+ ReceiveBudgetCapacity = receiveBudgetCapacity;
+ ReceiveMessageBudgetHits = receiveMessageBudgetHits;
+ ReceiveTimeBudgetHits = receiveTimeBudgetHits;
+ CurrentPendingConnections = currentPendingConnections;
+ PeakPendingConnections = peakPendingConnections;
+ SendFailureCount = sendFailureCount;
+ SentPacketsTransient = sentPacketsTransient;
+ SentPacketsControl = sentPacketsControl;
+ SentPacketsResource = sentPacketsResource;
+ SentBytesTransient = sentBytesTransient;
+ SentBytesControl = sentBytesControl;
+ SentBytesResource = sentBytesResource;
+ ReceivedPacketsTransient = receivedPacketsTransient;
+ ReceivedPacketsControl = receivedPacketsControl;
+ ReceivedPacketsResource = receivedPacketsResource;
+ ReceivedBytesTransient = receivedBytesTransient;
+ ReceivedBytesControl = receivedBytesControl;
+ ReceivedBytesResource = receivedBytesResource;
+ }
+ }
+
+ public static class BasisSteamTransportMetrics
+ {
+ private static long receivePollCount;
+ private static long receiveMessageCount;
+ private static long receiveBudgetUsed;
+ private static long receiveBudgetCapacity;
+ private static long receiveMessageBudgetHits;
+ private static long receiveTimeBudgetHits;
+ private static int currentPendingConnections;
+ private static int peakPendingConnections;
+ private static long sendFailureCount;
+ private static long sentPacketsTransient;
+ private static long sentPacketsControl;
+ private static long sentPacketsResource;
+ private static long sentBytesTransient;
+ private static long sentBytesControl;
+ private static long sentBytesResource;
+ private static long receivedPacketsTransient;
+ private static long receivedPacketsControl;
+ private static long receivedPacketsResource;
+ private static long receivedBytesTransient;
+ private static long receivedBytesControl;
+ private static long receivedBytesResource;
+
+ public static BasisSteamTransportMetricsSnapshot GetSnapshot()
+ {
+ return new BasisSteamTransportMetricsSnapshot(
+ Interlocked.Read(ref receivePollCount),
+ Interlocked.Read(ref receiveMessageCount),
+ Interlocked.Read(ref receiveBudgetUsed),
+ Interlocked.Read(ref receiveBudgetCapacity),
+ Interlocked.Read(ref receiveMessageBudgetHits),
+ Interlocked.Read(ref receiveTimeBudgetHits),
+ Volatile.Read(ref currentPendingConnections),
+ Volatile.Read(ref peakPendingConnections),
+ Interlocked.Read(ref sendFailureCount),
+ Interlocked.Read(ref sentPacketsTransient),
+ Interlocked.Read(ref sentPacketsControl),
+ Interlocked.Read(ref sentPacketsResource),
+ Interlocked.Read(ref sentBytesTransient),
+ Interlocked.Read(ref sentBytesControl),
+ Interlocked.Read(ref sentBytesResource),
+ Interlocked.Read(ref receivedPacketsTransient),
+ Interlocked.Read(ref receivedPacketsControl),
+ Interlocked.Read(ref receivedPacketsResource),
+ Interlocked.Read(ref receivedBytesTransient),
+ Interlocked.Read(ref receivedBytesControl),
+ Interlocked.Read(ref receivedBytesResource));
+ }
+
+ [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")]
+ public static void Reset()
+ {
+ Interlocked.Exchange(ref receivePollCount, 0);
+ Interlocked.Exchange(ref receiveMessageCount, 0);
+ Interlocked.Exchange(ref receiveBudgetUsed, 0);
+ Interlocked.Exchange(ref receiveBudgetCapacity, 0);
+ Interlocked.Exchange(ref receiveMessageBudgetHits, 0);
+ Interlocked.Exchange(ref receiveTimeBudgetHits, 0);
+ Interlocked.Exchange(ref currentPendingConnections, 0);
+ Interlocked.Exchange(ref peakPendingConnections, 0);
+ Interlocked.Exchange(ref sendFailureCount, 0);
+ Interlocked.Exchange(ref sentPacketsTransient, 0);
+ Interlocked.Exchange(ref sentPacketsControl, 0);
+ Interlocked.Exchange(ref sentPacketsResource, 0);
+ Interlocked.Exchange(ref sentBytesTransient, 0);
+ Interlocked.Exchange(ref sentBytesControl, 0);
+ Interlocked.Exchange(ref sentBytesResource, 0);
+ Interlocked.Exchange(ref receivedPacketsTransient, 0);
+ Interlocked.Exchange(ref receivedPacketsControl, 0);
+ Interlocked.Exchange(ref receivedPacketsResource, 0);
+ Interlocked.Exchange(ref receivedBytesTransient, 0);
+ Interlocked.Exchange(ref receivedBytesControl, 0);
+ Interlocked.Exchange(ref receivedBytesResource, 0);
+ }
+
+ [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")]
+ public static void RecordReceivePoll(int processedMessages, int budgetCapacity, bool hitMessageBudget, bool hitTimeBudget)
+ {
+ Interlocked.Increment(ref receivePollCount);
+ Interlocked.Add(ref receiveMessageCount, processedMessages);
+ Interlocked.Add(ref receiveBudgetUsed, processedMessages);
+ Interlocked.Add(ref receiveBudgetCapacity, budgetCapacity);
+
+ if (hitMessageBudget)
+ {
+ Interlocked.Increment(ref receiveMessageBudgetHits);
+ }
+
+ if (hitTimeBudget)
+ {
+ Interlocked.Increment(ref receiveTimeBudgetHits);
+ }
+ }
+
+ [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")]
+ public static void RecordPendingConnections(int count)
+ {
+ Interlocked.Exchange(ref currentPendingConnections, count);
+
+ int observedPeak;
+ do
+ {
+ observedPeak = Volatile.Read(ref peakPendingConnections);
+ if (count <= observedPeak)
+ {
+ return;
+ }
+ }
+ while (Interlocked.CompareExchange(ref peakPendingConnections, count, observedPeak) != observedPeak);
+ }
+
+ [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")]
+ public static void RecordSendSuccess(byte steamLane, int bytes)
+ {
+ switch (steamLane)
+ {
+ case 0:
+ Interlocked.Increment(ref sentPacketsTransient);
+ Interlocked.Add(ref sentBytesTransient, bytes);
+ break;
+ case 1:
+ Interlocked.Increment(ref sentPacketsControl);
+ Interlocked.Add(ref sentBytesControl, bytes);
+ break;
+ default:
+ Interlocked.Increment(ref sentPacketsResource);
+ Interlocked.Add(ref sentBytesResource, bytes);
+ break;
+ }
+ }
+
+ [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")]
+ public static void RecordReceiveSuccess(int steamLane, int bytes)
+ {
+ switch (steamLane)
+ {
+ case 0:
+ Interlocked.Increment(ref receivedPacketsTransient);
+ Interlocked.Add(ref receivedBytesTransient, bytes);
+ break;
+ case 1:
+ Interlocked.Increment(ref receivedPacketsControl);
+ Interlocked.Add(ref receivedBytesControl, bytes);
+ break;
+ default:
+ Interlocked.Increment(ref receivedPacketsResource);
+ Interlocked.Add(ref receivedBytesResource, bytes);
+ break;
+ }
+ }
+
+ [Conditional("UNITY_EDITOR"), Conditional("DEVELOPMENT_BUILD")]
+ public static void RecordSendFailure()
+ {
+ Interlocked.Increment(ref sendFailureCount);
+ }
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportMetrics.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportMetrics.cs.meta
new file mode 100644
index 0000000000..707b6ed261
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportMetrics.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 8d0f03cfe0884b549ea0e7f35a3794d8
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportTrace.cs b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportTrace.cs
new file mode 100644
index 0000000000..5357dac462
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportTrace.cs
@@ -0,0 +1,192 @@
+using System;
+using System.Collections.Concurrent;
+using System.IO;
+using System.Text;
+using System.Threading;
+using UnityEngine;
+
+namespace Basis.Scripts.Networking.Steam
+{
+ public static class BasisSteamTransportTrace
+ {
+ private const int MaxQueuedLines = 4096;
+ private const int FlushBatchSize = 512;
+ private const int FlushThreshold = 128;
+ private static readonly object sync = new object();
+ private static readonly ConcurrentQueue pendingLines = new ConcurrentQueue();
+ private static readonly StringBuilder flushBuilder = new StringBuilder(16 * 1024);
+ private static string logPath;
+ private static DateTime nextFlushUtc = DateTime.MinValue;
+ private static int queuedLineCount;
+ private static int droppedLineCount;
+ public static bool Enabled { get; private set; }
+
+ public static string LogPath
+ {
+ get
+ {
+ if (string.IsNullOrWhiteSpace(logPath))
+ {
+ logPath = Path.Combine(Application.persistentDataPath, "BasisSteamTransport.log");
+ }
+
+ return logPath;
+ }
+ }
+
+ public static void Clear()
+ {
+ if (!Enabled)
+ {
+ return;
+ }
+
+ try
+ {
+ ResetQueueState();
+ lock (sync)
+ {
+ File.WriteAllText(LogPath, $"[{DateTime.UtcNow:O}] BasisSteamTransport log start{Environment.NewLine}");
+ }
+ }
+ catch (Exception ex)
+ {
+ BasisDebug.LogError($"[BasisSteamTransportTrace] Failed to clear log: {ex.Message}", BasisDebug.LogTag.Networking);
+ }
+ }
+
+ public static void Configure(bool enabled)
+ {
+ if (!enabled && Enabled)
+ {
+ FlushPending(force: true);
+ ResetQueueState();
+ }
+
+ Enabled = enabled;
+ nextFlushUtc = DateTime.UtcNow;
+ }
+
+ public static void Log(string message)
+ {
+ Write("INFO", message);
+ }
+
+ public static void Warn(string message)
+ {
+ Write("WARN", message);
+ }
+
+ public static void Error(string message)
+ {
+ Write("ERROR", message);
+ }
+
+ public static void FlushPending(bool force = false)
+ {
+ if (!Enabled && !force)
+ {
+ return;
+ }
+
+ if (Volatile.Read(ref queuedLineCount) == 0 && Volatile.Read(ref droppedLineCount) == 0)
+ {
+ return;
+ }
+
+ DateTime now = DateTime.UtcNow;
+ if (!force && Volatile.Read(ref queuedLineCount) < FlushThreshold && now < nextFlushUtc)
+ {
+ return;
+ }
+
+ try
+ {
+ lock (sync)
+ {
+ if (!force && Volatile.Read(ref queuedLineCount) < FlushThreshold && DateTime.UtcNow < nextFlushUtc)
+ {
+ return;
+ }
+
+ int linesToFlush = force ? int.MaxValue : FlushBatchSize;
+ StringBuilder builder = flushBuilder;
+ builder.Clear();
+ int flushedCount = 0;
+
+ while (flushedCount < linesToFlush && pendingLines.TryDequeue(out string line))
+ {
+ builder.Append(line);
+ flushedCount++;
+ }
+
+ if (flushedCount > 0)
+ {
+ Interlocked.Add(ref queuedLineCount, -flushedCount);
+ }
+
+ int droppedCount = Interlocked.Exchange(ref droppedLineCount, 0);
+ if (droppedCount > 0)
+ {
+ builder.Append('[')
+ .Append(DateTime.UtcNow.ToString("O"))
+ .Append("] [WARN] Dropped ")
+ .Append(droppedCount)
+ .Append(" trace lines because the queue was full.")
+ .Append(Environment.NewLine);
+ }
+
+ if (builder.Length == 0)
+ {
+ nextFlushUtc = DateTime.UtcNow.AddMilliseconds(250);
+ return;
+ }
+
+ File.AppendAllText(LogPath, builder.ToString());
+ nextFlushUtc = DateTime.UtcNow.AddMilliseconds(250);
+ }
+ }
+ catch (Exception ex)
+ {
+ BasisDebug.LogError($"[BasisSteamTransportTrace] Failed to flush log: {ex.Message}", BasisDebug.LogTag.Networking);
+ }
+ }
+
+ private static void Write(string level, string message)
+ {
+ if (!Enabled)
+ {
+ return;
+ }
+
+ try
+ {
+ string line = $"[{DateTime.UtcNow:O}] [{level}] {message}{Environment.NewLine}";
+ int nextCount = Interlocked.Increment(ref queuedLineCount);
+ if (nextCount > MaxQueuedLines)
+ {
+ Interlocked.Decrement(ref queuedLineCount);
+ Interlocked.Increment(ref droppedLineCount);
+ return;
+ }
+
+ pendingLines.Enqueue(line);
+ }
+ catch (Exception ex)
+ {
+ BasisDebug.LogError($"[BasisSteamTransportTrace] Failed to write log: {ex.Message}", BasisDebug.LogTag.Networking);
+ }
+ }
+
+ private static void ResetQueueState()
+ {
+ while (pendingLines.TryDequeue(out _))
+ {
+ }
+
+ Interlocked.Exchange(ref queuedLineCount, 0);
+ Interlocked.Exchange(ref droppedLineCount, 0);
+ nextFlushUtc = DateTime.UtcNow;
+ }
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportTrace.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportTrace.cs.meta
new file mode 100644
index 0000000000..0c1eeac6a4
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamTransportTrace.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d1e44f71d6d808042a813e319f2538db
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisTransportFactory.cs b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisTransportFactory.cs
new file mode 100644
index 0000000000..31333bff1d
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisTransportFactory.cs
@@ -0,0 +1,19 @@
+using Basis.Network.Core;
+
+namespace Basis.Scripts.Networking.Steam
+{
+ public static class BasisTransportFactory
+ {
+ public static NetManager Create(EventBasedNetListener listener, Configuration configuration)
+ {
+ switch (configuration.TransportType)
+ {
+ case NetworkTransportType.Steam:
+ return new SteamNetManager(listener, configuration);
+ case NetworkTransportType.LiteNetLib:
+ default:
+ return new LNLNetManager(listener, configuration);
+ }
+ }
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisTransportFactory.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisTransportFactory.cs.meta
new file mode 100644
index 0000000000..fe75253f00
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisTransportFactory.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: abf7420a067f70140b7f00624fe89fa8
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Transport/SteamNetManager.cs b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/SteamNetManager.cs
new file mode 100644
index 0000000000..031854db65
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/SteamNetManager.cs
@@ -0,0 +1,1058 @@
+using Basis.Network.Core;
+using Steamworks;
+using Steamworks.Data;
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Net;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Basis.Scripts.Networking.Steam
+{
+ internal enum SteamTransportPacketType : byte
+ {
+ Application = 0,
+ ConnectRequest = 1,
+ AssignPeer = 2,
+ }
+
+ internal sealed class SteamPendingConnection
+ {
+ public Connection Connection;
+ public string Identity;
+ public bool IsResolved;
+ public SteamNetPeer Peer;
+ public DateTime CreatedUtc;
+ public DateTime LastActivityUtc;
+ }
+
+ internal sealed class SteamConnectionRequest : ConnectionRequest
+ {
+ private readonly SteamNetManager owner;
+ private readonly SteamPendingConnection pendingConnection;
+ private readonly NetDataReader data;
+
+ public SteamConnectionRequest(SteamNetManager owner, SteamPendingConnection pendingConnection, byte[] connectPayload)
+ {
+ this.owner = owner;
+ this.pendingConnection = pendingConnection;
+ data = new NetDataReader(connectPayload);
+ }
+
+ public NetDataReader Data => data;
+
+ public IPEndPoint RemoteEndPoint => new IPEndPoint(IPAddress.None, 0);
+
+ public string Identity => pendingConnection.Identity;
+
+ public NetPeer Accept()
+ {
+ return owner.AcceptPendingConnection(pendingConnection);
+ }
+
+ public void Reject(NetDataWriter w)
+ {
+ owner.RejectPendingConnection(pendingConnection, w);
+ }
+ }
+
+ internal sealed class SteamNetPeer : NetPeer
+ {
+ private readonly SteamNetManager owner;
+ private Connection connection;
+ private string identity;
+ private int id;
+ private int remoteId;
+ private DateTime lastPacketUtc = DateTime.UtcNow;
+
+ public SteamNetPeer(SteamNetManager owner, Connection connection, int id, int remoteId, string identity)
+ {
+ this.owner = owner;
+ this.connection = connection;
+ this.id = id;
+ this.remoteId = remoteId;
+ this.identity = identity ?? string.Empty;
+ }
+
+ public void UpdateAssignedRemoteId(int assignedRemoteId)
+ {
+ remoteId = assignedRemoteId;
+ if (id == 0)
+ {
+ id = assignedRemoteId;
+ }
+ }
+
+ public void UpdateConnection(Connection updatedConnection, string updatedIdentity)
+ {
+ connection = updatedConnection;
+ if (!string.IsNullOrWhiteSpace(updatedIdentity))
+ {
+ identity = updatedIdentity;
+ }
+ }
+
+ public void MarkPacketReceived()
+ {
+ lastPacketUtc = DateTime.UtcNow;
+ }
+
+ public int Id => id;
+
+ public IPAddress Address => IPAddress.None;
+
+ public string Identity => identity;
+
+ public int RemoteId => remoteId;
+
+ public int RoundTripTime
+ {
+ get
+ {
+ try
+ {
+ return connection.QuickStatus().Ping;
+ }
+ catch (Exception ex)
+ {
+ BasisSteamTransportTrace.Error($"RoundTripTime failed connectionId={connection.Id} {ex}");
+ return 0;
+ }
+ }
+ }
+
+ public float TimeSinceLastPacket => (float)(DateTime.UtcNow - lastPacketUtc).TotalSeconds;
+
+ public long RemoteTimeDelta => 0;
+
+ public int Mtu => 1200;
+
+ public void Disconnect()
+ {
+ connection.Close(false, 0, "Disconnected");
+ }
+
+ public void Disconnect(byte[] b)
+ {
+ connection.Close(false, 0, b == null || b.Length == 0 ? "Disconnected" : Encoding.UTF8.GetString(b));
+ }
+
+ public void DisconnectForce()
+ {
+ connection.Close(true, 0, "ForceDisconnect");
+ }
+
+ public void Send(byte[] data, byte channelNumber, DeliveryMethod deliveryMethod)
+ {
+ owner.SendApplicationMessage(connection, data, 0, data.Length, channelNumber, deliveryMethod);
+ }
+
+ public void Send(NetDataWriter data, byte channelNumber, DeliveryMethod deliveryMethod)
+ {
+ owner.SendApplicationMessage(connection, data.Data, 0, data.Length, channelNumber, deliveryMethod);
+ }
+
+ public void SendUnreliableRawMerge(byte[] data, int offset, int length, byte channelNumber, int patchOffset = -1, byte patchValue = 0)
+ {
+ if (data == null || length <= 0)
+ {
+ BasisSteamTransportTrace.Error($"SendUnreliableRawMerge called with invalid data={data != null} length={length}");
+ return;
+ }
+
+ if (patchOffset < 0 || patchOffset >= length)
+ {
+ owner.SendApplicationMessage(connection, data, offset, length, channelNumber, DeliveryMethod.Unreliable);
+ return;
+ }
+
+ byte[] patchedData = ArrayPool.Shared.Rent(length > 0 ? length : 1);
+ try
+ {
+ Buffer.BlockCopy(data, offset, patchedData, 0, length);
+ patchedData[patchOffset] = patchValue;
+ owner.SendApplicationMessage(connection, patchedData, 0, length, channelNumber, DeliveryMethod.Unreliable);
+ }
+ finally
+ {
+ ArrayPool.Shared.Return(patchedData);
+ }
+ }
+
+ public int GetPacketsCountInQueue(byte channel, DeliveryMethod deliveryMethod)
+ {
+ try
+ {
+ ConnectionStatus status = connection.QuickStatus();
+ return deliveryMethod == DeliveryMethod.Unreliable || deliveryMethod == DeliveryMethod.Sequenced
+ ? status.PendingUnreliable
+ : status.PendingReliable;
+ }
+ catch (Exception ex)
+ {
+ BasisSteamTransportTrace.Error($"GetPacketsCountInQueue failed connectionId={connection.Id} delivery={deliveryMethod} {ex}");
+ return 0;
+ }
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is SteamNetPeer other && other.connection == connection;
+ }
+
+ public override int GetHashCode()
+ {
+ return connection.Id.GetHashCode();
+ }
+ }
+
+ internal sealed class SteamServerSocketManager : SocketManager
+ {
+ public SteamNetManager Owner;
+
+ public override void OnConnecting(Connection connection, ConnectionInfo info)
+ {
+ connection.Accept();
+ Owner.RegisterPendingConnection(connection, info);
+ }
+
+ public override void OnMessage(Connection connection, NetIdentity identity, IntPtr data, int size, long messageNum, long recvTime, int channel)
+ {
+ Owner.HandleServerMessage(connection, identity, data, size, channel);
+ }
+
+ public override void OnDisconnected(Connection connection, ConnectionInfo info)
+ {
+ Owner.HandleServerDisconnected(connection, info);
+ connection.Close(false, 0, "Disconnected");
+ }
+ }
+
+ internal sealed class SteamClientConnectionManager : ConnectionManager
+ {
+ public SteamNetManager Owner;
+ public SteamNetPeer LocalPeer;
+ public byte[] ConnectPayload;
+ public bool HasAssignedPeerId;
+
+ public override void OnConnected(ConnectionInfo info)
+ {
+ Owner.ConfigureConnectionLanes(Connection, "ClientConnected");
+ if (ConnectPayload != null && ConnectPayload.Length > 0)
+ {
+ Owner.SendConnectRequest(Connection, ConnectPayload);
+ }
+ }
+
+ public override void OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel)
+ {
+ Owner.HandleClientMessage(this, data, size, channel);
+ }
+
+ public override void OnDisconnected(ConnectionInfo info)
+ {
+ Owner.HandleClientDisconnected(LocalPeer, info);
+ }
+ }
+
+ public class SteamNetManager : NetManager, IDisposable
+ {
+ private const int MaxAllocatedPeerId = ushort.MaxValue;
+ private const byte SteamTransientLane = 0;
+ private const byte SteamControlLane = 1;
+ private const byte SteamResourceLane = 2;
+ private const int MaxPendingConnections = 64;
+ private const double PendingConnectionTimeoutSeconds = 10.0d;
+ private const double PendingConnectionSweepIntervalSeconds = 1.0d;
+ private const int ReceiveBatchSize = 64;
+ private const int MaxMessagesPerPoll = 512;
+ private const double MaxReceivePollMilliseconds = 2.0d;
+ private static readonly double StopwatchTicksToMilliseconds = 1000d / Stopwatch.Frequency;
+ private static readonly int[] LanePriorities = { 10, 10, 10 };
+ private static readonly ushort[] LaneWeights = { 6, 3, 1 };
+ private static readonly ArrayPool PacketBufferPool = ArrayPool.Shared;
+ private static readonly List activeManagers = new List();
+ private static readonly object activeManagersSync = new object();
+ private static SteamNetManager[] activeManagersSnapshot = Array.Empty();
+ private readonly EventBasedNetListener listener;
+ private readonly Configuration configuration;
+ private readonly LNLNetManager fallbackManager;
+ private readonly bool useFallback;
+ private readonly NetStatistics statistics = new NetStatistics();
+ private readonly Dictionary pendingConnections = new Dictionary();
+ private readonly Dictionary peersByConnection = new Dictionary();
+ private readonly Dictionary peersById = new Dictionary();
+ private readonly List stalePendingConnections = new List();
+ private Func serverReceiveDelegate;
+ private Func clientReceiveDelegate;
+ private SteamServerSocketManager serverSocketManager;
+ private SteamClientConnectionManager clientConnectionManager;
+ private bool serverReceiveEnabled = true;
+ private bool clientReceiveEnabled = true;
+ private int nextPeerId = 1;
+ private DateTime nextPendingSweepUtc = DateTime.UtcNow;
+
+ public SteamNetManager(EventBasedNetListener listener, Configuration configuration)
+ {
+ this.listener = listener;
+ this.configuration = configuration;
+ RegisterActiveManager(this);
+
+ if (!configuration.UseSteamRelay)
+ {
+ useFallback = true;
+ fallbackManager = new LNLNetManager(listener, configuration);
+ BNL.LogWarning("Steam transport: UseSteamRelay is disabled, falling back to LiteNetLib.");
+ return;
+ }
+
+ if (!SteamClient.IsValid)
+ {
+ useFallback = true;
+ fallbackManager = new LNLNetManager(listener, configuration);
+ BNL.LogWarning("Steam transport: SteamClient is not initialized, falling back to LiteNetLib.");
+ }
+ else
+ {
+ BasisSteamTransportTrace.Log($"SteamNetManager created. LobbyId={configuration.SteamLobbyId} HostSteamId={configuration.SteamHostSteamId} VirtualPort={configuration.SteamVirtualPort}");
+ }
+ }
+
+ public void Start(IPAddress IPv4Address, IPAddress IPv6Address, int SetPort)
+ {
+ if (useFallback)
+ {
+ fallbackManager.Start(IPv4Address, IPv6Address, SetPort);
+ return;
+ }
+
+ if (SetPort > 0)
+ {
+ serverSocketManager = SteamNetworkingSockets.CreateRelaySocket(configuration.SteamVirtualPort);
+ serverSocketManager.Owner = this;
+ serverReceiveDelegate = serverSocketManager.Receive;
+ serverReceiveEnabled = serverSocketManager != null;
+ BasisSteamTransportTrace.Log($"CreateRelaySocket virtualPort={configuration.SteamVirtualPort} setPort={SetPort}");
+ }
+ }
+
+ public void Stop()
+ {
+ if (useFallback)
+ {
+ fallbackManager.Stop();
+ ResetStatistics();
+ UnregisterActiveManager(this);
+ return;
+ }
+
+ BasisSteamTransportTrace.Log("SteamNetManager.Stop");
+
+ if (clientConnectionManager != null)
+ {
+ clientConnectionManager.Close(false, 0, "Stop");
+ clientConnectionManager = null;
+ clientReceiveDelegate = null;
+ }
+
+ if (serverSocketManager != null)
+ {
+ serverSocketManager.Close();
+ serverSocketManager = null;
+ serverReceiveDelegate = null;
+ }
+
+ pendingConnections.Clear();
+ peersByConnection.Clear();
+ peersById.Clear();
+ stalePendingConnections.Clear();
+ ResetStatistics();
+ BasisSteamTransportMetrics.RecordPendingConnections(0);
+ serverReceiveEnabled = false;
+ clientReceiveEnabled = false;
+ nextPeerId = 1;
+ UnregisterActiveManager(this);
+ }
+
+ public void Dispose()
+ {
+ Stop();
+ }
+
+ public NetPeer Connect(string sIP, int port, NetDataWriter writer)
+ {
+ if (useFallback)
+ {
+ return fallbackManager.Connect(sIP, port, writer);
+ }
+
+ if (configuration.SteamHostSteamId == 0)
+ {
+ BasisSteamTransportTrace.Error("ConnectRelay aborted: SteamHostSteamId was 0.");
+ return null;
+ }
+
+ byte[] connectPayload = new byte[writer.Length];
+ Buffer.BlockCopy(writer.Data, 0, connectPayload, 0, writer.Length);
+
+ SteamNetPeer peer = new SteamNetPeer(this, default, 0, 0, configuration.SteamHostSteamId.ToString());
+ clientConnectionManager = SteamNetworkingSockets.ConnectRelay((SteamId)configuration.SteamHostSteamId, configuration.SteamVirtualPort);
+ clientConnectionManager.Owner = this;
+ clientConnectionManager.LocalPeer = peer;
+ clientConnectionManager.ConnectPayload = connectPayload;
+ clientReceiveDelegate = clientConnectionManager.Receive;
+ clientReceiveEnabled = clientConnectionManager != null;
+ BasisSteamTransportTrace.Log($"ConnectRelay hostSteamId={configuration.SteamHostSteamId} virtualPort={configuration.SteamVirtualPort} payloadBytes={connectPayload.Length}");
+ return peer;
+ }
+
+ public bool SendUnconnectedMessage(NetDataWriter writer, IPEndPoint remoteEndPoint)
+ {
+ if (useFallback)
+ {
+ return fallbackManager.SendUnconnectedMessage(writer, remoteEndPoint);
+ }
+
+ return false;
+ }
+
+ public void PollEvents()
+ {
+ if (useFallback)
+ {
+ fallbackManager.PollEvents();
+ return;
+ }
+
+ SweepPendingConnectionsIfNeeded();
+
+ if (serverReceiveEnabled && serverSocketManager != null && serverReceiveDelegate != null)
+ {
+ try
+ {
+ DrainReceiveQueue(serverReceiveDelegate);
+ }
+ catch (Exception ex)
+ {
+ serverReceiveEnabled = false;
+ BasisSteamTransportTrace.Error($"Server receive failed, disabling: {ex}");
+ try { serverSocketManager.Close(); }
+ catch (Exception closeEx) { BasisSteamTransportTrace.Error($"Server socket close also failed: {closeEx.Message}"); }
+ serverSocketManager = null;
+ serverReceiveDelegate = null;
+ }
+ }
+
+ if (clientReceiveEnabled && clientConnectionManager != null && clientReceiveDelegate != null)
+ {
+ try
+ {
+ DrainReceiveQueue(clientReceiveDelegate);
+ }
+ catch (Exception ex)
+ {
+ clientReceiveEnabled = false;
+ BasisSteamTransportTrace.Error($"Client receive failed, disabling: {ex}");
+ try { clientConnectionManager.Close(false, 0, "ReceiveException"); }
+ catch (Exception closeEx) { BasisSteamTransportTrace.Error($"Client connection close also failed: {closeEx.Message}"); }
+ clientConnectionManager = null;
+ clientReceiveDelegate = null;
+ }
+ }
+ }
+
+ private static void VerifyReceiveSignature()
+ {
+#if UNITY_EDITOR || DEVELOPMENT_BUILD
+ System.Reflection.MethodInfo connectionReceive = typeof(ConnectionManager).GetMethod("Receive", new[] { typeof(int), typeof(bool) });
+ System.Reflection.MethodInfo socketReceive = typeof(SocketManager).GetMethod("Receive", new[] { typeof(int), typeof(bool) });
+ if (connectionReceive == null || connectionReceive.ReturnType != typeof(int))
+ {
+ BasisDebug.LogError("Facepunch ConnectionManager.Receive must return int for the bounded drain strategy.", BasisDebug.LogTag.Networking);
+ }
+
+ if (socketReceive == null || socketReceive.ReturnType != typeof(int))
+ {
+ BasisDebug.LogError("Facepunch SocketManager.Receive must return int for the bounded drain strategy.", BasisDebug.LogTag.Networking);
+ }
+#endif
+ }
+
+ private static void DrainReceiveQueue(Func receive)
+ {
+ long startTimestamp = Stopwatch.GetTimestamp();
+ int processedTotal = 0;
+ bool hitMessageBudget = false;
+ bool hitTimeBudget = false;
+
+ while (processedTotal < MaxMessagesPerPoll)
+ {
+ int remainingBudget = MaxMessagesPerPoll - processedTotal;
+ int batchSize = remainingBudget < ReceiveBatchSize ? remainingBudget : ReceiveBatchSize;
+ int processed = receive(batchSize, false);
+
+ if (processed <= 0)
+ {
+ break;
+ }
+
+ processedTotal += processed;
+
+ if (processed < batchSize)
+ {
+ break;
+ }
+
+ if (HasExceededReceiveBudget(startTimestamp))
+ {
+ hitTimeBudget = true;
+ break;
+ }
+ }
+
+ if (processedTotal >= MaxMessagesPerPoll)
+ {
+ hitMessageBudget = true;
+ }
+
+ BasisSteamTransportMetrics.RecordReceivePoll(processedTotal, MaxMessagesPerPoll, hitMessageBudget, hitTimeBudget);
+ }
+
+ private static bool HasExceededReceiveBudget(long startTimestamp)
+ {
+ long elapsedTicks = Stopwatch.GetTimestamp() - startTimestamp;
+ double elapsedMilliseconds = elapsedTicks * StopwatchTicksToMilliseconds;
+ return elapsedMilliseconds >= MaxReceivePollMilliseconds;
+ }
+
+ public NetStatistics Statistics
+ {
+ get
+ {
+ if (useFallback)
+ {
+ return fallbackManager.Statistics;
+ }
+
+ return statistics;
+ }
+ }
+
+ public int ConnectedPeersCount
+ {
+ get
+ {
+ if (useFallback)
+ {
+ return fallbackManager.ConnectedPeersCount;
+ }
+
+ return peersById.Count;
+ }
+ }
+
+ internal void RegisterPendingConnection(Connection connection, ConnectionInfo info)
+ {
+ if (pendingConnections.Count >= MaxPendingConnections)
+ {
+ BasisSteamTransportTrace.Warn($"Pending connection limit reached. connectionId={connection.Id} limit={MaxPendingConnections}");
+ connection.Close(false, 0, "PendingLimitReached");
+ return;
+ }
+
+ ConfigureConnectionLanes(connection, "ServerPendingConnection");
+ DateTime now = DateTime.UtcNow;
+ BasisSteamTransportTrace.Log($"RegisterPendingConnection connectionId={connection.Id} identity={info.Identity} state={info.State} endReason={info.EndReason}");
+ pendingConnections[connection.Id] = new SteamPendingConnection
+ {
+ Connection = connection,
+ Identity = info.Identity.ToString(),
+ IsResolved = false,
+ CreatedUtc = now,
+ LastActivityUtc = now,
+ };
+ BasisSteamTransportMetrics.RecordPendingConnections(pendingConnections.Count);
+ }
+
+ internal void HandleServerMessage(Connection connection, NetIdentity identity, IntPtr data, int size, int channel)
+ {
+ byte[] managedData = CopyToPooledBuffer(data, size);
+ statistics.BytesReceived += size;
+ statistics.PacketsReceived++;
+ BasisSteamTransportMetrics.RecordReceiveSuccess(channel, size);
+ bool returnBuffer = true;
+
+ try
+ {
+ if (pendingConnections.TryGetValue(connection.Id, out SteamPendingConnection pending) && !pending.IsResolved)
+ {
+ pending.LastActivityUtc = DateTime.UtcNow;
+ HandlePendingConnectMessage(pending, managedData, size);
+ return;
+ }
+
+ if (!peersByConnection.TryGetValue(connection.Id, out SteamNetPeer peer))
+ {
+ BasisSteamTransportTrace.Error($"HandleServerMessage from unknown connectionId={connection.Id}, closing");
+ connection.Close(true, 0, "UnknownPeer");
+ return;
+ }
+
+ peer.MarkPacketReceived();
+ returnBuffer = false;
+ HandleApplicationMessage(peer, managedData, size, channel, true);
+ }
+ finally
+ {
+ if (returnBuffer)
+ {
+ ReturnPacketBuffer(managedData);
+ }
+ }
+ }
+
+ internal void HandleServerDisconnected(Connection connection, ConnectionInfo info)
+ {
+ BasisSteamTransportTrace.Warn($"HandleServerDisconnected connectionId={connection.Id} state={info.State} endReason={info.EndReason}");
+ if (pendingConnections.Remove(connection.Id))
+ {
+ BasisSteamTransportMetrics.RecordPendingConnections(pendingConnections.Count);
+ return;
+ }
+
+ if (!peersByConnection.TryGetValue(connection.Id, out SteamNetPeer peer))
+ {
+ BasisSteamTransportTrace.Error($"HandleServerDisconnected: unknown connectionId={connection.Id}, not in pending or active peers");
+ return;
+ }
+
+ peersByConnection.Remove(connection.Id);
+ peersById.Remove(peer.Id);
+ listener.RaisePeerDisconnected(peer, MapDisconnectInfo(info));
+ }
+
+ internal NetPeer AcceptPendingConnection(SteamPendingConnection pendingConnection)
+ {
+ if (pendingConnection.IsResolved && pendingConnection.Peer != null)
+ {
+ return pendingConnection.Peer;
+ }
+
+ int assignedPeerId = AllocatePeerId();
+ SteamNetPeer peer = new SteamNetPeer(this, pendingConnection.Connection, assignedPeerId, assignedPeerId, pendingConnection.Identity);
+ pendingConnection.IsResolved = true;
+ pendingConnection.Peer = peer;
+ pendingConnections.Remove(pendingConnection.Connection.Id);
+ BasisSteamTransportMetrics.RecordPendingConnections(pendingConnections.Count);
+ peersByConnection[pendingConnection.Connection.Id] = peer;
+ peersById[assignedPeerId] = peer;
+
+ BasisSteamTransportTrace.Log($"AcceptPendingConnection connectionId={pendingConnection.Connection.Id} assignedPeerId={assignedPeerId} identity={pendingConnection.Identity}");
+ SendAssignPeerId(pendingConnection.Connection, assignedPeerId);
+ listener.RaisePeerConnected(peer);
+ return peer;
+ }
+
+ internal void RejectPendingConnection(SteamPendingConnection pendingConnection, NetDataWriter writer)
+ {
+ pendingConnection.IsResolved = true;
+ pendingConnections.Remove(pendingConnection.Connection.Id);
+ BasisSteamTransportMetrics.RecordPendingConnections(pendingConnections.Count);
+ BasisSteamTransportTrace.Warn($"RejectPendingConnection connectionId={pendingConnection.Connection.Id}");
+ pendingConnection.Connection.Close(false, 0, "Rejected");
+ }
+
+ internal void HandleClientMessage(SteamClientConnectionManager manager, IntPtr data, int size, int channel)
+ {
+ byte[] managedData = CopyToPooledBuffer(data, size);
+ statistics.BytesReceived += size;
+ statistics.PacketsReceived++;
+ BasisSteamTransportMetrics.RecordReceiveSuccess(channel, size);
+ bool returnBuffer = true;
+
+ if (size == 0)
+ {
+ BasisSteamTransportTrace.Error("HandleClientMessage received empty packet");
+ ReturnPacketBuffer(managedData);
+ return;
+ }
+
+ try
+ {
+ switch ((SteamTransportPacketType)managedData[0])
+ {
+ case SteamTransportPacketType.AssignPeer:
+ if (size < 3)
+ {
+ BasisSteamTransportTrace.Error($"HandleClientMessage AssignPeer packet too small size={size}");
+ return;
+ }
+
+ ushort assignedPeerId = BitConverter.ToUInt16(managedData, 1);
+ manager.LocalPeer.UpdateConnection(manager.Connection, configuration.SteamHostSteamId.ToString());
+ manager.LocalPeer.UpdateAssignedRemoteId(assignedPeerId);
+ manager.LocalPeer.MarkPacketReceived();
+ BasisSteamTransportTrace.Log($"Client received AssignPeer assignedPeerId={assignedPeerId}");
+
+ if (!manager.HasAssignedPeerId)
+ {
+ manager.HasAssignedPeerId = true;
+ listener.RaisePeerConnected(manager.LocalPeer);
+ }
+ break;
+
+ case SteamTransportPacketType.Application:
+ manager.LocalPeer.MarkPacketReceived();
+ returnBuffer = false;
+ HandleApplicationMessage(manager.LocalPeer, managedData, size, channel, true);
+ break;
+ }
+ }
+ finally
+ {
+ if (returnBuffer)
+ {
+ ReturnPacketBuffer(managedData);
+ }
+ }
+ }
+
+ internal void HandleClientDisconnected(SteamNetPeer peer, ConnectionInfo info)
+ {
+ BasisSteamTransportTrace.Warn($"HandleClientDisconnected state={info.State} endReason={info.EndReason}");
+ listener.RaisePeerDisconnected(peer, MapDisconnectInfo(info));
+ }
+
+ internal void SendConnectRequest(Connection connection, byte[] connectPayload)
+ {
+ int packetLength = connectPayload.Length + 1;
+ byte[] packet = RentPacketBuffer(packetLength);
+ packet[0] = (byte)SteamTransportPacketType.ConnectRequest;
+ Buffer.BlockCopy(connectPayload, 0, packet, 1, connectPayload.Length);
+ BasisSteamTransportTrace.Log($"SendConnectRequest payloadBytes={connectPayload.Length}");
+ SendPacket(connection, packet, 0, packetLength, SteamControlLane, DeliveryMethod.ReliableOrdered, true);
+ }
+
+ internal void SendApplicationMessage(Connection connection, byte[] data, int offset, int length, byte channel, DeliveryMethod deliveryMethod)
+ {
+ int packetLength = length + 3;
+ byte[] packet = RentPacketBuffer(packetLength);
+ packet[0] = (byte)SteamTransportPacketType.Application;
+ packet[1] = (byte)deliveryMethod;
+ packet[2] = channel;
+ Buffer.BlockCopy(data, offset, packet, 3, length);
+ SendPacket(connection, packet, 0, packetLength, GetSteamLane(channel), deliveryMethod, true);
+ }
+
+ private void HandlePendingConnectMessage(SteamPendingConnection pendingConnection, byte[] managedData, int dataSize)
+ {
+ if (dataSize < 2 || (SteamTransportPacketType)managedData[0] != SteamTransportPacketType.ConnectRequest)
+ {
+ BasisSteamTransportTrace.Error($"Invalid connect request packet. size={dataSize}");
+ pendingConnection.Connection.Close(true, 0, "InvalidConnectRequest");
+ pendingConnections.Remove(pendingConnection.Connection.Id);
+ return;
+ }
+
+ byte[] payload = new byte[dataSize - 1];
+ Buffer.BlockCopy(managedData, 1, payload, 0, payload.Length);
+ BasisSteamTransportTrace.Log($"HandlePendingConnectMessage connectionId={pendingConnection.Connection.Id} payloadBytes={payload.Length}");
+ listener.RaiseConnectionRequest(new SteamConnectionRequest(this, pendingConnection, payload));
+ }
+
+ private void HandleApplicationMessage(SteamNetPeer peer, byte[] managedData, int dataSize, int channel, bool pooledBuffer)
+ {
+ if (dataSize < 3 || (SteamTransportPacketType)managedData[0] != SteamTransportPacketType.Application)
+ {
+ BasisSteamTransportTrace.Error($"HandleApplicationMessage invalid packet size={dataSize} type={(dataSize > 0 ? managedData[0] : -1)}");
+ if (pooledBuffer)
+ {
+ ReturnPacketBuffer(managedData);
+ }
+ return;
+ }
+
+ DeliveryMethod deliveryMethod = (DeliveryMethod)managedData[1];
+ byte basisChannel = managedData[2];
+ Action recycle = pooledBuffer ? () => ReturnPacketBuffer(managedData) : null;
+ NetPacketReader reader = NetPacketReader.Create(managedData, 3, dataSize, recycle);
+ try
+ {
+ listener.RaiseNetworkReceive(peer, reader, basisChannel, deliveryMethod);
+ }
+ catch (Exception ex)
+ {
+ BasisSteamTransportTrace.Error($"HandleApplicationMessage dispatch failed channel={basisChannel} delivery={deliveryMethod} {ex}");
+ reader.Recycle(true);
+ throw;
+ }
+ }
+
+ private void SendAssignPeerId(Connection connection, int assignedPeerId)
+ {
+ byte[] packet = new byte[3];
+ packet[0] = (byte)SteamTransportPacketType.AssignPeer;
+ packet[1] = (byte)assignedPeerId;
+ packet[2] = (byte)(assignedPeerId >> 8);
+ BasisSteamTransportTrace.Log($"SendAssignPeerId assignedPeerId={assignedPeerId}");
+ SendPacket(connection, packet, 0, packet.Length, SteamControlLane, DeliveryMethod.ReliableOrdered);
+ }
+
+ private void SendPacket(Connection connection, byte[] packet, int offset, int length, byte steamLane, DeliveryMethod deliveryMethod, bool returnToPool = false)
+ {
+ try
+ {
+ Result result = connection.SendMessage(packet, offset, length, MapSendType(deliveryMethod, steamLane), steamLane);
+ if (result == Result.OK)
+ {
+ statistics.BytesSent += length;
+ statistics.PacketsSent++;
+ BasisSteamTransportMetrics.RecordSendSuccess(steamLane, length);
+ }
+ else
+ {
+ BasisSteamTransportMetrics.RecordSendFailure();
+ BasisSteamTransportTrace.Error($"SendPacket failed result={result} connectionId={connection.Id} packetBytes={length} steamLane={steamLane} delivery={deliveryMethod}");
+ }
+ }
+ finally
+ {
+ if (returnToPool)
+ {
+ ReturnPacketBuffer(packet);
+ }
+ }
+ }
+
+ private static byte[] RentPacketBuffer(int size)
+ {
+ return PacketBufferPool.Rent(size > 0 ? size : 1);
+ }
+
+ private static byte[] CopyToPooledBuffer(IntPtr data, int size)
+ {
+ byte[] buffer = RentPacketBuffer(size);
+ if (size > 0)
+ {
+ Marshal.Copy(data, buffer, 0, size);
+ }
+
+ return buffer;
+ }
+
+ private static void ReturnPacketBuffer(byte[] buffer)
+ {
+ if (buffer != null)
+ {
+ PacketBufferPool.Return(buffer);
+ }
+ }
+
+ private void SweepPendingConnectionsIfNeeded()
+ {
+ if (pendingConnections.Count == 0)
+ {
+ nextPendingSweepUtc = DateTime.UtcNow.AddSeconds(PendingConnectionSweepIntervalSeconds);
+ return;
+ }
+
+ DateTime now = DateTime.UtcNow;
+ if (now < nextPendingSweepUtc)
+ {
+ return;
+ }
+
+ nextPendingSweepUtc = now.AddSeconds(PendingConnectionSweepIntervalSeconds);
+ stalePendingConnections.Clear();
+
+ foreach (SteamPendingConnection pendingConnection in pendingConnections.Values)
+ {
+ if (pendingConnection.IsResolved)
+ {
+ continue;
+ }
+
+ if ((now - pendingConnection.LastActivityUtc).TotalSeconds >= PendingConnectionTimeoutSeconds)
+ {
+ stalePendingConnections.Add(pendingConnection);
+ }
+ }
+
+ for (int index = 0; index < stalePendingConnections.Count; index++)
+ {
+ SteamPendingConnection pendingConnection = stalePendingConnections[index];
+ pendingConnections.Remove(pendingConnection.Connection.Id);
+ BasisSteamTransportTrace.Warn($"Pending connection timed out. connectionId={pendingConnection.Connection.Id} identity={pendingConnection.Identity} timeoutSeconds={PendingConnectionTimeoutSeconds}");
+ pendingConnection.Connection.Close(false, 0, "PendingTimeout");
+ }
+
+ stalePendingConnections.Clear();
+ BasisSteamTransportMetrics.RecordPendingConnections(pendingConnections.Count);
+ }
+
+ internal void ConfigureConnectionLanes(Connection connection, string context)
+ {
+ Result result = connection.ConfigureConnectionLanes(LanePriorities, LaneWeights);
+ if (result == Result.OK)
+ {
+ BasisSteamTransportTrace.Log($"ConfigureConnectionLanes context={context} connectionId={connection.Id} lanes=3");
+ }
+ else
+ {
+ BasisSteamTransportTrace.Error($"ConfigureConnectionLanes failed context={context} connectionId={connection.Id} result={result}");
+ }
+ }
+
+ private static byte GetSteamLane(byte basisChannel)
+ {
+ if (IsTransientBasisChannel(basisChannel))
+ {
+ return SteamTransientLane;
+ }
+
+ if (IsResourceBasisChannel(basisChannel))
+ {
+ return SteamResourceLane;
+ }
+
+ return SteamControlLane;
+ }
+
+ private static bool IsTransientBasisChannel(byte basisChannel)
+ {
+ return basisChannel == BasisNetworkCommons.VoiceChannel
+ || basisChannel == BasisNetworkCommons.ShoutVoiceChannel
+ || basisChannel == BasisNetworkCommons.AvatarChannel
+ || basisChannel == BasisNetworkCommons.CameraPIPPositionChannel
+ || (basisChannel >= BasisNetworkCommons.PlayerAvatarVeryLowChannel && basisChannel <= BasisNetworkCommons.PlayerAvatarHighAdditionalChannel);
+ }
+
+ private static bool IsResourceBasisChannel(byte basisChannel)
+ {
+ return basisChannel == BasisNetworkCommons.SceneChannel
+ || basisChannel == BasisNetworkCommons.LoadResourceChannel
+ || basisChannel == BasisNetworkCommons.UnloadResourceChannel
+ || basisChannel == BasisNetworkCommons.ContentShareChannel
+ || basisChannel == BasisNetworkCommons.ContentShareCleanupChannel;
+ }
+
+ private static SendType MapSendType(DeliveryMethod deliveryMethod, byte steamLane)
+ {
+ SendType sendType = deliveryMethod switch
+ {
+ DeliveryMethod.Unreliable => SendType.Unreliable,
+ DeliveryMethod.Sequenced => SendType.Unreliable,
+ DeliveryMethod.ReliableUnordered => SendType.Reliable,
+ DeliveryMethod.ReliableOrdered => SendType.Reliable,
+ DeliveryMethod.ReliableSequenced => SendType.Reliable,
+ _ => SendType.Reliable
+ };
+
+ if ((deliveryMethod == DeliveryMethod.Unreliable || deliveryMethod == DeliveryMethod.Sequenced) && steamLane == SteamTransientLane)
+ {
+ sendType |= SendType.NoNagle;
+ }
+
+ return sendType;
+ }
+
+ private DisconnectInfo MapDisconnectInfo(ConnectionInfo info)
+ {
+ DisconnectReason reason = info.State switch
+ {
+ ConnectionState.Connected => DisconnectReason.RemoteConnectionClose,
+ ConnectionState.ClosedByPeer => DisconnectReason.RemoteConnectionClose,
+ ConnectionState.ProblemDetectedLocally => DisconnectReason.ConnectionFailed,
+ _ => DisconnectReason.ConnectionFailed
+ };
+
+ switch (info.EndReason)
+ {
+ case NetConnectionEnd.Remote_Timeout:
+ case NetConnectionEnd.Misc_Timeout:
+ reason = DisconnectReason.Timeout;
+ break;
+ case NetConnectionEnd.Misc_NoRelaySessionsToClient:
+ case NetConnectionEnd.Misc_SteamConnectivity:
+ reason = DisconnectReason.HostUnreachable;
+ break;
+ }
+
+ return new DisconnectInfo
+ {
+ Reason = reason,
+ SocketErrorCode = 0,
+ AdditionalData = null
+ };
+ }
+
+ private int AllocatePeerId()
+ {
+ if (nextPeerId < 1 || nextPeerId > MaxAllocatedPeerId)
+ {
+ nextPeerId = 1;
+ }
+
+ int startPeerId = nextPeerId;
+ int candidatePeerId = nextPeerId;
+
+ do
+ {
+ if (!peersById.ContainsKey(candidatePeerId))
+ {
+ nextPeerId = candidatePeerId >= MaxAllocatedPeerId ? 1 : candidatePeerId + 1;
+ return candidatePeerId;
+ }
+
+ candidatePeerId = candidatePeerId >= MaxAllocatedPeerId ? 1 : candidatePeerId + 1;
+ }
+ while (candidatePeerId != startPeerId);
+
+ throw new InvalidOperationException("Peer id space exhausted.");
+ }
+
+ private void ResetStatistics()
+ {
+ statistics.PacketsSent = 0;
+ statistics.PacketsReceived = 0;
+ statistics.BytesSent = 0;
+ statistics.BytesReceived = 0;
+ statistics.PacketLoss = 0;
+ }
+
+ private static void RegisterActiveManager(SteamNetManager manager)
+ {
+ VerifyReceiveSignature();
+ lock (activeManagersSync)
+ {
+ if (!activeManagers.Contains(manager))
+ {
+ activeManagers.Add(manager);
+ activeManagersSnapshot = activeManagers.ToArray();
+ }
+ }
+ }
+
+ private static void UnregisterActiveManager(SteamNetManager manager)
+ {
+ lock (activeManagersSync)
+ {
+ if (activeManagers.Remove(manager))
+ {
+ activeManagersSnapshot = activeManagers.ToArray();
+ }
+ }
+ }
+
+ public static void PollActiveManagers()
+ {
+ SteamNetManager[] managers = activeManagersSnapshot;
+ for (int index = 0; index < managers.Length; index++)
+ {
+ managers[index].PollEvents();
+ }
+ }
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Transport/SteamNetManager.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/SteamNetManager.cs.meta
new file mode 100644
index 0000000000..ee787640ba
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/SteamNetManager.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 3ec782562978ec449864e498a2487119
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/UI.meta b/Basis/Packages/com.basis.steamtransport/Runtime/UI.meta
new file mode 100644
index 0000000000..2864b3bf56
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/UI.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: ccda75d5dc0a34243a5919d8c860ecd8
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.Actions.cs b/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.Actions.cs
new file mode 100644
index 0000000000..174d1c4270
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.Actions.cs
@@ -0,0 +1,463 @@
+using Basis.BasisUI;
+using Basis.Network.Core;
+using Basis.Scripts.BasisSdk.Players;
+using Basis.Scripts.Common;
+using Basis.Scripts.Device_Management.Devices.Desktop;
+using Basis.Scripts.Drivers;
+using Basis.Scripts.Networking;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Basis.Scripts.Networking.Steam
+{
+ public partial class SteamLobbiesProvider
+ {
+ private void SyncCurrentState()
+ {
+ if (BasisNetworkManagement.Instance != null)
+ {
+ useRelayToggle.SetValueWithoutNotify(BasisNetworkManagement.Instance.UseSteamRelay);
+ }
+
+ HandleLobbyStateChanged(BasisSteamLobbyService.State);
+ RefreshSelectedLobbyDescription();
+ ApplyUiState();
+ }
+
+ private async Task OnCreateLobbyButton()
+ {
+ SetBusy(createLobbyButton, true);
+
+ try
+ {
+ if (!TryPrepareLocalPlayer(out string userName))
+ {
+ return;
+ }
+
+ infoDescriptor.SetTitle("Steam");
+ infoDescriptor.SetDescription("Validating world BEE...");
+
+ BasisSteamBeeValidationResult validation = await BasisSteamBeeValidation.ValidateWorldAsync(worldUrlField.Value, worldPasswordField.Password);
+ if (!validation.IsValid)
+ {
+ infoDescriptor.SetTitle("World Error");
+ infoDescriptor.SetDescription(validation.ErrorMessage);
+ return;
+ }
+
+ BasisSteamLobbyState lobbyState = await BasisSteamLobbyService.CreateLobbyAsync(
+ lobbyNameField.Value,
+ validation,
+ friendsOnlyToggle.Value,
+ privateLobbyToggle.Value,
+ useRelayToggle.Value);
+
+ if (lobbyState == null)
+ {
+ infoDescriptor.SetTitle("Steam");
+ infoDescriptor.SetDescription("Steam lobby creation failed.");
+ return;
+ }
+
+ if (BasisNetworkManagement.Instance == null)
+ {
+ infoDescriptor.SetTitle("Error");
+ infoDescriptor.SetDescription("BasisNetworkManagement is not available.");
+ BasisDebug.LogError("Steam CreateLobby: BasisNetworkManagement.Instance is null", BasisDebug.LogTag.Networking);
+ return;
+ }
+
+ await BasisNetworkConnection.ResetConnectionStateAsync(BasisNetworkManagement.Instance);
+
+ BasisNetworkManagement.Instance.Transport = NetworkTransportType.Steam;
+ BasisNetworkManagement.Instance.IsHostMode = true;
+ BasisNetworkManagement.Instance.UpdateSteamLobbyState(lobbyState.LobbyId, lobbyState.HostSteamId, lobbyState.UseRelay, lobbyState.VirtualPort);
+ BasisNetworkManagement.Instance.SetPendingSteamWorld(validation.WorldUrl, validation.WorldPassword, validation.WorldName);
+
+ infoDescriptor.SetTitle("Steam");
+ infoDescriptor.SetDescription("Starting local host session for the Steam lobby...");
+ BasisMainMenu.Close();
+ BasisCursorManagement.OnReset();
+ BasisNetworkManagement.Instance.Connect();
+ if (BasisDesktopEye.Instance != null)
+ {
+ BasisDesktopEye.Instance.LockEye();
+ }
+ }
+ catch (Exception ex)
+ {
+ infoDescriptor.SetTitle("Error");
+ infoDescriptor.SetDescription("Steam lobby creation failed.");
+ BasisDebug.LogError(ex.ToString(), BasisDebug.LogTag.Networking);
+ }
+ finally
+ {
+ SetBusy(createLobbyButton, false);
+ ApplyUiState();
+ }
+ }
+
+ private async Task RefreshLobbiesAsync()
+ {
+ SetBusy(refreshLobbiesButton, true);
+
+ try
+ {
+ infoDescriptor.SetTitle("Steam");
+ infoDescriptor.SetDescription("Refreshing Steam lobbies...");
+
+ IReadOnlyList lobbies = await BasisSteamLobbyService.QueryLobbiesAsync();
+ cachedLobbies.Clear();
+ cachedLobbies.AddRange(lobbies);
+
+ List entries = new List();
+ for (int index = 0; index < cachedLobbies.Count; index++)
+ {
+ BasisSteamLobbyState lobby = cachedLobbies[index];
+ string worldName = string.IsNullOrWhiteSpace(lobby.WorldName) ? "Unknown World" : lobby.WorldName;
+ string lobbyName = string.IsNullOrWhiteSpace(lobby.LobbyName) ? $"Lobby {index + 1}" : lobby.LobbyName;
+ entries.Add($"{lobbyName} | {worldName}");
+ }
+
+ if (entries.Count == 0)
+ {
+ entries.Add("No lobbies found");
+ }
+
+ if (browserGroup)
+ {
+ browserGroup.SetTitle(entries.Count == 1 && cachedLobbies.Count == 0
+ ? "Available Lobbies"
+ : $"Available Lobbies ({cachedLobbies.Count})");
+ }
+
+ lobbySelectionDropdown.AssignEntries(entries);
+ lobbySelectionDropdown.SetValueWithoutNotify(entries[0]);
+ RefreshSelectedLobbyDescription();
+ infoDescriptor.SetTitle("Steam");
+ infoDescriptor.SetDescription($"Found {cachedLobbies.Count} Steam lobbies.");
+ }
+ catch (Exception ex)
+ {
+ infoDescriptor.SetTitle("Error");
+ infoDescriptor.SetDescription("Steam lobby refresh failed.");
+ BasisDebug.LogError(ex.ToString(), BasisDebug.LogTag.Networking);
+ }
+ finally
+ {
+ SetBusy(refreshLobbiesButton, false);
+ ApplyUiState();
+ }
+ }
+
+ private async Task OnJoinLobbyButton()
+ {
+ SetBusy(joinLobbyButton, true);
+
+ try
+ {
+ if (!TryPrepareLocalPlayer(out string userName))
+ {
+ return;
+ }
+
+ BasisSteamLobbyState selectedLobby = GetSelectedLobby();
+ if (selectedLobby == null)
+ {
+ infoDescriptor.SetTitle("Steam");
+ infoDescriptor.SetDescription("Select a Steam lobby first.");
+ return;
+ }
+
+ BasisSteamLobbyState joinedLobby = await BasisSteamLobbyService.JoinLobbyAsync(selectedLobby.LobbyId);
+ if (joinedLobby == null)
+ {
+ infoDescriptor.SetTitle("Steam");
+ infoDescriptor.SetDescription("Failed to join Steam lobby.");
+ return;
+ }
+
+ if (BasisNetworkManagement.Instance == null)
+ {
+ infoDescriptor.SetTitle("Error");
+ infoDescriptor.SetDescription("BasisNetworkManagement is not available.");
+ BasisDebug.LogError("Steam JoinLobby: BasisNetworkManagement.Instance is null", BasisDebug.LogTag.Networking);
+ return;
+ }
+
+ await BasisNetworkConnection.ResetConnectionStateAsync(BasisNetworkManagement.Instance);
+
+ BasisNetworkManagement.Instance.Transport = NetworkTransportType.Steam;
+ BasisNetworkManagement.Instance.IsHostMode = false;
+ BasisNetworkManagement.Instance.UpdateSteamLobbyState(joinedLobby.LobbyId, joinedLobby.HostSteamId, joinedLobby.UseRelay, joinedLobby.VirtualPort);
+ BasisNetworkManagement.Instance.ClearPendingSteamWorld();
+
+ infoDescriptor.SetTitle("Steam");
+ infoDescriptor.SetDescription($"Connecting to Steam host {joinedLobby.HostSteamId}...");
+ BasisMainMenu.Close();
+ BasisCursorManagement.OnReset();
+ BasisNetworkManagement.Instance.Connect();
+ if (BasisDesktopEye.Instance != null)
+ {
+ BasisDesktopEye.Instance.LockEye();
+ }
+ }
+ catch (Exception ex)
+ {
+ infoDescriptor.SetTitle("Error");
+ infoDescriptor.SetDescription("Steam lobby join failed.");
+ BasisDebug.LogError(ex.ToString(), BasisDebug.LogTag.Networking);
+ }
+ finally
+ {
+ SetBusy(joinLobbyButton, false);
+ ApplyUiState();
+ }
+ }
+
+ private async void OnLeaveLobbyButton()
+ {
+ try
+ {
+ if (BasisNetworkConnection.LocalPlayerIsConnected || BasisNetworkConnection.HasActiveClient())
+ {
+ SetInfo("Disconnecting", "Disconnecting from the active Steam session...");
+ await BasisNetworkConnection.ResetConnectionStateAsync(BasisNetworkManagement.Instance);
+ }
+ }
+ catch (Exception ex)
+ {
+ BasisDebug.LogError(ex.ToString(), BasisDebug.LogTag.Networking);
+ }
+
+ BasisSteamLobbyService.LeaveLobby();
+ cachedLobbies.Clear();
+ lobbySelectionDropdown?.AssignEntries(new List { "No lobbies loaded" });
+ lobbySelectionDropdown?.SetValueWithoutNotify("No lobbies loaded");
+ SetInfo("Steam", "Left the current Steam lobby.");
+ RefreshSelectedLobbyDescription();
+ ApplyUiState();
+ }
+
+ private void OnInviteFriendsButton()
+ {
+ if (!BasisSteamLobbyService.OpenInviteOverlay())
+ {
+ return;
+ }
+
+ SetInfo("Steam", "Opened the Steam invite overlay for the current lobby.");
+ }
+
+ private void HandleLobbyStateChanged(BasisSteamLobbyState lobbyState)
+ {
+ if (currentLobbyDescriptor == null)
+ {
+ return;
+ }
+
+ if (lobbyState == null || lobbyState.LobbyId == 0)
+ {
+ currentLobbyDescriptor.SetTitle("Lobby Details");
+ currentLobbyDescriptor.SetDescription("No active Steam lobby.");
+ ApplyUiState();
+ return;
+ }
+
+ string role = lobbyState.IsHost ? "Host" : "Member";
+ string lobbyName = string.IsNullOrWhiteSpace(lobbyState.LobbyName) ? "Unnamed Lobby" : lobbyState.LobbyName;
+ string worldName = string.IsNullOrWhiteSpace(lobbyState.WorldName) ? "Unknown World" : lobbyState.WorldName;
+ currentLobbyDescriptor.SetTitle(lobbyName);
+ currentLobbyDescriptor.SetDescription($"Role: {role}\nWorld: {worldName}\nLobbyId: {lobbyState.LobbyId}\nHostSteamId: {lobbyState.HostSteamId}\nVirtualPort: {lobbyState.VirtualPort}");
+ ApplyUiState();
+ }
+
+ private void HandleLobbyError(string error)
+ {
+ if (infoDescriptor == null)
+ {
+ return;
+ }
+
+ infoDescriptor.SetTitle("Steam Error");
+ infoDescriptor.SetDescription(error);
+ }
+
+ private void RefreshSelectedLobbyDescription()
+ {
+ if (selectedLobbyDescriptor == null)
+ {
+ return;
+ }
+
+ BasisSteamLobbyState selectedLobby = GetSelectedLobby();
+ if (selectedLobby == null)
+ {
+ selectedLobbyDescriptor.SetTitle("Lobby Preview");
+ selectedLobbyDescriptor.SetDescription("Refresh Steam lobbies to inspect a world before joining.");
+ ApplyUiState();
+ return;
+ }
+
+ string lobbyName = string.IsNullOrWhiteSpace(selectedLobby.LobbyName) ? "Unnamed Lobby" : selectedLobby.LobbyName;
+ string worldName = string.IsNullOrWhiteSpace(selectedLobby.WorldName) ? "Unknown World" : selectedLobby.WorldName;
+ string worldUrl = string.IsNullOrWhiteSpace(selectedLobby.WorldUrl) ? "n/a" : selectedLobby.WorldUrl;
+ selectedLobbyDescriptor.SetTitle(lobbyName);
+ selectedLobbyDescriptor.SetDescription($"World: {worldName}\nLobbyId: {selectedLobby.LobbyId}\nHostSteamId: {selectedLobby.HostSteamId}\nVirtualPort: {selectedLobby.VirtualPort}\nWorldUrl: {worldUrl}");
+ ApplyUiState();
+ }
+
+ private BasisSteamLobbyState GetSelectedLobby()
+ {
+ if (cachedLobbies.Count == 0 || lobbySelectionDropdown == null)
+ {
+ return null;
+ }
+
+ int selectedIndex = lobbySelectionDropdown.Index;
+ if (selectedIndex < 0 || selectedIndex >= cachedLobbies.Count)
+ {
+ return null;
+ }
+
+ return cachedLobbies[selectedIndex];
+ }
+
+ private bool HasLobbySelection()
+ {
+ return GetSelectedLobby() != null;
+ }
+
+ private bool TryPrepareLocalPlayer(out string userName)
+ {
+ userName = usernameField.Value;
+ if (string.IsNullOrWhiteSpace(userName))
+ {
+ infoDescriptor.SetTitle("Error");
+ infoDescriptor.SetDescription("Display Name Was Empty");
+ return false;
+ }
+
+ BasisLocalPlayer.Instance.DisplayName = userName;
+ BasisLocalPlayer.Instance.SetSafeDisplayname();
+ BasisDataStore.SaveString(BasisLocalPlayer.Instance.DisplayName, ServersProvider.UsernameFileName);
+ return true;
+ }
+
+ private void ApplyUiState()
+ {
+ if (!HasLiveUi())
+ {
+ return;
+ }
+
+ bool hasLobby = BasisSteamLobbyService.State != null && BasisSteamLobbyService.State.LobbyId != 0;
+ bool isConnected = BasisNetworkConnection.LocalPlayerIsConnected || BasisNetworkConnection.HasActiveClient();
+
+ if (createGroup)
+ {
+ createGroup.SetActive(!hasLobby);
+ }
+
+ if (browserGroup)
+ {
+ browserGroup.SetActive(!hasLobby);
+ browserGroup.SetTitle(cachedLobbies.Count > 0 ? $"Available Lobbies ({cachedLobbies.Count})" : "Available Lobbies");
+ }
+
+ if (sessionGroup)
+ {
+ sessionGroup.SetActive(hasLobby);
+ }
+
+ SetInteractable(friendsOnlyToggle?.ToggleComponent, !hasLobby);
+ SetInteractable(privateLobbyToggle?.ToggleComponent, !hasLobby);
+ SetInteractable(useRelayToggle?.ToggleComponent, !hasLobby);
+
+ if (createLobbyButton)
+ {
+ createLobbyButton.Descriptor.SetActive(!hasLobby);
+ }
+
+ if (refreshLobbiesButton)
+ {
+ refreshLobbiesButton.Descriptor.SetActive(!hasLobby);
+ }
+
+ if (joinLobbyButton)
+ {
+ joinLobbyButton.Descriptor.SetActive(!hasLobby);
+ joinLobbyButton.Descriptor.SetDescription(HasLobbySelection()
+ ? "Join the selected Steam lobby."
+ : "Refresh Steam lobbies and select one before joining.");
+
+ if (joinLobbyButton.ButtonComponent != null)
+ {
+ joinLobbyButton.ButtonComponent.interactable = !hasLobby && HasLobbySelection();
+ }
+ }
+
+ if (leaveLobbyButton)
+ {
+ leaveLobbyButton.Descriptor.SetActive(hasLobby);
+ leaveLobbyButton.Descriptor.SetTitle(isConnected ? "Disconnect And Leave Lobby" : "Leave Current Lobby");
+ leaveLobbyButton.Descriptor.SetDescription(isConnected
+ ? "Disconnect from the active session and leave the Steam lobby."
+ : "Leave the current Steam lobby and clear its state.");
+ }
+
+ if (inviteFriendsButton)
+ {
+ inviteFriendsButton.Descriptor.SetActive(hasLobby);
+ inviteFriendsButton.Descriptor.SetTitle("Invite Friends");
+ inviteFriendsButton.Descriptor.SetDescription(hasLobby
+ ? "Open the Steam overlay invite dialog for this lobby."
+ : "Create or join a lobby before sending Steam invites.");
+
+ if (inviteFriendsButton.ButtonComponent != null)
+ {
+ inviteFriendsButton.ButtonComponent.interactable = hasLobby;
+ }
+ }
+
+ if (sessionGroup)
+ {
+ sessionGroup.SetDescription(hasLobby
+ ? (isConnected ? "You are currently inside this Steam-backed Basis session." : "You are still inside the Steam lobby, but not connected to its Basis session.")
+ : "Actions for the currently active Steam lobby.");
+ }
+ }
+
+ private static void SetBusy(PanelButton button, bool interactable)
+ {
+ if (button?.ButtonComponent != null)
+ {
+ button.ButtonComponent.interactable = !interactable;
+ }
+ }
+
+ private static void SetInteractable(UnityEngine.UI.Selectable selectable, bool interactable)
+ {
+ if (selectable != null)
+ {
+ selectable.interactable = interactable;
+ }
+ }
+
+ private void SetInfo(string title, string description)
+ {
+ if (infoDescriptor)
+ {
+ infoDescriptor.SetTitle(title);
+ infoDescriptor.SetDescription(description);
+ }
+ }
+
+ private bool HasLiveUi()
+ {
+ return infoDescriptor && currentLobbyDescriptor && selectedLobbyDescriptor;
+ }
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.Actions.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.Actions.cs.meta
new file mode 100644
index 0000000000..091cdebd9e
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.Actions.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: b48991837a184cfda21851ed65d0c8ed
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.PanelSetup.cs b/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.PanelSetup.cs
new file mode 100644
index 0000000000..8814a915f8
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.PanelSetup.cs
@@ -0,0 +1,161 @@
+using Basis.BasisUI;
+using Basis.Scripts.Common;
+using UnityEngine;
+using UnityEngine.UI;
+
+namespace Basis.Scripts.Networking.Steam
+{
+ public partial class SteamLobbiesProvider
+ {
+ private void BuildPanelContents(BasisMenuPanel panel)
+ {
+ RectTransform container = panel.Descriptor.ContentParent;
+ PanelElementDescriptor layout = PanelElementDescriptor.CreateNew(PanelElementDescriptor.ElementStyles.ScrollViewVertical, container);
+ container = layout.ContentParent;
+
+ infoDescriptor = PanelElementDescriptor.CreateNew(PanelElementDescriptor.ElementStyles.Group, container);
+ infoDescriptor.SetTitle("Steam");
+ infoDescriptor.SetDescription("Create or browse Steam lobbies for Basis sessions.");
+
+ usernameField = PanelTextField.CreateNewEntry(container);
+ usernameField.Descriptor.SetTitle("Username");
+ usernameField.SetValueWithoutNotify(BasisDataStore.LoadString(ServersProvider.UsernameFileName, string.Empty));
+
+ RectTransform lobbyColumns = BuildHorizontalRow(container);
+
+ createGroup = PanelElementDescriptor.CreateNew(PanelElementDescriptor.ElementStyles.Group, lobbyColumns);
+ ApplyColumnWeight(createGroup, 1f);
+ createGroup.SetTitle("Create Lobby");
+ createGroup.SetDescription("Host a Basis session and attach a world BEE to the lobby.");
+
+ lobbyNameField = PanelTextField.CreateNewEntry(createGroup.ContentParent);
+ lobbyNameField.Descriptor.SetTitle("Lobby Name");
+ lobbyNameField.SetValueWithoutNotify("Basis Steam Lobby");
+
+ worldUrlField = PanelTextField.CreateNewEntry(createGroup.ContentParent);
+ worldUrlField.Descriptor.SetTitle("World BEE URL");
+
+ worldPasswordField = PanelPasswordField.CreateNewEntry(createGroup.ContentParent);
+ worldPasswordField.Descriptor.SetTitle("World Password");
+
+ BasisSteamSettings settings = BasisSteamBootstrap.ActiveSettings;
+
+ useRelayToggle = PanelToggle.CreateNewEntry(createGroup.ContentParent);
+ useRelayToggle.Descriptor.SetTitle("Use Relay");
+ useRelayToggle.Descriptor.SetDescription("Prefer Steam relay for lobby sessions.");
+ useRelayToggle.SetValueWithoutNotify(settings == null || settings.UseRelayByDefault);
+
+ friendsOnlyToggle = PanelToggle.CreateNewEntry(createGroup.ContentParent);
+ friendsOnlyToggle.Descriptor.SetTitle("Friends Only");
+ friendsOnlyToggle.Descriptor.SetDescription("Only friends can discover and join.");
+ friendsOnlyToggle.SetValueWithoutNotify(settings != null && settings.CreateFriendsOnlyByDefault);
+
+ privateLobbyToggle = PanelToggle.CreateNewEntry(createGroup.ContentParent);
+ privateLobbyToggle.Descriptor.SetTitle("Private Lobby");
+ privateLobbyToggle.Descriptor.SetDescription("Create the lobby as private.");
+ privateLobbyToggle.SetValueWithoutNotify(false);
+
+ createLobbyButton = PanelButton.CreateNew(createGroup.ContentParent);
+ createLobbyButton.Descriptor.SetTitle("Create Steam Lobby");
+ createLobbyButton.Descriptor.SetHeight(80);
+ createLobbyButton.OnClicked += () => _ = OnCreateLobbyButton();
+
+ browserGroup = PanelElementDescriptor.CreateNew(PanelElementDescriptor.ElementStyles.Group, lobbyColumns);
+ ApplyColumnWeight(browserGroup, 1f);
+ browserGroup.SetTitle("Available Lobbies");
+ browserGroup.SetDescription("Refresh Steam lobby metadata and inspect the selected world.");
+
+ RectTransform lobbyActions = BuildHorizontalRow(browserGroup.ContentParent);
+
+ refreshLobbiesButton = PanelButton.CreateNew(lobbyActions);
+ refreshLobbiesButton.Descriptor.SetTitle("Refresh Lobbies");
+ ApplyButtonWeight(refreshLobbiesButton, 1f);
+ refreshLobbiesButton.OnClicked += () => _ = RefreshLobbiesAsync();
+
+ lobbySelectionDropdown = PanelDropdown.CreateNew(PanelDropdown.DropdownStyles.EntryNoLabel, browserGroup.ContentParent);
+ lobbySelectionDropdown.Descriptor.SetSize(new Vector2(60, 80));
+ lobbySelectionDropdown.AssignEntries(new System.Collections.Generic.List { "No lobbies loaded" });
+ lobbySelectionDropdown.SetValueWithoutNotify("No lobbies loaded");
+ lobbySelectionDropdown.OnValueChanged += _ => RefreshSelectedLobbyDescription();
+
+ selectedLobbyDescriptor = PanelElementDescriptor.CreateNew(PanelElementDescriptor.ElementStyles.Group, browserGroup.ContentParent);
+ selectedLobbyDescriptor.SetTitle("Lobby Preview");
+ selectedLobbyDescriptor.SetDescription("Refresh Steam lobbies to inspect a world before joining.");
+
+ joinLobbyButton = PanelButton.CreateNew(lobbyActions);
+ joinLobbyButton.Descriptor.SetTitle("Join Lobby");
+ ApplyButtonWeight(joinLobbyButton, 1f);
+ joinLobbyButton.OnClicked += () => _ = OnJoinLobbyButton();
+
+ sessionGroup = PanelElementDescriptor.CreateNew(PanelElementDescriptor.ElementStyles.Group, container);
+ sessionGroup.SetTitle("Current Session");
+ sessionGroup.SetDescription("Actions for the currently active Steam lobby.");
+
+ inviteFriendsButton = PanelButton.CreateNew(sessionGroup.ContentParent);
+ inviteFriendsButton.Descriptor.SetTitle("Invite Friends");
+ inviteFriendsButton.Descriptor.SetDescription("Open the Steam overlay invite dialog for this lobby.");
+ inviteFriendsButton.OnClicked += OnInviteFriendsButton;
+
+ leaveLobbyButton = PanelButton.CreateNew(sessionGroup.ContentParent);
+ leaveLobbyButton.Descriptor.SetTitle("Leave Current Lobby");
+ leaveLobbyButton.OnClicked += OnLeaveLobbyButton;
+
+ currentLobbyDescriptor = PanelElementDescriptor.CreateNew(PanelElementDescriptor.ElementStyles.Group, sessionGroup.ContentParent);
+ currentLobbyDescriptor.SetTitle("Lobby Details");
+ currentLobbyDescriptor.SetDescription("No Steam lobby selected.");
+ }
+
+ private static RectTransform BuildHorizontalRow(RectTransform parent)
+ {
+ GameObject rowObject = new GameObject("SteamLobbyRow", typeof(RectTransform));
+ RectTransform rowTransform = (RectTransform)rowObject.transform;
+ rowTransform.SetParent(parent, false);
+
+ rowTransform.anchorMin = new Vector2(0f, 1f);
+ rowTransform.anchorMax = new Vector2(1f, 1f);
+ rowTransform.pivot = new Vector2(0.5f, 1f);
+
+ HorizontalLayoutGroup layoutGroup = rowObject.AddComponent();
+ layoutGroup.childForceExpandWidth = true;
+ layoutGroup.childForceExpandHeight = false;
+ layoutGroup.childControlWidth = true;
+ layoutGroup.childControlHeight = true;
+ layoutGroup.spacing = 8f;
+ layoutGroup.padding = new RectOffset(0, 0, 0, 0);
+
+ ContentSizeFitter fitter = rowObject.AddComponent();
+ fitter.verticalFit = ContentSizeFitter.FitMode.PreferredSize;
+
+ LayoutElement layout = rowObject.AddComponent();
+ layout.minWidth = 0f;
+ layout.preferredWidth = 0f;
+ layout.flexibleWidth = 1f;
+
+ return rowTransform;
+ }
+
+ private static void ApplyColumnWeight(PanelElementDescriptor descriptor, float flex)
+ {
+ if (descriptor == null || descriptor.Layout == null)
+ {
+ return;
+ }
+
+ descriptor.Layout.minWidth = 0f;
+ descriptor.Layout.preferredWidth = 0f;
+ descriptor.Layout.flexibleWidth = flex;
+ }
+
+ private static void ApplyButtonWeight(PanelButton button, float flex)
+ {
+ if (button == null || button.Layout == null)
+ {
+ return;
+ }
+
+ button.Layout.minWidth = 0f;
+ button.Layout.preferredWidth = 0f;
+ button.Layout.flexibleWidth = flex;
+ }
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.PanelSetup.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.PanelSetup.cs.meta
new file mode 100644
index 0000000000..b652c295ba
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.PanelSetup.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 4a7a5ac46adc4a718dbdd5de311bf831
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.cs b/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.cs
new file mode 100644
index 0000000000..6ce481cd71
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.cs
@@ -0,0 +1,122 @@
+using Basis.BasisUI;
+using Basis.Network.Core;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace Basis.Scripts.Networking.Steam
+{
+ public partial class SteamLobbiesProvider : BasisMenuActionProvider
+ {
+ [RuntimeInitializeOnLoadMethod]
+ public static void AddToMenu()
+ {
+ BasisMenuBase.AddProvider(new SteamLobbiesProvider());
+ }
+
+ public override string Title => "Steam Lobbies";
+ public override string IconAddress => AddressableAssets.Sprites.Servers;
+ public override int Order => 2;
+ public override bool Hidden => false;
+
+ private static readonly List cachedLobbies = new List();
+
+ private PanelTextField usernameField;
+ private PanelTextField lobbyNameField;
+ private PanelTextField worldUrlField;
+ private PanelPasswordField worldPasswordField;
+ private PanelToggle friendsOnlyToggle;
+ private PanelToggle privateLobbyToggle;
+ private PanelToggle useRelayToggle;
+ private PanelElementDescriptor createGroup;
+ private PanelElementDescriptor browserGroup;
+ private PanelElementDescriptor sessionGroup;
+ private PanelButton createLobbyButton;
+ private PanelButton refreshLobbiesButton;
+ private PanelButton joinLobbyButton;
+ private PanelButton leaveLobbyButton;
+ private PanelButton inviteFriendsButton;
+ private PanelDropdown lobbySelectionDropdown;
+ private PanelElementDescriptor infoDescriptor;
+ private PanelElementDescriptor selectedLobbyDescriptor;
+ private PanelElementDescriptor currentLobbyDescriptor;
+ private bool isSubscribedToLobbyEvents;
+
+ public override void RunAction()
+ {
+ if (BasisMainMenu.ActiveMenuTitle == Title)
+ {
+ OnReleaseEvent();
+ BasisMainMenu.Instance.ActiveMenu.ReleaseInstance();
+ return;
+ }
+
+ BasisMenuPanel panel = BasisMainMenu.CreateActiveMenu(
+ BasisMenuPanel.PanelData.Standard(Title),
+ BasisMenuPanel.PanelStyles.Page,
+ this);
+ BoundButton?.BindActiveStateToAddressablesInstance(panel);
+
+ BuildPanelContents(panel);
+ SubscribeToLobbyEvents();
+ SyncCurrentState();
+
+ if (BasisSteamLobbyService.State == null || BasisSteamLobbyService.State.LobbyId == 0)
+ {
+ _ = RefreshLobbiesAsync();
+ }
+ }
+
+ public override void OnReleaseEvent()
+ {
+ UnsubscribeFromLobbyEvents();
+ ClearUiReferences();
+ }
+
+ private void SubscribeToLobbyEvents()
+ {
+ if (isSubscribedToLobbyEvents)
+ {
+ return;
+ }
+
+ BasisSteamLobbyService.OnLobbyStateChanged += HandleLobbyStateChanged;
+ BasisSteamLobbyService.OnLobbyError += HandleLobbyError;
+ isSubscribedToLobbyEvents = true;
+ }
+
+ private void UnsubscribeFromLobbyEvents()
+ {
+ if (!isSubscribedToLobbyEvents)
+ {
+ return;
+ }
+
+ BasisSteamLobbyService.OnLobbyStateChanged -= HandleLobbyStateChanged;
+ BasisSteamLobbyService.OnLobbyError -= HandleLobbyError;
+ isSubscribedToLobbyEvents = false;
+ }
+
+ private void ClearUiReferences()
+ {
+ usernameField = null;
+ lobbyNameField = null;
+ worldUrlField = null;
+ worldPasswordField = null;
+ friendsOnlyToggle = null;
+ privateLobbyToggle = null;
+ useRelayToggle = null;
+ createGroup = null;
+ browserGroup = null;
+ sessionGroup = null;
+ createLobbyButton = null;
+ refreshLobbiesButton = null;
+ joinLobbyButton = null;
+ leaveLobbyButton = null;
+ inviteFriendsButton = null;
+ lobbySelectionDropdown = null;
+ infoDescriptor = null;
+ selectedLobbyDescriptor = null;
+ currentLobbyDescriptor = null;
+ }
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.cs.meta
new file mode 100644
index 0000000000..d395c5e305
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 56c5b3110ecdfbe4b8cee20fbe4d3112
diff --git a/Basis/Packages/com.basis.steamtransport/package.json b/Basis/Packages/com.basis.steamtransport/package.json
new file mode 100644
index 0000000000..31d78199cf
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "com.basis.steamtransport",
+ "version": "0.1.0",
+ "displayName": "Basis Steam Transport",
+ "description": "Steam lobby and transport integration for Basis while preserving the existing LiteNetLib implementation as a parallel option.",
+ "unity": "6000.4",
+ "author": {
+ "name": "Basis"
+ }
+}
diff --git a/Basis/Packages/com.basis.steamtransport/package.json.meta b/Basis/Packages/com.basis.steamtransport/package.json.meta
new file mode 100644
index 0000000000..d932dea6cb
--- /dev/null
+++ b/Basis/Packages/com.basis.steamtransport/package.json.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: d941ce865af6aeb4a8f8080e7efad237
+PackageManifestImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Basis/Packages/packages-lock.json b/Basis/Packages/packages-lock.json
index bfc87183b8..c1362d4ed1 100644
--- a/Basis/Packages/packages-lock.json
+++ b/Basis/Packages/packages-lock.json
@@ -161,6 +161,12 @@
"source": "embedded",
"dependencies": {}
},
+ "com.basis.steamtransport": {
+ "version": "file:com.basis.steamtransport",
+ "depth": 0,
+ "source": "embedded",
+ "dependencies": {}
+ },
"com.basis.vehicles": {
"version": "file:com.basis.vehicles",
"depth": 0,