using System; using System.Collections.Generic; using System.Threading.Tasks; using Unity.Netcode; using Unity.Netcode.Transports.UTP; using Unity.Services.Authentication; using Unity.Services.Core; using Unity.Services.Lobbies; using Unity.Services.Lobbies.Models; using Unity.Services.Relay; using Unity.Services.Relay.Models; using UnityEngine; using UnityEngine.SceneManagement; [RequireComponent(typeof(NetworkManager), typeof(UnityTransport))] public sealed class NetworkSessionController : MonoBehaviour { private const string PlayerNamePreferenceKey = "player_name"; private const string RelayCodeDataKey = "relay_code"; public static NetworkSessionController Instance { get; private set; } [SerializeField] private NetworkObject playerPrefab; [SerializeField] private string menuSceneName = "MainMenu"; [SerializeField] private string gameplaySceneName = "Factory"; [SerializeField, Min(1)] private int maxClientCount = 3; [SerializeField] private bool useSecureRelay = false; public string Status { get; private set; } = "Relay is not initialized."; public string JoinCode { get; private set; } = string.Empty; public string LocalPlayerName { get; private set; } public bool IsBusy { get; private set; } public bool IsOnline => NetworkManager.Singleton != null && NetworkManager.Singleton.IsListening; public bool IsHost => networkManager != null && networkManager.IsHost; public IReadOnlyList PublicLobbies => publicLobbies; private readonly List publicLobbies = new(); private NetworkManager networkManager; private UnityTransport transport; private bool loadFactoryAfterServerStart; private void Awake() { if (Instance != null) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); LocalPlayerName = PlayerPrefs.GetString(PlayerNamePreferenceKey, "Worker"); networkManager = GetComponent(); transport = GetComponent(); networkManager.NetworkConfig.NetworkTransport = transport; networkManager.NetworkConfig.PlayerPrefab = playerPrefab.gameObject; networkManager.NetworkConfig.EnableSceneManagement = true; networkManager.OnServerStarted += HandleServerStarted; networkManager.OnClientConnectedCallback += HandleClientConnected; } private void Start() => SceneManager.LoadScene(menuSceneName); private void OnDestroy() { if (networkManager != null) networkManager.OnServerStarted -= HandleServerStarted; if (networkManager != null) networkManager.OnClientConnectedCallback -= HandleClientConnected; if (Instance == this) Instance = null; } public void SetLocalPlayerName(string playerName) { string sanitizedName = string.IsNullOrWhiteSpace(playerName) ? "Worker" : playerName.Trim(); LocalPlayerName = sanitizedName.Length > 24 ? sanitizedName[..24] : sanitizedName; PlayerPrefs.SetString(PlayerNamePreferenceKey, LocalPlayerName); PlayerPrefs.Save(); } public async Task StartHostAsync() { if (IsBusy || IsOnline) return; IsBusy = true; try { await InitializeServicesAsync(); Status = "Creating Relay allocation..."; Allocation allocation = await RelayService.Instance.CreateAllocationAsync(maxClientCount); transport.SetHostRelayData(allocation.RelayServer.IpV4, (ushort)allocation.RelayServer.Port, allocation.AllocationIdBytes, allocation.Key, allocation.ConnectionData, useSecureRelay); JoinCode = await RelayService.Instance.GetJoinCodeAsync(allocation.AllocationId); await PublishLobbyAsync(); loadFactoryAfterServerStart = true; if (!networkManager.StartHost()) throw new InvalidOperationException("Netcode host could not start."); Status = "Host started."; } catch (Exception exception) { Status = $"Relay host error: {exception.Message}"; Debug.LogException(exception); } finally { IsBusy = false; } } public async Task StartClientAsync(string joinCode) { if (IsBusy || IsOnline || string.IsNullOrWhiteSpace(joinCode)) return; IsBusy = true; try { await InitializeServicesAsync(); Status = "Joining Relay allocation..."; JoinAllocation allocation = await RelayService.Instance.JoinAllocationAsync(joinCode.Trim().ToUpperInvariant()); transport.SetClientRelayData(allocation.RelayServer.IpV4, (ushort)allocation.RelayServer.Port, allocation.AllocationIdBytes, allocation.Key, allocation.ConnectionData, allocation.HostConnectionData, useSecureRelay); if (!networkManager.StartClient()) throw new InvalidOperationException("Netcode client could not start."); Status = "Connecting to host..."; } catch (Exception exception) { Status = $"Relay join error: {exception.Message}"; Debug.LogException(exception); } finally { IsBusy = false; } } public async Task RefreshPublicLobbiesAsync() { if (IsBusy) return; IsBusy = true; try { await InitializeServicesAsync(); QueryResponse response = await LobbyService.Instance.QueryLobbiesAsync(new QueryLobbiesOptions { Count = 20 }); publicLobbies.Clear(); publicLobbies.AddRange(response.Results); Status = $"{publicLobbies.Count} open lobby/lobbies found."; } catch (Exception exception) { Status = $"Lobby query error: {exception.Message}"; Debug.LogException(exception); } finally { IsBusy = false; } } public async Task JoinPublicLobbyAsync(string lobbyId) { if (IsBusy || IsOnline || string.IsNullOrWhiteSpace(lobbyId)) return; string relayCode = null; IsBusy = true; try { await InitializeServicesAsync(); Lobby lobby = await LobbyService.Instance.JoinLobbyByIdAsync(lobbyId); if (lobby.Data.TryGetValue(RelayCodeDataKey, out DataObject relayCodeData)) relayCode = relayCodeData.Value; else throw new InvalidOperationException("The lobby has no Relay join code."); } catch (Exception exception) { Status = $"Lobby join error: {exception.Message}"; Debug.LogException(exception); } finally { IsBusy = false; } if (!string.IsNullOrEmpty(relayCode)) await StartClientAsync(relayCode); } private async Task PublishLobbyAsync() { try { var options = new CreateLobbyOptions { IsPrivate = false, Data = new Dictionary { { RelayCodeDataKey, new DataObject(DataObject.VisibilityOptions.Public, JoinCode) } } }; await LobbyService.Instance.CreateLobbyAsync($"{LocalPlayerName}'s Factory", maxClientCount + 1, options); } catch (Exception exception) { Debug.LogWarning($"Lobby publishing failed. Join code still works. {exception.Message}"); } } private static async Task InitializeServicesAsync() { if (UnityServices.State == ServicesInitializationState.Uninitialized) await UnityServices.InitializeAsync(); if (!AuthenticationService.Instance.IsSignedIn) await AuthenticationService.Instance.SignInAnonymouslyAsync(); } private void HandleServerStarted() { if (!loadFactoryAfterServerStart) return; loadFactoryAfterServerStart = false; LoadGameplayScene(); } private void HandleClientConnected(ulong clientId) { if (!networkManager.IsHost && clientId == networkManager.LocalClientId) { Status = "Connected. Loading Factory..."; if (SceneManager.GetActiveScene().name != gameplaySceneName) SceneManager.LoadScene(gameplaySceneName); } } private void LoadGameplayScene() { if (!IsHost) return; Status = "Loading Factory..."; networkManager.SceneManager.LoadScene(gameplaySceneName, LoadSceneMode.Single); } }