using System; using System.Collections.Generic; using System.Threading; using Cysharp.Threading.Tasks; using RebootKit.Engine.Simulation; using Unity.Collections; using Unity.Netcode; using UnityEngine; using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootKit.Engine.Main { class NetworkClientState { public ulong ClientID; public bool IsWorldLoaded; public bool AreActorsSynced; public bool IsReadyForActorsSync; public int ActorsSyncPacketsLeft; public bool IsReady; } public class NetworkSystem : NetworkBehaviour { static readonly Logger s_Logger = new Logger(nameof(NetworkSystem)); [field: SerializeField] public ActorsManager Actors { get; private set; } readonly Dictionary m_Clients = new Dictionary(); FixedString512Bytes m_WorldID = new FixedString512Bytes(""); bool m_IsChangingWorld = false; void Awake() { RR.NetworkSystemInstance = this; } public override void OnNetworkSpawn() { base.OnNetworkSpawn(); NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected; NetworkManager.Singleton.OnClientDisconnectCallback += OnClientDisconnect; } public override void OnNetworkDespawn() { base.OnNetworkDespawn(); NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnected; NetworkManager.Singleton.OnClientDisconnectCallback -= OnClientDisconnect; } void OnClientConnected(ulong clientID) { if (!IsServer) { return; } s_Logger.Info($"OnClientConnected: {clientID}"); NetworkClientState newClientState = new NetworkClientState { ClientID = clientID, IsWorldLoaded = false, AreActorsSynced = false, IsReadyForActorsSync = false, IsReady = false }; m_Clients.Add(clientID, newClientState); if (!m_WorldID.IsEmpty) { s_Logger.Info($"Synchronizing world load for client {clientID} with world ID '{m_WorldID}'"); ClientLoadWorldRpc(m_WorldID.ToString(), RpcTarget.Single(clientID, RpcTargetUse.Temp)); } } void OnClientDisconnect(ulong clientID) { if (!IsServer) { return; } s_Logger.Info($"OnClientDisconnect: {clientID}"); m_Clients.Remove(clientID); } internal NetworkClientState GetClientState(ulong clientID) { if (m_Clients.TryGetValue(clientID, out NetworkClientState clientState)) { return clientState; } s_Logger.Error($"Client state for {clientID} not found."); return null; } public void SetCurrentWorld(string worldID) { if (!IsServer) { s_Logger.Error("Only server can set the current world."); return; } if (m_IsChangingWorld) { s_Logger.Error($"Already changing world to '{m_WorldID}'. Please wait until the current world change is complete."); return; } WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID); if (worldConfigAsset is null) { s_Logger.Error($"Failed to set current world: World config asset for '{worldID}' not found."); return; } m_WorldID = worldID; foreach (KeyValuePair kv in m_Clients) { kv.Value.IsWorldLoaded = false; kv.Value.AreActorsSynced = false; kv.Value.IsReadyForActorsSync = false; kv.Value.IsReady = false; } ServerLoadWorldAsync(worldConfigAsset, destroyCancellationToken).Forget(); } async UniTask ServerLoadWorldAsync(WorldConfigAsset asset, CancellationToken cancellationToken) { s_Logger.Info($"ServerLoadWorldAsync: {asset.Config.name}"); m_IsChangingWorld = true; RR.World.Unload(); RR.CloseMainMenu(); await RR.World.LoadAsync(asset.Config, cancellationToken); m_IsChangingWorld = false; NetworkClientState localClientState = GetClientState(NetworkManager.Singleton.LocalClientId); localClientState.IsReady = true; RR.GameInstance.PlayerBecameReady(localClientState.ClientID); ClientLoadWorldRpc(asset.name, RpcTarget.NotMe); } [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)] void ClientLoadWorldRpc(string worldID, RpcParams rpcParams) { WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID); if (worldConfigAsset is null) { s_Logger.Error($"World config asset for '{worldID}' not found."); RR.Disconnect(); return; } ClientLoadWorldAsync(worldID, destroyCancellationToken).Forget(); } async UniTask ClientLoadWorldAsync(string worldID, CancellationToken cancellationToken) { s_Logger.Info($"ClientLoadWorldAsync: {worldID}"); WorldConfigAsset worldConfigAsset = RR.GetWorldConfigAsset(worldID); if (worldConfigAsset is null) { s_Logger.Error($"World config asset for '{worldID}' not found."); return; } RR.World.Unload(); RR.CloseMainMenu(); await RR.World.LoadAsync(worldConfigAsset.Config, cancellationToken); m_WorldID = worldID; ClientLoadedWorldRpc(worldID); } [Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)] void ClientLoadedWorldRpc(string worldID, RpcParams rpcParams = default) { ulong clientID = rpcParams.Receive.SenderClientId; if (!m_WorldID.Equals(worldID)) { s_Logger.Error($"Client {clientID} tried to load world '{worldID}', but server is in world '{m_WorldID}'."); NetworkManager.Singleton.DisconnectClient(clientID, "World mismatch!"); return; } if (m_Clients.TryGetValue(clientID, out NetworkClientState clientState)) { clientState.IsWorldLoaded = true; clientState.IsReadyForActorsSync = false; Actors.SynchronizeActorsForClient(clientID); } else { NetworkManager.Singleton.DisconnectClient(clientID, "Client is not registered!"); } } internal void ClientSynchronizedActors(ulong clientID) { NetworkClientState clientState = GetClientState(clientID); if (clientState is null) { s_Logger.Error($"Client state for {clientID} not found."); return; } clientState.IsReady = true; RR.GameInstance.PlayerBecameReady(clientID); } } }