#if UNITY_EDITOR || DEVELOPMENT_BUILD #define DEVELOPMENT #endif using FishNet.Connection; using FishNet.Managing.Debugging; using FishNet.Managing.Logging; using FishNet.Managing.Server; using FishNet.Managing.Timing; using FishNet.Managing.Transporting; using FishNet.Serializing; using FishNet.Transporting; using FishNet.Transporting.Multipass; using GameKit.Dependencies.Utilities; using System; using System.Collections.Generic; using UnityEngine; namespace FishNet.Managing.Client { /// /// A container for local client data and actions. /// [DisallowMultipleComponent] [AddComponentMenu("FishNet/Manager/ClientManager")] public sealed partial class ClientManager : MonoBehaviour { #region Public. /// /// This is set true if the server has notified the client it is using a development build. /// Value is set before authentication. /// public bool IsServerDevelopment { get; private set; } /// /// Called after local client has authenticated. /// public event Action OnAuthenticated; /// /// Called when the local client connection to the server has timed out. /// This is called immediately before disconnecting. /// public event Action OnClientTimeOut; /// /// Called after the local client connection state changes. /// public event Action OnClientConnectionState; /// /// Called when a client other than self connects. /// This is only available when using ServerManager.ShareIds. /// public event Action OnRemoteConnectionState; /// /// Called when the server sends all currently connected clients. /// This is only available when using ServerManager.ShareIds. /// public event Action OnConnectedClients; /// /// True if the client connection is connected to the server. /// public bool Started { get; private set; } /// /// NetworkConnection the local client is using to send data to the server. /// public NetworkConnection Connection = NetworkManager.EmptyConnection; /// /// Handling and information for objects known to the local client. /// public ClientObjects Objects { get; private set; } /// /// All currently connected clients. This field only contains data while ServerManager.ShareIds is enabled. /// public Dictionary Clients = new(); /// /// NetworkManager for client. /// [HideInInspector] public NetworkManager NetworkManager { get; private set; } #endregion #region Serialized. /// /// What platforms to enable remote server timeout. /// [Tooltip("What platforms to enable remote server timeout.")] [SerializeField] private RemoteTimeoutType _remoteServerTimeout = RemoteTimeoutType.Development; /// /// How long in seconds server must go without sending any packets before the local client disconnects. This is independent of any transport settings. /// [Tooltip("How long in seconds server must go without sending any packets before the local client disconnects. This is independent of any transport settings.")] [Range(1, ServerManager.MAXIMUM_REMOTE_CLIENT_TIMEOUT_DURATION)] [SerializeField] private ushort _remoteServerTimeoutDuration = 60; /// /// Sets timeout settings. Can be used at runtime. /// /// public void SetRemoteServerTimeout(RemoteTimeoutType timeoutType, ushort duration) { _remoteServerTimeout = timeoutType; duration = (ushort)Mathf.Clamp(duration, 1, ServerManager.MAXIMUM_REMOTE_CLIENT_TIMEOUT_DURATION); _remoteServerTimeoutDuration = duration; } //todo add remote server timeout (see ServerManager.RemoteClientTimeout). /// /// True to automatically set the frame rate when the client connects. /// [Tooltip("True to automatically set the frame rate when the client connects.")] [SerializeField] private bool _changeFrameRate = true; /// /// Maximum frame rate the client may run at. When as host this value runs at whichever is higher between client and server. /// internal ushort FrameRate => (_changeFrameRate) ? _frameRate : (ushort)0; [Tooltip("Maximum frame rate the client may run at. When as host this value runs at whichever is higher between client and server.")] [Range(1, NetworkManager.MAXIMUM_FRAMERATE)] [SerializeField] private ushort _frameRate = NetworkManager.MAXIMUM_FRAMERATE; /// Sets the maximum frame rate the client may run at. Calling this method will enable ChangeFrameRate. /// /// New value. public void SetFrameRate(ushort value) { _frameRate = (ushort)Mathf.Clamp(value, 0, NetworkManager.MAXIMUM_FRAMERATE); _changeFrameRate = true; if (NetworkManager != null) NetworkManager.UpdateFramerate(); } #endregion #region Private. /// /// Last unscaled time client got a packet. /// private float _lastPacketTime; /// /// Used to read splits. /// private SplitReader _splitReader = new(); #endregion private void OnDestroy() { Objects?.SubscribeToSceneLoaded(false); } /// /// Initializes this script for use. /// /// internal void InitializeOnce_Internal(NetworkManager manager) { NetworkManager = manager; Objects = new(manager); Objects.SubscribeToSceneLoaded(true); /* Unsubscribe before subscribing. * Shouldn't be an issue but better safe than sorry. */ SubscribeToEvents(false); SubscribeToEvents(true); //Listen for client connections from server. RegisterBroadcast(OnClientConnectionBroadcast); RegisterBroadcast(OnConnectedClientsBroadcast); } /// /// Called when the server sends a connection state change for any client. /// /// private void OnClientConnectionBroadcast(ClientConnectionChangeBroadcast args, Channel channel) { //If connecting invoke after added to clients, otherwise invoke before removed. RemoteConnectionStateArgs rcs = new((args.Connected) ? RemoteConnectionState.Started : RemoteConnectionState.Stopped, args.Id, -1); if (args.Connected) { Clients[args.Id] = new(NetworkManager, args.Id, -1, false); OnRemoteConnectionState?.Invoke(rcs); } else { OnRemoteConnectionState?.Invoke(rcs); if (Clients.TryGetValue(args.Id, out NetworkConnection c)) { c.ResetState(); Clients.Remove(args.Id); } } } /// /// Called when the server sends all currently connected clients. /// /// private void OnConnectedClientsBroadcast(ConnectedClientsBroadcast args, Channel channel) { NetworkManager.ClearClientsCollection(Clients); List collection = args.Values; //No connected clients except self. if (collection == null) { collection = new(); } //Other clients. else { int count = collection.Count; for (int i = 0; i < count; i++) { int id = collection[i]; Clients[id] = new(NetworkManager, id, -1, false); } } OnConnectedClients?.Invoke(new(collection)); } /// /// Changes subscription status to transport. /// /// private void SubscribeToEvents(bool subscribe) { if (NetworkManager == null || NetworkManager.TransportManager == null || NetworkManager.TransportManager.Transport == null) return; if (subscribe) { NetworkManager.TransportManager.OnIterateIncomingEnd += TransportManager_OnIterateIncomingEnd; NetworkManager.TransportManager.Transport.OnClientReceivedData += Transport_OnClientReceivedData; NetworkManager.TransportManager.Transport.OnClientConnectionState += Transport_OnClientConnectionState; NetworkManager.TimeManager.OnPostTick += TimeManager_OnPostTick; } else { NetworkManager.TransportManager.OnIterateIncomingEnd -= TransportManager_OnIterateIncomingEnd; NetworkManager.TransportManager.Transport.OnClientReceivedData -= Transport_OnClientReceivedData; NetworkManager.TransportManager.Transport.OnClientConnectionState -= Transport_OnClientConnectionState; NetworkManager.TimeManager.OnPostTick -= TimeManager_OnPostTick; } } /// /// Gets the transport index being used for the local client. /// If only one transport is used this will return 0. If Multipass is being used this will return the client's transport in multipass. /// /// public int GetTransportIndex() { if (NetworkManager.TransportManager.Transport is Multipass mp) return mp.ClientTransport.Index; else return 0; } /// /// Stops the local client connection. /// public bool StopConnection() { return NetworkManager.TransportManager.Transport.StopConnection(false); } /// /// Starts the local client connection. /// public bool StartConnection() { return NetworkManager.TransportManager.Transport.StartConnection(false); } /// /// Sets the transport address and starts the local client connection. /// public bool StartConnection(string address) { NetworkManager.TransportManager.Transport.SetClientAddress(address); return StartConnection(); } /// /// Sets the transport address and port, and starts the local client connection. /// public bool StartConnection(string address, ushort port) { NetworkManager.TransportManager.Transport.SetClientAddress(address); NetworkManager.TransportManager.Transport.SetPort(port); return StartConnection(); } /// /// Called when a connection state changes for the local client. /// /// private void Transport_OnClientConnectionState(ClientConnectionStateArgs args) { LocalConnectionState state = args.ConnectionState; Started = (state == LocalConnectionState.Started); Objects.OnClientConnectionState(args); //Clear connection after so objects can update using current Connection value. if (!Started) { Connection = NetworkManager.EmptyConnection; NetworkManager.ClearClientsCollection(Clients); } else { _lastPacketTime = Time.unscaledTime; //Send version. PooledWriter writer = WriterPool.Retrieve(); writer.WritePacketIdUnpacked(PacketId.Version); writer.WriteString(NetworkManager.FISHNET_VERSION); NetworkManager.TransportManager.SendToServer((byte)Channel.Reliable, writer.GetArraySegment()); WriterPool.Store(writer); } if (NetworkManager.CanLog(LoggingType.Common)) { Transport t = NetworkManager.TransportManager.GetTransport(args.TransportIndex); string tName = (t == null) ? "Unknown" : t.GetType().Name; string socketInformation = string.Empty; if (state == LocalConnectionState.Starting) socketInformation = $" Server IP is {t.GetClientAddress()}, port is {t.GetPort()}."; NetworkManager.Log($"Local client is {state.ToString().ToLower()} for {tName}.{socketInformation}"); } NetworkManager.UpdateFramerate(); OnClientConnectionState?.Invoke(args); } /// /// Called when a socket receives data. /// private void Transport_OnClientReceivedData(ClientReceivedDataArgs args) { ParseReceived(args); } /// /// Called after IterateIncoming has completed. /// private void TransportManager_OnIterateIncomingEnd(bool server) { /* Should the last packet received be a spawn or despawn * then the cache won't yet be iterated because it only * iterates when a packet is anything but those two. Because * of such if any object caches did come in they must be iterated * at the end of the incoming cycle. This isn't as clean as I'd * like but it does ensure there will be no missing network object * references on spawned objects. */ if (Started && !server) Objects.IterateObjectCache(); } /// /// Parses received data. /// private void ParseReceived(ClientReceivedDataArgs args) { _lastPacketTime = Time.unscaledTime; ArraySegment segment; if (NetworkManager.TransportManager.HasIntermediateLayer) segment = NetworkManager.TransportManager.ProcessIntermediateIncoming(args.Data, true); else segment = args.Data; NetworkManager.StatisticsManager.NetworkTraffic.LocalClientReceivedData((ulong)segment.Count); if (segment.Count <= TransportManager.UNPACKED_TICK_LENGTH) return; PooledReader reader = ReaderPool.Retrieve(segment, NetworkManager, Reader.DataSource.Server); TimeManager tm = NetworkManager.TimeManager; tm.LastPacketTick.Update(reader.ReadTickUnpacked(), EstimatedTick.OldTickOption.Discard, false); ParseReader(reader, args.Channel); ReaderPool.Store(reader); } internal void ParseReader(PooledReader reader, Channel channel, bool print = false) { PacketId packetId = PacketId.Unset; #if !DEVELOPMENT try { #endif Reader.DataSource dataSource = Reader.DataSource.Server; /* This is a special condition where a message may arrive split. * When this occurs buffer each packet until all packets are * received. */ if (reader.PeekPacketId() == PacketId.Split) { #if DEVELOPMENT NetworkManager.PacketIdHistory.ReceivedPacket(PacketId.Split, packetFromServer: true); #endif //Skip packetId. reader.ReadPacketId(); int expectedMessages; _splitReader.GetHeader(reader, out expectedMessages); _splitReader.Write(NetworkManager.TimeManager.LastPacketTick.LastRemoteTick, reader, expectedMessages); /* If fullMessage returns 0 count then the split * has not written fully yet. Otherwise, if there is * data within then reinitialize reader with the * full message. */ ArraySegment fullMessage = _splitReader.GetFullMessage(); if (fullMessage.Count == 0) return; reader.Initialize(fullMessage, NetworkManager, dataSource); } while (reader.Remaining > 0) { packetId = reader.ReadPacketId(); #if DEVELOPMENT NetworkManager.PacketIdHistory.ReceivedPacket(packetId, packetFromServer: true); // if (!NetworkManager.IsServerStarted) // print = true; // if (print) // { // if (packetId == PacketId.ObserversRpc) // Debug.Log($"PacketId {packetId} - Remaining {reader.Remaining}."); // else // Debug.LogWarning($"PacketId {packetId} - Remaining {reader.Remaining}."); // } // print = false; #endif bool spawnOrDespawn = (packetId == PacketId.ObjectSpawn || packetId == PacketId.ObjectDespawn); /* Length of data. Only available if using unreliable. Unreliable packets * can arrive out of order which means object orientated messages such as RPCs may * arrive after the object for which they target has already been destroyed. When this happens * on lesser solutions they just dump the entire packet. However, since FishNet batches data. * it's very likely a packet will contain more than one packetId. With this mind, length is * sent as well so if any reason the data does have to be dumped it will only be dumped for * that single packetId but not the rest. Broadcasts don't need length either even if unreliable * because they are not object bound. */ //Is spawn or despawn; cache packet. if (spawnOrDespawn) { if (packetId == PacketId.ObjectSpawn) Objects.ReadSpawn(reader); else if (packetId == PacketId.ObjectDespawn) Objects.CacheDespawn(reader); } //Not spawn or despawn. else { /* Iterate object cache should any of the * incoming packets rely on it. Objects * in cache will always be received before any messages * that use them. */ Objects.IterateObjectCache(); //Then process packet normally. if ((ushort)packetId >= NetworkManager.StartingRpcLinkIndex) { Objects.ParseRpcLink(reader, (ushort)packetId, channel); } else if (packetId == PacketId.StateUpdate) { NetworkManager.PredictionManager.ParseStateUpdate(reader, channel); } else if (packetId == PacketId.Replicate) { Objects.ParseReplicateRpc(reader, null, channel); } else if (packetId == PacketId.Reconcile) { Objects.ParseReconcileRpc(reader, channel); } else if (packetId == PacketId.ObserversRpc) { Objects.ParseObserversRpc(reader, channel); } else if (packetId == PacketId.TargetRpc) { Objects.ParseTargetRpc(reader, channel); } else if (packetId == PacketId.Broadcast) { ParseBroadcast(reader, channel); } else if (packetId == PacketId.PingPong) { ParsePingPong(reader); } else if (packetId == PacketId.SyncType) { Objects.ParseSyncType(reader, channel); } else if (packetId == PacketId.PredictedSpawnResult) { Objects.ParsePredictedSpawnResult(reader); } else if (packetId == PacketId.TimingUpdate) { NetworkManager.TimeManager.ParseTimingUpdate(reader); } else if (packetId == PacketId.OwnershipChange) { Objects.ParseOwnershipChange(reader); } else if (packetId == PacketId.Authenticated) { ParseAuthenticated(reader); } else if (packetId == PacketId.Disconnect) { reader.Clear(); StopConnection(); } else if (packetId == PacketId.Version) { ParseVersion(reader); } else { NetworkManager.LogError($"Client received an unhandled PacketId of {(ushort)packetId} on channel {channel}. Remaining data has been purged."); #if DEVELOPMENT NetworkManager.LogError(NetworkManager.PacketIdHistory.GetReceivedPacketIds(packetsFromServer: true)); #endif return; } } #if DEVELOPMENT if (print) Debug.Log($"Reader remaining {reader.Remaining}"); #endif } /* Iterate cache when reader is emptied. * This is incase the last packet received * was a spawned, which wouldn't trigger * the above iteration. There's no harm * in doing this check multiple times as there's * an exit early check. */ Objects.IterateObjectCache(); #if !DEVELOPMENT } catch (Exception e) { NetworkManagerExtensions.LogError($"Client encountered an error while parsing data for packetId {packetId}. Message: {e.Message}."); } #endif } /// /// Parses a PingPong packet. /// /// private void ParsePingPong(PooledReader reader) { uint clientTick = reader.ReadTickUnpacked(); NetworkManager.TimeManager.ModifyPing(clientTick); } /// /// Parses a Version packet. /// /// private void ParseVersion(PooledReader reader) { IsServerDevelopment = reader.ReadBoolean(); } /// /// Parses a received connectionId. This is received before client receives connection state change. /// /// private void ParseAuthenticated(PooledReader reader) { NetworkManager networkManager = NetworkManager; int connectionId = reader.ReadNetworkConnectionId(); //If only a client then make a new connection. if (!networkManager.IsServerStarted) { Clients.TryGetValueIL2CPP(connectionId, out Connection); /* This is bad and should never happen unless the connection is dropping * while receiving authenticated. Would have to be a crazy race condition * but with the network anything is possible. */ if (Connection == null) { NetworkManager.LogWarning($"Client connection could not be found while parsing authenticated status. This usually occurs when the client is receiving a packet immediately before losing connection."); Connection = new(networkManager, connectionId, GetTransportIndex(), false); } } /* If also the server then use the servers connection * for the connectionId. This is to resolve host problems * where LocalConnection for client differs from the server Connection * reference, which results in different field values. */ else { if (networkManager.ServerManager.Clients.TryGetValueIL2CPP(connectionId, out NetworkConnection conn)) { Connection = conn; } else { networkManager.LogError($"Unable to lookup LocalConnection for {connectionId} as host."); Connection = new(networkManager, connectionId, GetTransportIndex(), false); } } //If predicted spawning is enabled also get reserved Ids. if (NetworkManager.ServerManager.GetAllowPredictedSpawning()) { int count = (int)reader.ReadSignedPackedWhole(); Queue q = Connection.PredictedObjectIds; for (int i = 0; i < count; i++) q.Enqueue(reader.ReadNetworkObjectId()); } /* Set the TimeManager tick to lastReceivedTick. * This still doesn't account for latency but * it's the best we can do until the client gets * a ping response. */ if (!networkManager.IsServerStarted) networkManager.TimeManager.Tick = networkManager.TimeManager.LastPacketTick.LastRemoteTick; //Mark as authenticated. Connection.ConnectionAuthenticated(); OnAuthenticated?.Invoke(); /* Register scene objects for all scenes * after being authenticated. This is done after * authentication rather than when the connection * is started because if also as server an online * scene may already be loaded on server, but not * for client. This means the sceneLoaded unity event * won't fire, and since client isn't authenticated * at the connection start phase objects won't be added. */ Objects.RegisterAndDespawnSceneObjects(); } /// /// Called when the TimeManager calls OnPostTick. /// private void TimeManager_OnPostTick() { CheckServerTimeout(); } /// /// Checks to timeout client connections. /// private void CheckServerTimeout() { /* Not connected or host. There should be no way * for server to drop and client not know about it as host. * This would mean a game crash or force close in which * the client would be gone as well anyway. */ if (!Started || NetworkManager.IsServerStarted) return; if (_remoteServerTimeout == RemoteTimeoutType.Disabled) return; #if DEVELOPMENT //If development but not set to development return. else if (_remoteServerTimeout != RemoteTimeoutType.Development) return; #endif //Wait two timing intervals to give packets a chance to come through. if (NetworkManager.SceneManager.IsIteratingQueue(2f)) return; /* ServerManager version only checks every so often * to perform iterations over time so the checks are not * impactful on the CPU. The client however can check every tick * since it's simple math. */ if (Time.unscaledTime - _lastPacketTime > _remoteServerTimeoutDuration) { OnClientTimeOut?.Invoke(); NetworkManager.Log($"Server has timed out. You can modify this feature on the ClientManager component."); StopConnection(); } } } }