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,