Files
2026-08-11 17:12:34 +03:00

310 lines
11 KiB
C#

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<Lobby> PublicLobbies => publicLobbies;
private readonly List<Lobby> 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<NetworkManager>();
transport = GetComponent<UnityTransport>();
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<string, DataObject>
{
{ 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);
}
}