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; public enum NetworkConnectionMode { None, Relay, Lan } [RequireComponent(typeof(NetworkManager), typeof(UnityTransport))] public sealed class NetworkSessionController : MonoBehaviour { public const ushort DefaultLanPort = 7777; 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; } = "Ready."; public string JoinCode { get; private set; } = string.Empty; public string LocalPlayerName { get; private set; } public string LanAddress { get; private set; } = "127.0.0.1"; public ushort LanPort { get; private set; } = DefaultLanPort; public NetworkConnectionMode ConnectionMode { 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(); ConnectionMode = NetworkConnectionMode.Relay; loadFactoryAfterServerStart = true; if (!networkManager.StartHost()) throw new InvalidOperationException("Netcode host could not start."); Status = "Host started."; } catch (Exception exception) { ConnectionMode = NetworkConnectionMode.None; loadFactoryAfterServerStart = false; 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); ConnectionMode = NetworkConnectionMode.Relay; if (!networkManager.StartClient()) throw new InvalidOperationException("Netcode client could not start."); Status = "Connecting to host..."; } catch (Exception exception) { ConnectionMode = NetworkConnectionMode.None; Status = $"Relay join error: {exception.Message}"; Debug.LogException(exception); } finally { IsBusy = false; } } public bool StartLanHost(ushort port = DefaultLanPort) { if (IsBusy || IsOnline) return false; IsBusy = true; try { JoinCode = string.Empty; LanAddress = "127.0.0.1"; LanPort = port; ConnectionMode = NetworkConnectionMode.Lan; transport.SetConnectionData(LanAddress, LanPort, "0.0.0.0"); loadFactoryAfterServerStart = true; if (!networkManager.StartHost()) throw new InvalidOperationException("Netcode LAN host could not start."); Status = $"LAN host started on port {LanPort}."; return true; } catch (Exception exception) { ConnectionMode = NetworkConnectionMode.None; loadFactoryAfterServerStart = false; Status = $"LAN host error: {exception.Message}"; Debug.LogException(exception); return false; } finally { IsBusy = false; } } public bool StartLanClient(string address, ushort port = DefaultLanPort) { if (IsBusy || IsOnline) return false; if (string.IsNullOrWhiteSpace(address)) { Status = "LAN address is empty."; return false; } IsBusy = true; try { JoinCode = string.Empty; LanAddress = address.Trim(); LanPort = port; ConnectionMode = NetworkConnectionMode.Lan; transport.SetConnectionData(LanAddress, LanPort); if (!networkManager.StartClient()) throw new InvalidOperationException("Netcode LAN client could not start."); Status = $"Connecting directly to {LanAddress}:{LanPort}..."; return true; } catch (Exception exception) { ConnectionMode = NetworkConnectionMode.None; Status = $"LAN join error: {exception.Message}"; Debug.LogException(exception); return false; } 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); } }