using FishNet.Connection; using FishNet.Managing.Debugging; using FishNet.Managing.Logging; using FishNet.Managing.Server; using FishNet.Managing.Transporting; using FishNet.Serializing; using FishNet.Transporting; using FishNet.Utility.Extension; 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. /// /// Called after local client has authenticated. /// public event Action OnAuthenticated; /// /// 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; /// /// 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 Dictionary(); /// /// NetworkManager for client. /// [HideInInspector] public NetworkManager NetworkManager { get; private set; } #endregion #region Serialized. /// /// 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; /// /// /// [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; /// /// 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; #endregion #region Private. /// /// Used to read splits. /// private SplitReader _splitReader = new SplitReader(); #if UNITY_EDITOR || DEVELOPMENT_BUILD /// /// Logs data about parser to help debug. /// private ParseLogger _parseLogger = new ParseLogger(); #endif #endregion private void OnDestroy() { Objects?.SubscribeToSceneLoaded(false); } /// /// Initializes this script for use. /// /// internal void InitializeOnce_Internal(NetworkManager manager) { NetworkManager = manager; Objects = new ClientObjects(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) { //If connecting invoke after added to clients, otherwise invoke before removed. RemoteConnectionStateArgs rcs = new RemoteConnectionStateArgs((args.Connected) ? RemoteConnectionState.Started : RemoteConnectionState.Stopped, args.Id, -1); if (args.Connected) { Clients[args.Id] = new NetworkConnection(NetworkManager, args.Id, false); OnRemoteConnectionState?.Invoke(rcs); } else { OnRemoteConnectionState?.Invoke(rcs); if (Clients.TryGetValue(args.Id, out NetworkConnection c)) { c.Dispose(); Clients.Remove(args.Id); } } } /// /// Called when the server sends all currently connected clients. /// /// private void OnConnectedClientsBroadcast(ConnectedClientsBroadcast args) { NetworkManager.ClearClientsCollection(Clients); List collection = args.ListCache.Collection;// args.Ids; //No connected clients except self. if (collection == null) return; int count = collection.Count; for (int i = 0; i < count; i++) { int id = collection[i]; Clients[id] = new NetworkConnection(NetworkManager, id, false); } } /// /// 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; } else { NetworkManager.TransportManager.OnIterateIncomingEnd -= TransportManager_OnIterateIncomingEnd; NetworkManager.TransportManager.Transport.OnClientReceivedData -= Transport_OnClientReceivedData; NetworkManager.TransportManager.Transport.OnClientConnectionState -= Transport_OnClientConnectionState; } } /// /// Stops the local client connection. /// public void StopConnection() { NetworkManager.TransportManager.Transport.StopConnection(false); } /// /// Starts the local client connection. /// public void StartConnection() { NetworkManager.TransportManager.Transport.StartConnection(false); } /// /// Sets the transport address and starts the local client connection. /// public void StartConnection(string address) { StartConnection(address, NetworkManager.TransportManager.Transport.GetPort()); } /// /// Sets the transport address and port, and starts the local client connection. /// public void StartConnection(string address, ushort port) { NetworkManager.TransportManager.Transport.SetClientAddress(address); NetworkManager.TransportManager.Transport.SetPort(port); 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); } if (NetworkManager.CanLog(LoggingType.Common)) { Transport t = NetworkManager.TransportManager.GetTransport(args.TransportIndex); string tName = (t == null) ? "Unknown" : t.GetType().Name; Debug.Log($"Local client is {state.ToString().ToLower()} for {tName}."); } NetworkManager.UpdateFramerate(); OnClientConnectionState?.Invoke(args); } /// /// Called when a socket receives data. /// private void Transport_OnClientReceivedData(ClientReceivedDataArgs args) { args.Data = NetworkManager.TransportManager.ProcessIntermediateIncoming(args.Data, true); 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) { #if UNITY_EDITOR || DEVELOPMENT_BUILD _parseLogger.Reset(); #endif ArraySegment segment = args.Data; NetworkManager.StatisticsManager.NetworkTraffic.LocalClientReceivedData((ulong)segment.Count); if (segment.Count <= TransportManager.TICK_BYTES) return; PacketId packetId = PacketId.Unset; #if !UNITY_EDITOR && !DEVELOPMENT_BUILD try { #endif using (PooledReader reader = ReaderPool.GetReader(segment, NetworkManager)) { NetworkManager.TimeManager.LastPacketTick = reader.ReadUInt32(AutoPackType.Unpacked); /* 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) { //Skip packetId. reader.ReadPacketId(); int expectedMessages; _splitReader.GetHeader(reader, out expectedMessages); _splitReader.Write(NetworkManager.TimeManager.LastPacketTick, 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; //Initialize reader with full message. reader.Initialize(fullMessage, NetworkManager); } while (reader.Remaining > 0) { packetId = reader.ReadPacketId(); #if UNITY_EDITOR || DEVELOPMENT_BUILD _parseLogger.AddPacket(packetId); #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.CacheSpawn(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, args.Channel); } else if (packetId == PacketId.Reconcile) { Objects.ParseReconcileRpc(reader, args.Channel); } else if (packetId == PacketId.ObserversRpc) { Objects.ParseObserversRpc(reader, args.Channel); } else if (packetId == PacketId.TargetRpc) { Objects.ParseTargetRpc(reader, args.Channel); } else if (packetId == PacketId.Broadcast) { ParseBroadcast(reader, args.Channel); } else if (packetId == PacketId.PingPong) { ParsePingPong(reader); } else if (packetId == PacketId.SyncVar) { Objects.ParseSyncType(reader, false, args.Channel); } else if (packetId == PacketId.SyncObject) { Objects.ParseSyncType(reader, true, args.Channel); } else if (packetId == PacketId.PredictedSpawnResult) { Objects.ParsePredictedSpawnResult(reader); } else if (packetId == PacketId.TimingUpdate) { NetworkManager.TimeManager.ParseTimingUpdate(); } else if (packetId == PacketId.OwnershipChange) { Objects.ParseOwnershipChange(reader); } else if (packetId == PacketId.Authenticated) { ParseAuthenticated(reader); } else if (packetId == PacketId.Disconnect) { reader.Clear(); StopConnection(); } else { NetworkManager.LogError($"Client received an unhandled PacketId of {(ushort)packetId}. Remaining data has been purged."); #if UNITY_EDITOR || DEVELOPMENT_BUILD _parseLogger.Print(NetworkManager); #endif return; } } } /* 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 !UNITY_EDITOR && !DEVELOPMENT_BUILD } catch (Exception e) { if (NetworkManager.CanLog(LoggingType.Error)) Debug.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.ReadUInt32(AutoPackType.Unpacked); NetworkManager.TimeManager.ModifyPing(clientTick); } /// /// 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.IsServer) { Clients.TryGetValueIL2CPP(connectionId, out Connection); } /* 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 NetworkConnection(networkManager, connectionId, false); } } //If predicted spawning is enabled also get reserved Ids. if (NetworkManager.PredictionManager.GetAllowPredictedSpawning()) { byte count = reader.ReadByte(); 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. */ networkManager.TimeManager.Tick = networkManager.TimeManager.LastPacketTick; //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(); } } }