using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using Cysharp.Threading.Tasks; using RebootKit.Engine.Extensions; using RebootKit.Engine.Foundation; using RebootKit.Engine.Main; using RebootKit.Engine.Simulation; using Unity.Collections; using Unity.Netcode; using UnityEngine; using UnityEngine.Assertions; using Logger = RebootKit.Engine.Foundation.Logger; namespace RebootKit.Engine.Network { enum NetworkClientSyncState { NotReady, LoadingWorld, PreparingForActorsSync, SyncingActors, Ready } struct NetworkClientState : INetworkSerializable { public ulong ClientID; public NetworkClientSyncState SyncState; public int ActorsSyncPacketsLeft; public NetworkPacketQueue ReliableQueue; public NetworkPacketQueue UnreliableQueue; public bool IsReady { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return SyncState == NetworkClientSyncState.Ready; } } public void NetworkSerialize(BufferSerializer serializer) where T : IReaderWriter { serializer.SerializeValue(ref ClientID); serializer.SerializeValue(ref SyncState); serializer.SerializeValue(ref ActorsSyncPacketsLeft); } } struct NetworkPacketTarget { public enum Type { AllClients, Single, Server } public Type TargetType; public ulong ClientID; public static NetworkPacketTarget AllClients() { return new NetworkPacketTarget { TargetType = Type.AllClients, ClientID = 0 }; } public static NetworkPacketTarget Single(ulong clientID) { return new NetworkPacketTarget { TargetType = Type.Single, ClientID = clientID }; } public static NetworkPacketTarget Server() { return new NetworkPacketTarget { TargetType = Type.Server, ClientID = 0 }; } } public class NetworkSystem : NetworkBehaviour { [ConfigVar("sv.tick_rate", 32, "Server tick rate in Hz", CVarFlags.Server)] public static ConfigVar TickRate; static readonly Logger s_Logger = new Logger(nameof(NetworkSystem)); const int k_ReliablePacketQueueSize = 1024; const int k_UnreliablePacketQueueSize = 512; [field: SerializeField] public ActorsManager Actors { get; private set; } internal readonly Dictionary Clients = new Dictionary(); public FixedString512Bytes WorldID { get; private set; } = new FixedString512Bytes(""); bool m_IsChangingWorld = false; float m_TickTimer; public ulong TickCount { get; private set; } public event Action ServerTick = delegate { }; public ulong LocalClientID { [MethodImpl(MethodImplOptions.AggressiveInlining)] get { return NetworkManager.Singleton.LocalClientId; } } public int LastTickPacketsSentCount { get; private set; } NetworkPacketQueue m_ReliablePacketQueue; NetworkPacketQueue m_UnreliablePacketQueue; NetworkPacketQueue m_ReliablePacketQueueToServer; NetworkPacketQueue m_UnreliablePacketQueueToServer; // // @MARK: Unity callbacks // void Awake() { RR.NetworkSystemInstance = this; m_ReliablePacketQueue = new NetworkPacketQueue(k_ReliablePacketQueueSize); m_UnreliablePacketQueue = new NetworkPacketQueue(k_UnreliablePacketQueueSize); m_ReliablePacketQueueToServer = new NetworkPacketQueue(k_ReliablePacketQueueSize); m_UnreliablePacketQueueToServer = new NetworkPacketQueue(k_UnreliablePacketQueueSize); } void Update() { float deltaTime = Time.deltaTime; float tickDeltaTime = 1.0f / TickRate.IndexValue; m_TickTimer += deltaTime; while (m_TickTimer >= tickDeltaTime) { m_TickTimer -= tickDeltaTime; LastTickPacketsSentCount = 0; if (RR.IsServer()) { Actors.ServerTick(tickDeltaTime); ServerTick?.Invoke(TickCount); TickCount++; FlushServerPackets(); } if (RR.IsClient()) { FlushClientPackets(); } } } // // @MARK: NetworkBehaviour callbacks // 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, SyncState = NetworkClientSyncState.NotReady, ReliableQueue = new NetworkPacketQueue(k_ReliablePacketQueueSize), UnreliableQueue = new NetworkPacketQueue(k_UnreliablePacketQueueSize) }; Clients.Add(clientID, newClientState); if (clientID != NetworkManager.Singleton.LocalClientId) { foreach (NetworkClientState state in Clients.Values) { UpdateClientStateRpc(state, RpcTarget.Single(clientID, RpcTargetUse.Temp)); } } if (!WorldID.IsEmpty) { s_Logger.Info($"Synchronizing world load for client {clientID} with world ID '{WorldID}'"); ClientLoadWorldRpc(WorldID.ToString(), RpcTarget.Single(clientID, RpcTargetUse.Temp)); } } void OnClientDisconnect(ulong clientID) { s_Logger.Info($"OnClientDisconnect: {clientID}"); Clients.Remove(clientID); } // // @MARK: Server API // public void KickClient(ulong clientID, string reason = "Kicked by server") { if (!IsServer) { s_Logger.Error("Only server can kick clients."); return; } if (NetworkManager.Singleton.ConnectedClients.TryGetValue(clientID, out NetworkClient client)) { NetworkManager.Singleton.DisconnectClient(clientID, reason); s_Logger.Info($"Kicked client {clientID}: {reason}"); } else { s_Logger.Error($"Client {clientID} not found."); } } 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 '{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; } WorldID = worldID; foreach ((ulong _, NetworkClientState clientState) in Clients.ToList()) { NetworkClientState state = clientState; state.SyncState = NetworkClientSyncState.LoadingWorld; UpdateClientState(state); } 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; if (!TryGetClientState(NetworkManager.Singleton.LocalClientId, out NetworkClientState localClientState)) { s_Logger.Error($"Local client state not found for client ID {NetworkManager.Singleton.LocalClientId}."); RR.Disconnect(); return; } localClientState.SyncState = NetworkClientSyncState.Ready; UpdateClientState(localClientState); 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); WorldID = worldID; ClientLoadedWorldRpc(worldID); } [Rpc(SendTo.Server, Delivery = RpcDelivery.Reliable)] void ClientLoadedWorldRpc(string worldID, RpcParams rpcParams = default) { ulong clientID = rpcParams.Receive.SenderClientId; if (!WorldID.Equals(worldID)) { s_Logger.Error($"Client {clientID} tried to load world '{worldID}', but server is in world '{WorldID}'."); NetworkManager.Singleton.DisconnectClient(clientID, "World mismatch!"); return; } if (Clients.TryGetValue(clientID, out NetworkClientState clientState)) { Actors.InitializeActorsForClient(clientID); } else { NetworkManager.Singleton.DisconnectClient(clientID, "Client is not registered!"); } } // // @MARK: Internal // internal bool TryGetClientState(ulong clientID, out NetworkClientState clientState) { return Clients.TryGetValue(clientID, out clientState); } internal void UpdateClientState(NetworkClientState clientState) { if (!IsServer) { s_Logger.Error("UpdateClientState can only be called on the server."); return; } Clients[clientState.ClientID] = clientState; UpdateClientStateRpc(clientState, RpcTarget.NotServer); } [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)] void UpdateClientStateRpc(NetworkClientState newState, RpcParams rpcParams) { Clients[newState.ClientID] = newState; } internal void ClientSynchronizedActors(ulong clientID) { if (TryGetClientState(clientID, out NetworkClientState state)) { state.SyncState = NetworkClientSyncState.Ready; UpdateClientState(state); RR.GameInstance.PlayerBecameReady(clientID); } else { s_Logger.Error($"Client state for {clientID} not found."); } } internal int GetReadyClientsCount() { int count = 0; foreach (NetworkClientState clientState in Clients.Values) { if (clientState.IsReady) { count++; } } return count; } // // @MARK: Network packets // NetworkPacketQueue GetPacketQueue(NetworkPacketTarget target, bool reliable) { if (target.TargetType == NetworkPacketTarget.Type.AllClients) { return reliable ? m_ReliablePacketQueue : m_UnreliablePacketQueue; } if (target.TargetType == NetworkPacketTarget.Type.Server) { return reliable ? m_ReliablePacketQueueToServer : m_UnreliablePacketQueueToServer; } if (target.TargetType == NetworkPacketTarget.Type.Single) { if (TryGetClientState(target.ClientID, out NetworkClientState clientState)) { return reliable ? clientState.ReliableQueue : clientState.UnreliableQueue; } s_Logger.Error($"Client state for {target.ClientID} not found."); return null; } s_Logger.Error($"Invalid network packet target type: {target.TargetType}"); return null; } internal void WriteActorState(NetworkPacketTarget target, ulong actorID, IActorData actorData) { if (!RR.IsServer()) { s_Logger.Error("WriteActorState can only be called on the server."); return; } NetworkPacketQueue queue = GetPacketQueue(target, true); queue.WriteActorState(actorID, actorData); } internal void WriteActorTransformState(NetworkPacketTarget target, ulong actorID, ActorTransformSyncData transformData) { if (!RR.IsServer()) { s_Logger.Error("WriteActorTransformState can only be called on the server."); return; } NetworkPacketQueue queue = GetPacketQueue(target, false); queue.WriteActorTransformState(actorID, transformData); } internal void WriteActorCoreState(NetworkPacketTarget target, ulong actorID, ActorCoreStateSnapshot coreData) { if (!RR.IsServer()) { s_Logger.Error("WriteActorCoreState can only be called on the server."); return; } NetworkPacketQueue queue = GetPacketQueue(target, true); queue.WriteActorCoreState(actorID, coreData); } internal void WriteSpawnActor(NetworkPacketTarget target, string assetGUID, ulong actorID, ActorCoreStateSnapshot coreState, IActorData actorData) { if (!RR.IsServer()) { s_Logger.Error("WriteSpawnActor can only be called on the server."); return; } NetworkPacketQueue queue = GetPacketQueue(target, true); queue.WriteSpawnActor(assetGUID, actorID, coreState, actorData); } internal void WriteActorSynchronize(NetworkPacketTarget target, ulong actorID, ActorCoreStateSnapshot coreState, IActorData actorData) { if (!RR.IsServer()) { s_Logger.Error("WriteActorSynchronize can only be called on the server."); return; } NetworkPacketQueue queue = GetPacketQueue(target, true); queue.WriteActorSynchronize(actorID, coreState, actorData); } internal void WriteActorEvent(NetworkPacketTarget target, ActorEvent actorEvent) { if (!RR.IsServer()) { s_Logger.Error("WriteActorEvent can only be called on the server."); return; } NetworkPacketQueue queue = GetPacketQueue(target, true); queue.WriteActorEvent(actorEvent); } internal void WriteActorCommand(ActorCommand actorCommand) { NetworkPacketQueue queue = GetPacketQueue(NetworkPacketTarget.Server(), true); queue.WriteActorCommand(actorCommand); } void FlushClientPackets() { if (!RR.IsClient()) { return; } if (RR.IsServer()) { foreach (NetworkPacket networkPacket in m_ReliablePacketQueueToServer.NetworkPackets) { if (networkPacket.EntityCount > 0) { OnReceivedNetworkPacket(networkPacket.Data); } } foreach (NetworkPacket networkPacket in m_UnreliablePacketQueueToServer.NetworkPackets) { if (networkPacket.EntityCount > 0) { OnReceivedNetworkPacket(networkPacket.Data); } } } else { foreach (NetworkPacket networkPacket in m_ReliablePacketQueueToServer.NetworkPackets) { if (networkPacket.EntityCount == 0) { continue; } LastTickPacketsSentCount += 1; ReliableReceiveNetworkPacketRpc(networkPacket.Data, RpcTarget.Server); } foreach (NetworkPacket networkPacket in m_UnreliablePacketQueueToServer.NetworkPackets) { if (networkPacket.EntityCount == 0) { continue; } LastTickPacketsSentCount += 1; UnreliableReceiveNetworkPacketRpc(networkPacket.Data, RpcTarget.Server); } } m_ReliablePacketQueueToServer.Clear(); m_UnreliablePacketQueueToServer.Clear(); } void FlushServerPackets() { if (!RR.IsServer()) { return; } foreach (NetworkPacket networkPacket in m_ReliablePacketQueue.NetworkPackets) { foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) { if (clientID == NetworkManager.Singleton.LocalClientId) { continue; } if (networkPacket.EntityCount == 0) { continue; } if (state.IsReady) { LastTickPacketsSentCount += 1; ReliableReceiveNetworkPacketRpc(networkPacket.Data, RpcTarget.Single(clientID, RpcTargetUse.Temp)); } } } foreach (NetworkPacket networkPacket in m_UnreliablePacketQueue.NetworkPackets) { foreach ((ulong clientID, NetworkClientState state) in RR.NetworkSystemInstance.Clients) { if (clientID == NetworkManager.Singleton.LocalClientId) { continue; } if (networkPacket.EntityCount == 0) { continue; } if (state.IsReady) { LastTickPacketsSentCount += 1; UnreliableReceiveNetworkPacketRpc(networkPacket.Data, RpcTarget.Single(clientID, RpcTargetUse.Temp)); } } } m_ReliablePacketQueue.Clear(); m_UnreliablePacketQueue.Clear(); foreach (NetworkClientState clientState in Clients.Values) { if (clientState.ClientID == NetworkManager.Singleton.LocalClientId) { continue; } foreach (NetworkPacket networkPacket in clientState.ReliableQueue.NetworkPackets) { if (networkPacket.EntityCount == 0) { continue; } LastTickPacketsSentCount += 1; ReliableReceiveNetworkPacketRpc(networkPacket.Data, RpcTarget.Single(clientState.ClientID, RpcTargetUse.Temp)); } foreach (NetworkPacket networkPacket in clientState.UnreliableQueue.NetworkPackets) { if (networkPacket.EntityCount == 0) { continue; } LastTickPacketsSentCount += 1; UnreliableReceiveNetworkPacketRpc(networkPacket.Data, RpcTarget.Single(clientState.ClientID, RpcTargetUse.Temp)); } clientState.ReliableQueue.Clear(); clientState.UnreliableQueue.Clear(); } } [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Reliable)] void ReliableReceiveNetworkPacketRpc(NativeArray data, RpcParams rpcParams) { OnReceivedNetworkPacket(data); } [Rpc(SendTo.SpecifiedInParams, Delivery = RpcDelivery.Unreliable)] void UnreliableReceiveNetworkPacketRpc(NativeArray data, RpcParams rpcParams) { OnReceivedNetworkPacket(data); } void OnReceivedNetworkPacket(NativeArray data) { using NetworkBufferReader reader = new NetworkBufferReader(data); NetworkPacketHeader packetHeader = new NetworkPacketHeader(); packetHeader.Deserialize(reader); // s_Logger.Info($"Received packet: MagicNumber={packetHeader.MagicNumber}, Version={packetHeader.Version}, EntityCount={packetHeader.EntityCount}"); Assert.IsTrue(packetHeader.MagicNumber == RConsts.k_NetworkPacketMagicNumber, "Received packet with invalid magic number."); if (packetHeader.EntityCount == 0) { s_Logger.Info("Received packet with no entities.\n" + data.ToHexString()); } for (int i = 0; i < packetHeader.EntityCount; i++) { NetworkDataHeader dataHeader = new NetworkDataHeader(); dataHeader.Deserialize(reader); // s_Logger.Info($"Received entity: Type={dataHeader.Type}, ActorID={dataHeader.ActorID}, DataSize={dataHeader.DataSize}"); if (dataHeader.Type == NetworkDataType.None) { s_Logger.Info("Data of packet with entry with type None:\n" + data.ToHexString()); } Assert.IsTrue(dataHeader.Type != NetworkDataType.None, "Received packet with invalid data type."); reader.Read(out NativeArray entityData, dataHeader.DataSize, Allocator.Temp); OnReceivedEntity(dataHeader, entityData); entityData.Dispose(); } } void OnReceivedEntity(NetworkDataHeader header, NativeArray data) { Actors.OnReceivedEntity(header, data); } } }