diff --git a/Basis/Packages/com.basis.eventdriver/BasisEventDriver.cs b/Basis/Packages/com.basis.eventdriver/BasisEventDriver.cs index 84630dabd7..f784935ac6 100644 --- a/Basis/Packages/com.basis.eventdriver/BasisEventDriver.cs +++ b/Basis/Packages/com.basis.eventdriver/BasisEventDriver.cs @@ -128,6 +128,8 @@ public partial class BasisEventDriver : MonoBehaviour public static bool StateOfOnRenderBefore = false; + public static Action OnUpdate; + // ── Lifecycle ─────────────────────────────────────────────── @@ -213,6 +215,7 @@ public void Update() OSCAcquisitionServer.Simulate(); SMModuleAvatarPerformanceLimits.SimulateDebounce(); timeSinceLastUpdate += DeltaTime; + OnUpdate?.Invoke(); } /// diff --git a/Basis/Packages/com.basis.server/BasisNetworkCore/BasisNetworkShell.cs b/Basis/Packages/com.basis.server/BasisNetworkCore/BasisNetworkShell.cs index 017964c02a..fdb74c829a 100644 --- a/Basis/Packages/com.basis.server/BasisNetworkCore/BasisNetworkShell.cs +++ b/Basis/Packages/com.basis.server/BasisNetworkCore/BasisNetworkShell.cs @@ -43,6 +43,31 @@ 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) + { + NetworkReceiveEvent?.Invoke(peer, reader, channel, deliveryMethod); + } + + public void RaisePeerConnected(NetPeer peer) + { + PeerConnectedEvent?.Invoke(peer); + } + + public void RaiseNetworkReceiveUnconnected(IPEndPoint remoteEndPoint, NetPacketReader reader) + { + NetworkReceiveUnconnectedEvent?.Invoke(remoteEndPoint, reader); + } } public interface ConnectionRequest @@ -99,6 +124,10 @@ public void Start(int SetPort) public sealed partial class NetStatistics { + public NetStatistics() + { + } + public long PacketsSent; public long PacketsReceived; public long BytesSent; @@ -115,6 +144,20 @@ public partial class NetPacketReader : NetDataReader internal DeliveryMethod method; #endif + public NetPacketReader() + { + } + + public NetPacketReader(byte[] source, int offset, int maxSize, Action recycle) : base(source, offset, maxSize) + { + RecycleInternal = recycle; + } + + public static NetPacketReader Create(byte[] source, int offset, int maxSize, Action recycle) + { + return new NetPacketReader(source, offset, maxSize, recycle); + } + public void Recycle(bool IsOkTOHaveEmptyData = false) { #if UNITY_EDITOR || DEVELOPMENT_BUILD 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..43ecf6bb20 --- /dev/null +++ b/Basis/Packages/com.basis.steamtransport/Runtime/Integration/BasisSteamNetworkIntegration.cs @@ -0,0 +1,268 @@ +using Basis.BasisUI; +using Basis.Scripts.BasisSdk.Players; +using Basis.Scripts.Device_Management; +using Basis.Scripts.Device_Management.Devices.Desktop; +using Basis.Scripts.Networking; +using Basis.Scripts.Networking.NetworkedAvatar; +using System; +using System.Globalization; +using System.Threading.Tasks; +using UnityEngine; + +namespace Basis.Scripts.Networking.Steam +{ + public static class BasisSteamNetworkIntegration + { + private const ushort DefaultPort = 4296; + + private static bool isSubscribed; + private static ulong bootstrappedLobbyId; + private static ulong pendingLobbyId; + private static string pendingWorldUrl = string.Empty; + private static string pendingWorldPassword = string.Empty; + private static string pendingWorldName = string.Empty; + private static bool suppressNextLobbyLeave; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)] + private static void Initialize() + { + BasisSteamNetworkStack.Register(); + Subscribe(); + SyncLobbyStateToTransportConfig(BasisSteamLobbyService.State); + } + + private static void Subscribe() + { + if (isSubscribed) + { + return; + } + + isSubscribed = true; + BasisSteamLobbyService.OnLobbyStateChanged += SyncLobbyStateToTransportConfig; + BasisSteamLobbyService.OnLobbyJoinRequested += HandleLobbyJoinRequested; + BasisNetworkManagement.OnIstanceCreated += OnNetworkManagementCreated; + } + + private static void SubscribeNetworkEvents() + { + BasisNetworkPlayer.OnLocalPlayerJoined -= HandleLocalPlayerJoined; + BasisNetworkPlayer.OnLocalPlayerJoined += HandleLocalPlayerJoined; + BasisNetworkPlayer.OnLocalPlayerLeft -= HandleLocalPlayerLeft; + BasisNetworkPlayer.OnLocalPlayerLeft += HandleLocalPlayerLeft; + } + + private static void OnNetworkManagementCreated() + { + SyncLobbyStateToTransportConfig(BasisSteamLobbyService.State); + } + + public static void PrepareSteamConnection(BasisSteamLobbyState lobbyState, bool isHost, string worldUrl = "", string worldPassword = "", string worldName = "") + { + SyncLobbyStateToTransportConfig(lobbyState); + SubscribeNetworkEvents(); + + BasisNetworkManagement.NetworkStackId = BasisSteamNetworkStack.StackId; + BasisNetworkManagement.IsHostMode = isHost; + BasisNetworkManagement.Ip = isHost ? "localhost" : (lobbyState?.HostSteamId.ToString(CultureInfo.InvariantCulture) ?? "steam"); + if (BasisNetworkManagement.Port == 0) + { + BasisNetworkManagement.Port = DefaultPort; + } + + if (isHost) + { + SetPendingSteamWorld(lobbyState?.LobbyId ?? 0, worldUrl, worldPassword, worldName); + } + else + { + ClearPendingSteamWorld(); + } + } + + public static async Task ResetNetworkStateAsync(bool keepSteamLobby) + { + suppressNextLobbyLeave = keepSteamLobby; + try + { + if (BasisNetworkConnection.LocalPlayerIsConnected) + { + await BasisNetworkLifeCycle.Destroy(); + BasisNetworkLifeCycle.Initalize(); + return; + } + + if (!BasisNetworkManagement.IsInitialized) + { + BasisNetworkLifeCycle.Initalize(); + } + } + finally + { + suppressNextLobbyLeave = false; + } + } + + public static void SyncLobbyStateToTransportConfig(BasisSteamLobbyState lobbyState) + { + BasisSteamTransportConfig config = BasisSteamNetworkStack.GetConfig(); + if (config == null) + { + return; + } + + if (lobbyState == null || lobbyState.LobbyId == 0) + { + config.Clear(); + return; + } + + config.LobbyId = lobbyState.LobbyId; + config.HostSteamId = lobbyState.HostSteamId; + config.UseSteamRelay = lobbyState.UseRelay; + config.VirtualPort = lobbyState.VirtualPort; + } + + public static void ClearTransportState() + { + BasisSteamNetworkStack.GetConfig()?.Clear(); + if (string.Equals(BasisNetworkManagement.NetworkStackId, BasisSteamNetworkStack.StackId, StringComparison.OrdinalIgnoreCase)) + { + BasisNetworkManagement.NetworkStackId = string.Empty; + } + } + + public static void SetPendingSteamWorld(ulong lobbyId, string worldUrl, string worldPassword, string worldName) + { + pendingLobbyId = lobbyId; + pendingWorldUrl = worldUrl ?? string.Empty; + pendingWorldPassword = worldPassword ?? string.Empty; + pendingWorldName = worldName ?? string.Empty; + } + + public static void ClearPendingSteamWorld() + { + pendingLobbyId = 0; + pendingWorldUrl = string.Empty; + pendingWorldPassword = string.Empty; + pendingWorldName = string.Empty; + } + + public static bool HasPendingSteamWorld() + { + return !string.IsNullOrWhiteSpace(pendingWorldUrl) && !string.IsNullOrWhiteSpace(pendingWorldPassword); + } + + private static void HandleLocalPlayerJoined(BasisNetworkPlayer networkedPlayer, BasisLocalPlayer localPlayer) + { + BasisDeviceManagement.EnqueueOnMainThread(() => + { + if (!IsSteamStackActive()) + { + return; + } + + if (BasisSteamLobbyService.State.IsHost == false || pendingLobbyId == 0) + { + return; + } + + if (HasPendingSteamWorld() == false) + { + return; + } + + if (bootstrappedLobbyId == pendingLobbyId) + { + return; + } + + if (BasisNetworkSpawnItem.RequestSceneLoad( + pendingWorldPassword, + pendingWorldUrl, + true, + false, + out _, + 2)) + { + bootstrappedLobbyId = pendingLobbyId; + BasisDebug.Log($"Steam host bootstrap queued world load for {pendingWorldName}", BasisDebug.LogTag.Networking); + } + else + { + BasisDebug.LogError("Steam host bootstrap failed to queue world load request.", BasisDebug.LogTag.Networking); + } + }); + } + + private static void HandleLocalPlayerLeft(BasisNetworkPlayer networkedPlayer, BasisLocalPlayer localPlayer) + { + bootstrappedLobbyId = 0; + + if (!IsSteamStackActive() || suppressNextLobbyLeave) + { + return; + } + + BasisSteamLobbyService.LeaveLobby(); + } + + private static bool IsSteamStackActive() + { + return string.Equals(BasisNetworkManagement.NetworkStackId, BasisSteamNetworkStack.StackId, StringComparison.OrdinalIgnoreCase); + } + + 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; + } + + if (BasisSteamLobbyService.State.LobbyId == lobbyId && + IsSteamStackActive() && + BasisNetworkConnection.LocalPlayerIsConnected) + { + return; + } + + try + { + if (BasisSteamLobbyService.State.LobbyId != 0 && BasisSteamLobbyService.State.LobbyId != lobbyId) + { + BasisSteamLobbyService.LeaveLobby(); + } + + await ResetNetworkStateAsync(keepSteamLobby: true); + + BasisSteamLobbyState joinedLobby = await BasisSteamLobbyService.JoinLobbyAsync(lobbyId); + if (joinedLobby == null) + { + BasisDebug.LogError($"Steam lobby invite join failed for lobby {lobbyId}.", BasisDebug.LogTag.Networking); + return; + } + + PrepareSteamConnection(joinedLobby, false); + + BasisDebug.Log($"Joining Steam lobby from invite {joinedLobby.LobbyId}.", BasisDebug.LogTag.Networking); + BasisMainMenu.Close(); + BasisCursorManagement.OnReset(); + BasisNetworkManagement.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..670c2e125b --- /dev/null +++ b/Basis/Packages/com.basis.steamtransport/Runtime/Lobby/BasisSteamLobbyService.cs @@ -0,0 +1,345 @@ +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, BasisSteamNetworkStack.StackId); + 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, BasisSteamNetworkStack.StackId) + .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; + BasisSteamNetworkIntegration.ClearPendingSteamWorld(); + BasisSteamNetworkIntegration.ClearTransportState(); + 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/BasisSteamNetworkStack.cs b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamNetworkStack.cs new file mode 100644 index 0000000000..98f8cde118 --- /dev/null +++ b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamNetworkStack.cs @@ -0,0 +1,138 @@ +using Basis.Network.Core; +using System; +using System.Globalization; +using UnityEngine; + +namespace Basis.Scripts.Networking.Steam +{ + [Serializable] + public class BasisSteamTransportConfig + { + public ulong LobbyId; + public ulong HostSteamId; + public int VirtualPort; + public bool UseSteamRelay = true; + + public void Clear() + { + LobbyId = 0; + HostSteamId = 0; + VirtualPort = 0; + UseSteamRelay = true; + } + } + + public static class BasisSteamNetworkStack + { + public const string StackId = "steam"; + public const string DisplayName = "Steam"; + public const string HostSteamIdKey = "hostSteamId"; + public const string VirtualPortKey = "virtualPort"; + public const string UseRelayKey = "useRelay"; + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterAssembliesLoaded)] + private static void Initialize() + { + Register(); + } + + public static void Register() + { + BasisNetworkStackRegistry.Register(StackId, DisplayName, Create); + BasisNetworkStackRegistry.RegisterParser(StackId, new BasisSteamConnectionTargetParser()); + BasisNetworkStackRegistry.RegisterTick(StackId, SteamNetManager.PollActiveManagers); + BasisTransportConfigStore.RegisterType(StackId, typeof(BasisSteamTransportConfig)); + } + + public static NetManager Create(EventBasedNetListener listener, Configuration configuration) + { + return new SteamNetManager(listener, configuration); + } + + public static BasisSteamTransportConfig GetConfig() + { + return BasisTransportConfigStore.Get(StackId); + } + } + + internal sealed class BasisSteamConnectionTargetParser : IConnectionTargetParser + { + public void Parse(ConnectionTarget target) + { + if (target == null) + { + return; + } + + string raw = target.Raw ?? string.Empty; + if (raw.StartsWith("steam://lobby/", StringComparison.OrdinalIgnoreCase)) + { + target.Set(ConnectionTarget.Keys.LobbyId, raw.Substring("steam://lobby/".Length)); + return; + } + + if (raw.StartsWith("steam://", StringComparison.OrdinalIgnoreCase)) + { + target.Set(ConnectionTarget.Keys.LobbyId, raw.Substring("steam://".Length)); + return; + } + + if (raw.StartsWith("steam:", StringComparison.OrdinalIgnoreCase)) + { + target.Set(ConnectionTarget.Keys.LobbyId, raw.Substring("steam:".Length)); + return; + } + + int separator = raw.IndexOf(':'); + if (separator > 0) + { + target.Set(BasisSteamNetworkStack.HostSteamIdKey, raw.Substring(0, separator)); + target.Set(BasisSteamNetworkStack.VirtualPortKey, raw.Substring(separator + 1)); + return; + } + + if (!string.IsNullOrWhiteSpace(raw)) + { + target.Set(BasisSteamNetworkStack.HostSteamIdKey, raw); + } + } + + public string Format(ConnectionTarget target) + { + if (target == null) + { + return string.Empty; + } + + string lobbyId = target.Get(ConnectionTarget.Keys.LobbyId); + if (!string.IsNullOrWhiteSpace(lobbyId)) + { + return "steam://lobby/" + lobbyId; + } + + string hostSteamId = target.Get(BasisSteamNetworkStack.HostSteamIdKey); + if (string.IsNullOrWhiteSpace(hostSteamId)) + { + hostSteamId = target.Get(ConnectionTarget.Keys.Address); + } + + string virtualPort = target.Get(BasisSteamNetworkStack.VirtualPortKey); + if (string.IsNullOrWhiteSpace(virtualPort)) + { + virtualPort = target.Get(ConnectionTarget.Keys.Port); + } + + if (string.IsNullOrWhiteSpace(hostSteamId)) + { + return target.Raw ?? string.Empty; + } + + if (int.TryParse(virtualPort, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedPort)) + { + return string.Format(CultureInfo.InvariantCulture, "{0}:{1}", hostSteamId, parsedPort); + } + + return hostSteamId; + } + } +} diff --git a/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamNetworkStack.cs.meta b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamNetworkStack.cs.meta new file mode 100644 index 0000000000..af74b1a931 --- /dev/null +++ b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/BasisSteamNetworkStack.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: abf7420a067f70140b7f00624fe89fa8 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/SteamNetManager.cs b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/SteamNetManager.cs new file mode 100644 index 0000000000..e908072f1f --- /dev/null +++ b/Basis/Packages/com.basis.steamtransport/Runtime/Transport/SteamNetManager.cs @@ -0,0 +1,1059 @@ +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 BasisSteamTransportConfig steamConfig; + 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; + steamConfig = BasisSteamNetworkStack.GetConfig(); + RegisterActiveManager(this); + + if (steamConfig == null || !steamConfig.UseSteamRelay) + { + useFallback = true; + fallbackManager = new LNLNetManager(listener, configuration); + BNL.LogWarning("Steam transport: UseSteamRelay is disabled or unavailable, 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={steamConfig.LobbyId} HostSteamId={steamConfig.HostSteamId} VirtualPort={steamConfig.VirtualPort}"); + } + } + + public void Start(IPAddress IPv4Address, IPAddress IPv6Address, int SetPort) + { + if (useFallback) + { + fallbackManager.Start(IPv4Address, IPv6Address, SetPort); + return; + } + + if (SetPort > 0) + { + serverSocketManager = SteamNetworkingSockets.CreateRelaySocket(steamConfig.VirtualPort); + serverSocketManager.Owner = this; + serverReceiveDelegate = serverSocketManager.Receive; + serverReceiveEnabled = serverSocketManager != null; + BasisSteamTransportTrace.Log($"CreateRelaySocket virtualPort={steamConfig.VirtualPort} 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 (steamConfig.HostSteamId == 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, steamConfig.HostSteamId.ToString()); + clientConnectionManager = SteamNetworkingSockets.ConnectRelay((SteamId)steamConfig.HostSteamId, steamConfig.VirtualPort); + clientConnectionManager.Owner = this; + clientConnectionManager.LocalPeer = peer; + clientConnectionManager.ConnectPayload = connectPayload; + clientReceiveDelegate = clientConnectionManager.Receive; + clientReceiveEnabled = clientConnectionManager != null; + BasisSteamTransportTrace.Log($"ConnectRelay hostSteamId={steamConfig.HostSteamId} virtualPort={steamConfig.VirtualPort} 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) + { + 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, steamConfig.HostSteamId.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..7f81ceb746 --- /dev/null +++ b/Basis/Packages/com.basis.steamtransport/Runtime/UI/SteamLobbiesProvider.Actions.cs @@ -0,0 +1,441 @@ +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() + { + useRelayToggle.SetValueWithoutNotify(BasisSteamNetworkStack.GetConfig().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; + } + + await BasisSteamNetworkIntegration.ResetNetworkStateAsync(keepSteamLobby: true); + BasisSteamNetworkIntegration.PrepareSteamConnection( + lobbyState, + true, + validation.WorldUrl, + validation.WorldPassword, + validation.WorldName); + + infoDescriptor.SetTitle("Steam"); + infoDescriptor.SetDescription("Starting local host session for the Steam lobby..."); + BasisMainMenu.Close(); + BasisCursorManagement.OnReset(); + BasisNetworkManagement.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; + } + + await BasisSteamNetworkIntegration.ResetNetworkStateAsync(keepSteamLobby: true); + BasisSteamNetworkIntegration.PrepareSteamConnection(joinedLobby, false); + + infoDescriptor.SetTitle("Steam"); + infoDescriptor.SetDescription($"Connecting to Steam host {joinedLobby.HostSteamId}..."); + BasisMainMenu.Close(); + BasisCursorManagement.OnReset(); + BasisNetworkManagement.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) + { + SetInfo("Disconnecting", "Disconnecting from the active Steam session..."); + await BasisSteamNetworkIntegration.ResetNetworkStateAsync(keepSteamLobby: true); + } + } + 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; + + 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 178c78cb6b..84a3de87d7 100644 --- a/Basis/Packages/packages-lock.json +++ b/Basis/Packages/packages-lock.json @@ -155,6 +155,12 @@ "com.unity.mathematics": "1.3.3" } }, + "com.basis.steamtransport": { + "version": "file:com.basis.steamtransport", + "depth": 0, + "source": "embedded", + "dependencies": {} + }, "com.basis.textmeshpro": { "version": "file:com.basis.textmeshpro", "depth": 0,