using System; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace Mirror { public enum ConnectState { None, // connecting between Connect() and OnTransportConnected() Connecting, Connected, // disconnecting between Disconnect() and OnTransportDisconnected() Disconnecting, Disconnected } /// NetworkClient with connection to server. public static class NetworkClient { // message handlers by messageId internal static readonly Dictionary handlers = new Dictionary(); /// Client's NetworkConnection to server. public static NetworkConnection connection { get; internal set; } /// True if client is ready (= joined world). // TODO redundant state. point it to .connection.isReady instead (& test) // TODO OR remove NetworkConnection.isReady? unless it's used on server // // TODO maybe ClientState.Connected/Ready/AddedPlayer/etc.? // way better for security if we can check states in callbacks public static bool ready; /// The NetworkConnection object that is currently "ready". // TODO this is from UNET. it's redundant and we should probably obsolete it. // Deprecated 2021-03-10 [Obsolete("NetworkClient.readyConnection is redundant. Use NetworkClient.connection and use NetworkClient.ready to check if it's ready.")] public static NetworkConnection readyConnection => ready ? connection : null; /// NetworkIdentity of the localPlayer public static NetworkIdentity localPlayer { get; internal set; } // NetworkClient state internal static ConnectState connectState = ConnectState.None; /// IP address of the connection to server. // empty if the client has not connected yet. public static string serverIp => connection.address; /// active is true while a client is connecting/connected // (= while the network is active) public static bool active => connectState == ConnectState.Connecting || connectState == ConnectState.Connected; /// Check if client is connecting (before connected). public static bool isConnecting => connectState == ConnectState.Connecting; /// Check if client is connected (after connecting). public static bool isConnected => connectState == ConnectState.Connected; /// True if client is running in host mode. public static bool isHostClient => connection is LocalConnectionToServer; // Deprecated 2021-05-26 [Obsolete("isLocalClient was renamed to isHostClient because that's what it actually means.")] public static bool isLocalClient => isHostClient; // OnConnected / OnDisconnected used to be NetworkMessages that were // invoked. this introduced a bug where external clients could send // Connected/Disconnected messages over the network causing undefined // behaviour. // => public so that custom NetworkManagers can hook into it public static Action OnConnectedEvent; public static Action OnDisconnectedEvent; public static Action OnErrorEvent; /// Registered spawnable prefabs by assetId. public static readonly Dictionary prefabs = new Dictionary(); // spawn handlers internal static readonly Dictionary spawnHandlers = new Dictionary(); internal static readonly Dictionary unspawnHandlers = new Dictionary(); // spawning static bool isSpawnFinished; // Disabled scene objects that can be spawned again, by sceneId. internal static readonly Dictionary spawnableObjects = new Dictionary(); static Unbatcher unbatcher = new Unbatcher(); // interest management component (optional) // only needed for SetHostVisibility public static InterestManagement aoi; // scene loading public static bool isLoadingScene; // initialization ////////////////////////////////////////////////////// static void AddTransportHandlers() { Transport.activeTransport.OnClientConnected = OnTransportConnected; Transport.activeTransport.OnClientDataReceived = OnTransportData; Transport.activeTransport.OnClientDisconnected = OnTransportDisconnected; Transport.activeTransport.OnClientError = OnError; } internal static void RegisterSystemHandlers(bool hostMode) { // host mode client / regular client react to some messages differently. // but we still need to add handlers for all of them to avoid // 'message id not found' errors. if (hostMode) { RegisterHandler(OnHostClientObjectDestroy); RegisterHandler(OnHostClientObjectHide); RegisterHandler(msg => {}, false); RegisterHandler(OnHostClientSpawn); // host mode doesn't need spawning RegisterHandler(msg => {}); // host mode doesn't need spawning RegisterHandler(msg => {}); // host mode doesn't need state updates RegisterHandler(msg => {}); } else { RegisterHandler(OnObjectDestroy); RegisterHandler(OnObjectHide); RegisterHandler(NetworkTime.OnClientPong, false); RegisterHandler(OnSpawn); RegisterHandler(OnObjectSpawnStarted); RegisterHandler(OnObjectSpawnFinished); RegisterHandler(OnEntityStateMessage); } RegisterHandler(OnRPCMessage); } // connect ///////////////////////////////////////////////////////////// /// Connect client to a NetworkServer by address. public static void Connect(string address) { // Debug.Log("Client Connect: " + address); Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.activeTransport' first"); RegisterSystemHandlers(false); Transport.activeTransport.enabled = true; AddTransportHandlers(); connectState = ConnectState.Connecting; Transport.activeTransport.ClientConnect(address); connection = new NetworkConnectionToServer(); } /// Connect client to a NetworkServer by Uri. public static void Connect(Uri uri) { // Debug.Log("Client Connect: " + uri); Debug.Assert(Transport.activeTransport != null, "There was no active transport when calling NetworkClient.Connect, If you are calling Connect manually then make sure to set 'Transport.activeTransport' first"); RegisterSystemHandlers(false); Transport.activeTransport.enabled = true; AddTransportHandlers(); connectState = ConnectState.Connecting; Transport.activeTransport.ClientConnect(uri); connection = new NetworkConnectionToServer(); } // TODO why are there two connect host methods? // called from NetworkManager.FinishStartHost() public static void ConnectHost() { //Debug.Log("Client Connect Host to Server"); RegisterSystemHandlers(true); connectState = ConnectState.Connected; // create local connection objects and connect them LocalConnectionToServer connectionToServer = new LocalConnectionToServer(); LocalConnectionToClient connectionToClient = new LocalConnectionToClient(); connectionToServer.connectionToClient = connectionToClient; connectionToClient.connectionToServer = connectionToServer; connection = connectionToServer; // create server connection to local client NetworkServer.SetLocalConnection(connectionToClient); } /// Connect host mode // called from NetworkManager.StartHostClient // TODO why are there two connect host methods? public static void ConnectLocalServer() { // call server OnConnected with server's connection to client NetworkServer.OnConnected(NetworkServer.localConnection); // call client OnConnected with client's connection to server // => previously we used to send a ConnectMessage to // NetworkServer.localConnection. this would queue the message // until NetworkClient.Update processes it. // => invoking the client's OnConnected event directly here makes // tests fail. so let's do it exactly the same order as before by // queueing the event for next Update! //OnConnectedEvent?.Invoke(connection); ((LocalConnectionToServer)connection).QueueConnectedEvent(); } // disconnect ////////////////////////////////////////////////////////// /// Disconnect from server. public static void Disconnect() { // only if connected or connecting. // don't disconnect() again if already in the process of // disconnecting or fully disconnected. if (connectState != ConnectState.Connecting && connectState != ConnectState.Connected) return; // we are disconnecting until OnTransportDisconnected is called. // setting state to Disconnected would stop OnTransportDisconnected // from calling cleanup code because it would think we are already // disconnected fully. // TODO move to 'cleanup' code below if safe connectState = ConnectState.Disconnecting; ready = false; // call Disconnect on the NetworkConnection connection?.Disconnect(); // IMPORTANT: do NOT clear connection here yet. // we still need it in OnTransportDisconnected for callbacks. // connection = null; } /// Disconnect host mode. // this is needed to call DisconnectMessage for the host client too. // Deprecated 2021-05-11 [Obsolete("Call NetworkClient.Disconnect() instead. Nobody should use DisconnectLocalServer.")] public static void DisconnectLocalServer() { // only if host connection is running if (NetworkServer.localConnection != null) { // TODO ConnectLocalServer manually sends a ConnectMessage to the // local connection. should we send a DisconnectMessage here too? // (if we do then we get an Unknown Message ID log) //NetworkServer.localConnection.Send(new DisconnectMessage()); NetworkServer.OnTransportDisconnected(NetworkServer.localConnection.connectionId); } } // transport events //////////////////////////////////////////////////// // called by Transport static void OnTransportConnected() { if (connection != null) { // reset network time stats NetworkTime.Reset(); // reset unbatcher in case any batches from last session remain. unbatcher = new Unbatcher(); // the handler may want to send messages to the client // thus we should set the connected state before calling the handler connectState = ConnectState.Connected; NetworkTime.UpdateClient(); OnConnectedEvent?.Invoke(); } else Debug.LogError("Skipped Connect message handling because connection is null."); } // helper function static bool UnpackAndInvoke(NetworkReader reader, int channelId) { if (MessagePacking.Unpack(reader, out ushort msgType)) { // try to invoke the handler for that message if (handlers.TryGetValue(msgType, out NetworkMessageDelegate handler)) { handler.Invoke(connection, reader, channelId); // message handler may disconnect client, making connection = null // therefore must check for null to avoid NRE. if (connection != null) connection.lastMessageTime = Time.time; return true; } else { // Debug.Log("Unknown message ID " + msgType + " " + this + ". May be due to no existing RegisterHandler for this message."); return false; } } else { Debug.LogError("Closed connection: " + connection + ". Invalid message header."); connection.Disconnect(); return false; } } // called by Transport internal static void OnTransportData(ArraySegment data, int channelId) { if (connection != null) { // server might batch multiple messages into one packet. // feed it to the Unbatcher. // NOTE: we don't need to associate a channelId because we // always process all messages in the batch. if (!unbatcher.AddBatch(data)) { Debug.LogWarning($"NetworkClient: failed to add batch, disconnecting."); connection.Disconnect(); return; } // process all messages in the batch. // only while NOT loading a scene. // if we get a scene change message, then we need to stop // processing. otherwise we might apply them to the old scene. // => fixes https://github.com/vis2k/Mirror/issues/2651 // // NOTE: is scene starts loading, then the rest of the batch // would only be processed when OnTransportData is called // the next time. // => consider moving processing to NetworkEarlyUpdate. while (!isLoadingScene && unbatcher.GetNextMessage(out NetworkReader reader, out double remoteTimestamp)) { // enough to read at least header size? if (reader.Remaining >= MessagePacking.HeaderSize) { // make remoteTimeStamp available to the user connection.remoteTimeStamp = remoteTimestamp; // handle message if (!UnpackAndInvoke(reader, channelId)) break; } // otherwise disconnect else { Debug.LogError($"NetworkClient: received Message was too short (messages should start with message id)"); connection.Disconnect(); return; } } } else Debug.LogError("Skipped Data message handling because connection is null."); } // called by Transport // IMPORTANT: often times when disconnecting, we call this from Mirror // too because we want to remove the connection and handle // the disconnect immediately. // => which is fine as long as we guarantee it only runs once // => which we do by setting the state to Disconnected! internal static void OnTransportDisconnected() { // StopClient called from user code triggers Disconnected event // from transport which calls StopClient again, so check here // and short circuit running the Shutdown process twice. if (connectState == ConnectState.Disconnected) return; // Raise the event before changing ConnectState // because 'active' depends on this during shutdown if (connection != null) OnDisconnectedEvent?.Invoke(); connectState = ConnectState.Disconnected; ready = false; // now that everything was handled, clear the connection. // previously this was done in Disconnect() already, but we still // need it for the above OnDisconnectedEvent. connection = null; } static void OnError(Exception exception) { Debug.LogException(exception); OnErrorEvent?.Invoke(exception); } // send //////////////////////////////////////////////////////////////// /// Send a NetworkMessage to the server over the given channel. public static void Send(T message, int channelId = Channels.Reliable) where T : struct, NetworkMessage { if (connection != null) { if (connectState == ConnectState.Connected) { connection.Send(message, channelId); } else Debug.LogError("NetworkClient Send when not connected to a server"); } else Debug.LogError("NetworkClient Send with no connection"); } // message handlers //////////////////////////////////////////////////// /// Register a handler for a message type T. Most should require authentication. // Deprecated 2021-03-13 [Obsolete("Use RegisterHandler version without NetworkConnection parameter. It always points to NetworkClient.connection anyway.")] public static void RegisterHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { ushort msgType = MessagePacking.GetId(); if (handlers.ContainsKey(msgType)) { Debug.LogWarning($"NetworkClient.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); } handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); } /// Register a handler for a message type T. Most should require authentication. public static void RegisterHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { ushort msgType = MessagePacking.GetId(); if (handlers.ContainsKey(msgType)) { Debug.LogWarning($"NetworkClient.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); } // we use the same WrapHandler function for server and client. // so let's wrap it to ignore the NetworkConnection parameter. // it's not needed on client. it's always NetworkClient.connection. void HandlerWrapped(NetworkConnection _, T value) => handler(value); handlers[msgType] = MessagePacking.WrapHandler((Action) HandlerWrapped, requireAuthentication); } /// Replace a handler for a particular message type. Should require authentication by default. // TODO does anyone even use that? consider removing public static void ReplaceHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { ushort msgType = MessagePacking.GetId(); handlers[msgType] = MessagePacking.WrapHandler(handler, requireAuthentication); } /// Replace a handler for a particular message type. Should require authentication by default. // TODO does anyone even use that? consider removing public static void ReplaceHandler(Action handler, bool requireAuthentication = true) where T : struct, NetworkMessage { ReplaceHandler((NetworkConnection _, T value) => { handler(value); }, requireAuthentication); } /// Unregister a message handler of type T. public static bool UnregisterHandler() where T : struct, NetworkMessage { // use int to minimize collisions ushort msgType = MessagePacking.GetId(); return handlers.Remove(msgType); } // spawnable prefabs /////////////////////////////////////////////////// /// Find the registered prefab for this asset id. // Useful for debuggers public static bool GetPrefab(Guid assetId, out GameObject prefab) { prefab = null; return assetId != Guid.Empty && prefabs.TryGetValue(assetId, out prefab) && prefab != null; } /// Validates Prefab then adds it to prefabs dictionary. static void RegisterPrefabIdentity(NetworkIdentity prefab) { if (prefab.assetId == Guid.Empty) { Debug.LogError($"Can not Register '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead"); return; } if (prefab.sceneId != 0) { Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene."); return; } NetworkIdentity[] identities = prefab.GetComponentsInChildren(); if (identities.Length > 1) { Debug.LogError($"Prefab '{prefab.name}' has multiple NetworkIdentity components. There should only be one NetworkIdentity on a prefab, and it must be on the root object."); } if (prefabs.ContainsKey(prefab.assetId)) { GameObject existingPrefab = prefabs[prefab.assetId]; Debug.LogWarning($"Replacing existing prefab with assetId '{prefab.assetId}'. Old prefab '{existingPrefab.name}', New prefab '{prefab.name}'"); } if (spawnHandlers.ContainsKey(prefab.assetId) || unspawnHandlers.ContainsKey(prefab.assetId)) { Debug.LogWarning($"Adding prefab '{prefab.name}' with assetId '{prefab.assetId}' when spawnHandlers with same assetId already exists."); } // Debug.Log($"Registering prefab '{prefab.name}' as asset:{prefab.assetId}"); prefabs[prefab.assetId] = prefab.gameObject; } /// Register spawnable prefab with custom assetId. // Note: newAssetId can not be set on GameObjects that already have an assetId // Note: registering with assetId is useful for assetbundles etc. a lot // of people use this. public static void RegisterPrefab(GameObject prefab, Guid newAssetId) { if (prefab == null) { Debug.LogError("Could not register prefab because it was null"); return; } if (newAssetId == Guid.Empty) { Debug.LogError($"Could not register '{prefab.name}' with new assetId because the new assetId was empty"); return; } NetworkIdentity identity = prefab.GetComponent(); if (identity == null) { Debug.LogError($"Could not register '{prefab.name}' since it contains no NetworkIdentity component"); return; } if (identity.assetId != Guid.Empty && identity.assetId != newAssetId) { Debug.LogError($"Could not register '{prefab.name}' to {newAssetId} because it already had an AssetId, Existing assetId {identity.assetId}"); return; } identity.assetId = newAssetId; RegisterPrefabIdentity(identity); } /// Register spawnable prefab. public static void RegisterPrefab(GameObject prefab) { if (prefab == null) { Debug.LogError("Could not register prefab because it was null"); return; } NetworkIdentity identity = prefab.GetComponent(); if (identity == null) { Debug.LogError($"Could not register '{prefab.name}' since it contains no NetworkIdentity component"); return; } RegisterPrefabIdentity(identity); } /// Register a spawnable prefab with custom assetId and custom spawn/unspawn handlers. // Note: newAssetId can not be set on GameObjects that already have an assetId // Note: registering with assetId is useful for assetbundles etc. a lot // of people use this. // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate? public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) { // We need this check here because we don't want a null handler in the lambda expression below if (spawnHandler == null) { Debug.LogError($"Can not Register null SpawnHandler for {newAssetId}"); return; } RegisterPrefab(prefab, newAssetId, msg => spawnHandler(msg.position, msg.assetId), unspawnHandler); } /// Register a spawnable prefab with custom spawn/unspawn handlers. // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate? public static void RegisterPrefab(GameObject prefab, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) { if (prefab == null) { Debug.LogError("Could not register handler for prefab because the prefab was null"); return; } NetworkIdentity identity = prefab.GetComponent(); if (identity == null) { Debug.LogError("Could not register handler for '" + prefab.name + "' since it contains no NetworkIdentity component"); return; } if (identity.sceneId != 0) { Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene."); return; } Guid assetId = identity.assetId; if (assetId == Guid.Empty) { Debug.LogError($"Can not Register handler for '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead"); return; } // We need this check here because we don't want a null handler in the lambda expression below if (spawnHandler == null) { Debug.LogError($"Can not Register null SpawnHandler for {assetId}"); return; } RegisterPrefab(prefab, msg => spawnHandler(msg.position, msg.assetId), unspawnHandler); } /// Register a spawnable prefab with custom assetId and custom spawn/unspawn handlers. // Note: newAssetId can not be set on GameObjects that already have an assetId // Note: registering with assetId is useful for assetbundles etc. a lot // of people use this. // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate? public static void RegisterPrefab(GameObject prefab, Guid newAssetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler) { if (newAssetId == Guid.Empty) { Debug.LogError($"Could not register handler for '{prefab.name}' with new assetId because the new assetId was empty"); return; } if (prefab == null) { Debug.LogError("Could not register handler for prefab because the prefab was null"); return; } NetworkIdentity identity = prefab.GetComponent(); if (identity == null) { Debug.LogError("Could not register handler for '" + prefab.name + "' since it contains no NetworkIdentity component"); return; } if (identity.assetId != Guid.Empty && identity.assetId != newAssetId) { Debug.LogError($"Could not register Handler for '{prefab.name}' to {newAssetId} because it already had an AssetId, Existing assetId {identity.assetId}"); return; } if (identity.sceneId != 0) { Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene."); return; } identity.assetId = newAssetId; Guid assetId = identity.assetId; if (spawnHandler == null) { Debug.LogError($"Can not Register null SpawnHandler for {assetId}"); return; } if (unspawnHandler == null) { Debug.LogError($"Can not Register null UnSpawnHandler for {assetId}"); return; } if (spawnHandlers.ContainsKey(assetId) || unspawnHandlers.ContainsKey(assetId)) { Debug.LogWarning($"Replacing existing spawnHandlers for prefab '{prefab.name}' with assetId '{assetId}'"); } if (prefabs.ContainsKey(assetId)) { // this is error because SpawnPrefab checks prefabs before handler Debug.LogError($"assetId '{assetId}' is already used by prefab '{prefabs[assetId].name}', unregister the prefab first before trying to add handler"); } NetworkIdentity[] identities = prefab.GetComponentsInChildren(); if (identities.Length > 1) { Debug.LogError($"Prefab '{prefab.name}' has multiple NetworkIdentity components. There should only be one NetworkIdentity on a prefab, and it must be on the root object."); } // Debug.Log("Registering custom prefab '" + prefab.name + "' as asset:" + assetId + " " + spawnHandler.GetMethodName() + "/" + unspawnHandler.GetMethodName()); spawnHandlers[assetId] = spawnHandler; unspawnHandlers[assetId] = unspawnHandler; } /// Register a spawnable prefab with custom spawn/unspawn handlers. // TODO why do we have one with SpawnDelegate and one with SpawnHandlerDelegate? public static void RegisterPrefab(GameObject prefab, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler) { if (prefab == null) { Debug.LogError("Could not register handler for prefab because the prefab was null"); return; } NetworkIdentity identity = prefab.GetComponent(); if (identity == null) { Debug.LogError("Could not register handler for '" + prefab.name + "' since it contains no NetworkIdentity component"); return; } if (identity.sceneId != 0) { Debug.LogError($"Can not Register '{prefab.name}' because it has a sceneId, make sure you are passing in the original prefab and not an instance in the scene."); return; } Guid assetId = identity.assetId; if (assetId == Guid.Empty) { Debug.LogError($"Can not Register handler for '{prefab.name}' because it had empty assetid. If this is a scene Object use RegisterSpawnHandler instead"); return; } if (spawnHandler == null) { Debug.LogError($"Can not Register null SpawnHandler for {assetId}"); return; } if (unspawnHandler == null) { Debug.LogError($"Can not Register null UnSpawnHandler for {assetId}"); return; } if (spawnHandlers.ContainsKey(assetId) || unspawnHandlers.ContainsKey(assetId)) { Debug.LogWarning($"Replacing existing spawnHandlers for prefab '{prefab.name}' with assetId '{assetId}'"); } if (prefabs.ContainsKey(assetId)) { // this is error because SpawnPrefab checks prefabs before handler Debug.LogError($"assetId '{assetId}' is already used by prefab '{prefabs[assetId].name}', unregister the prefab first before trying to add handler"); } NetworkIdentity[] identities = prefab.GetComponentsInChildren(); if (identities.Length > 1) { Debug.LogError($"Prefab '{prefab.name}' has multiple NetworkIdentity components. There should only be one NetworkIdentity on a prefab, and it must be on the root object."); } // Debug.Log("Registering custom prefab '" + prefab.name + "' as asset:" + assetId + " " + spawnHandler.GetMethodName() + "/" + unspawnHandler.GetMethodName()); spawnHandlers[assetId] = spawnHandler; unspawnHandlers[assetId] = unspawnHandler; } /// Removes a registered spawn prefab that was setup with NetworkClient.RegisterPrefab. public static void UnregisterPrefab(GameObject prefab) { if (prefab == null) { Debug.LogError("Could not unregister prefab because it was null"); return; } NetworkIdentity identity = prefab.GetComponent(); if (identity == null) { Debug.LogError("Could not unregister '" + prefab.name + "' since it contains no NetworkIdentity component"); return; } Guid assetId = identity.assetId; prefabs.Remove(assetId); spawnHandlers.Remove(assetId); unspawnHandlers.Remove(assetId); } // spawn handlers ////////////////////////////////////////////////////// /// This is an advanced spawning function that registers a custom assetId with the spawning system. // This can be used to register custom spawning methods for an assetId - // instead of the usual method of registering spawning methods for a // prefab. This should be used when no prefab exists for the spawned // objects - such as when they are constructed dynamically at runtime // from configuration data. public static void RegisterSpawnHandler(Guid assetId, SpawnDelegate spawnHandler, UnSpawnDelegate unspawnHandler) { // We need this check here because we don't want a null handler in the lambda expression below if (spawnHandler == null) { Debug.LogError($"Can not Register null SpawnHandler for {assetId}"); return; } RegisterSpawnHandler(assetId, msg => spawnHandler(msg.position, msg.assetId), unspawnHandler); } /// This is an advanced spawning function that registers a custom assetId with the spawning system. // This can be used to register custom spawning methods for an assetId - // instead of the usual method of registering spawning methods for a // prefab. This should be used when no prefab exists for the spawned // objects - such as when they are constructed dynamically at runtime // from configuration data. public static void RegisterSpawnHandler(Guid assetId, SpawnHandlerDelegate spawnHandler, UnSpawnDelegate unspawnHandler) { if (spawnHandler == null) { Debug.LogError($"Can not Register null SpawnHandler for {assetId}"); return; } if (unspawnHandler == null) { Debug.LogError($"Can not Register null UnSpawnHandler for {assetId}"); return; } if (assetId == Guid.Empty) { Debug.LogError("Can not Register SpawnHandler for empty Guid"); return; } if (spawnHandlers.ContainsKey(assetId) || unspawnHandlers.ContainsKey(assetId)) { Debug.LogWarning($"Replacing existing spawnHandlers for {assetId}"); } if (prefabs.ContainsKey(assetId)) { // this is error because SpawnPrefab checks prefabs before handler Debug.LogError($"assetId '{assetId}' is already used by prefab '{prefabs[assetId].name}'"); } // Debug.Log("RegisterSpawnHandler asset '" + assetId + "' " + spawnHandler.GetMethodName() + "/" + unspawnHandler.GetMethodName()); spawnHandlers[assetId] = spawnHandler; unspawnHandlers[assetId] = unspawnHandler; } /// Removes a registered spawn handler function that was registered with NetworkClient.RegisterHandler(). public static void UnregisterSpawnHandler(Guid assetId) { spawnHandlers.Remove(assetId); unspawnHandlers.Remove(assetId); } /// This clears the registered spawn prefabs and spawn handler functions for this client. public static void ClearSpawners() { prefabs.Clear(); spawnHandlers.Clear(); unspawnHandlers.Clear(); } internal static bool InvokeUnSpawnHandler(Guid assetId, GameObject obj) { if (unspawnHandlers.TryGetValue(assetId, out UnSpawnDelegate handler) && handler != null) { handler(obj); return true; } return false; } // ready /////////////////////////////////////////////////////////////// /// Sends Ready message to server, indicating that we loaded the scene, ready to enter the game. // This could be for example when a client enters an ongoing game and // has finished loading the current scene. The server should respond to // the SYSTEM_READY event with an appropriate handler which instantiates // the players object for example. public static bool Ready() { // Debug.Log("NetworkClient.Ready() called with connection [" + conn + "]"); if (ready) { Debug.LogError("NetworkClient is already ready. It shouldn't be called twice."); return false; } // need a valid connection to become ready if (connection == null) { Debug.LogError("Ready() called with invalid connection object: conn=null"); return false; } // Set these before sending the ReadyMessage, otherwise host client // will fail in InternalAddPlayer with null readyConnection. // TODO this is redundant. have one source of truth for .ready ready = true; connection.isReady = true; // Tell server we're ready to have a player object spawned connection.Send(new ReadyMessage()); return true; } // Deprecated 2021-03-13 [Obsolete("NetworkClient.Ready doesn't need a NetworkConnection parameter anymore. It always uses NetworkClient.connection anyway.")] public static bool Ready(NetworkConnection conn) => Ready(); // add player ////////////////////////////////////////////////////////// // called from message handler for Owner message internal static void InternalAddPlayer(NetworkIdentity identity) { //Debug.Log("NetworkClient.InternalAddPlayer"); // NOTE: It can be "normal" when changing scenes for the player to be destroyed and recreated. // But, the player structures are not cleaned up, we'll just replace the old player localPlayer = identity; // NOTE: we DONT need to set isClient=true here, because OnStartClient // is called before OnStartLocalPlayer, hence it's already set. // localPlayer.isClient = true; // TODO this check might not be necessary //if (readyConnection != null) if (ready && connection != null) { connection.identity = identity; } else Debug.LogWarning("No ready connection found for setting player controller during InternalAddPlayer"); } /// Sends AddPlayer message to the server, indicating that we want to join the world. public static bool AddPlayer() { // ensure valid ready connection if (connection == null) { Debug.LogError("AddPlayer requires a valid NetworkClient.connection."); return false; } // UNET checked 'if readyConnection != null'. // in other words, we need a connection and we need to be ready. if (!ready) { Debug.LogError("AddPlayer requires a ready NetworkClient."); return false; } if (connection.identity != null) { Debug.LogError("NetworkClient.AddPlayer: a PlayerController was already added. Did you call AddPlayer twice?"); return false; } // Debug.Log("NetworkClient.AddPlayer() called with connection [" + readyConnection + "]"); connection.Send(new AddPlayerMessage()); return true; } // Deprecated 2021-03-13 [Obsolete("NetworkClient.AddPlayer doesn't need a NetworkConnection parameter anymore. It always uses NetworkClient.connection anyway.")] public static bool AddPlayer(NetworkConnection readyConn) => AddPlayer(); // spawning //////////////////////////////////////////////////////////// internal static void ApplySpawnPayload(NetworkIdentity identity, SpawnMessage message) { if (message.assetId != Guid.Empty) identity.assetId = message.assetId; if (!identity.gameObject.activeSelf) { identity.gameObject.SetActive(true); } // apply local values for VR support identity.transform.localPosition = message.position; identity.transform.localRotation = message.rotation; identity.transform.localScale = message.scale; identity.hasAuthority = message.isOwner; identity.netId = message.netId; if (message.isLocalPlayer) InternalAddPlayer(identity); // deserialize components if any payload // (Count is 0 if there were no components) if (message.payload.Count > 0) { using (PooledNetworkReader payloadReader = NetworkReaderPool.GetReader(message.payload)) { identity.OnDeserializeAllSafely(payloadReader, true); } } NetworkIdentity.spawned[message.netId] = identity; // objects spawned as part of initial state are started on a second pass if (isSpawnFinished) { identity.NotifyAuthority(); identity.OnStartClient(); CheckForLocalPlayer(identity); } } // Finds Existing Object with NetId or spawns a new one using AssetId or sceneId internal static bool FindOrSpawnObject(SpawnMessage message, out NetworkIdentity identity) { // was the object already spawned? identity = GetExistingObject(message.netId); // if found, return early if (identity != null) { return true; } if (message.assetId == Guid.Empty && message.sceneId == 0) { Debug.LogError($"OnSpawn message with netId '{message.netId}' has no AssetId or sceneId"); return false; } identity = message.sceneId == 0 ? SpawnPrefab(message) : SpawnSceneObject(message); if (identity == null) { Debug.LogError($"Could not spawn assetId={message.assetId} scene={message.sceneId:X} netId={message.netId}"); return false; } return true; } static NetworkIdentity GetExistingObject(uint netid) { NetworkIdentity.spawned.TryGetValue(netid, out NetworkIdentity localObject); return localObject; } static NetworkIdentity SpawnPrefab(SpawnMessage message) { if (GetPrefab(message.assetId, out GameObject prefab)) { GameObject obj = GameObject.Instantiate(prefab, message.position, message.rotation); //Debug.Log("Client spawn handler instantiating [netId:" + msg.netId + " asset ID:" + msg.assetId + " pos:" + msg.position + " rotation: " + msg.rotation + "]"); return obj.GetComponent(); } if (spawnHandlers.TryGetValue(message.assetId, out SpawnHandlerDelegate handler)) { GameObject obj = handler(message); if (obj == null) { Debug.LogError($"Spawn Handler returned null, Handler assetId '{message.assetId}'"); return null; } NetworkIdentity identity = obj.GetComponent(); if (identity == null) { Debug.LogError($"Object Spawned by handler did not have a NetworkIdentity, Handler assetId '{message.assetId}'"); return null; } return identity; } Debug.LogError($"Failed to spawn server object, did you forget to add it to the NetworkManager? assetId={message.assetId} netId={message.netId}"); return null; } static NetworkIdentity SpawnSceneObject(SpawnMessage message) { NetworkIdentity identity = GetAndRemoveSceneObject(message.sceneId); if (identity == null) { Debug.LogError($"Spawn scene object not found for {message.sceneId:X}. Make sure that client and server use exactly the same project. This only happens if the hierarchy gets out of sync."); // dump the whole spawnable objects dict for easier debugging //foreach (KeyValuePair kvp in spawnableObjects) // Debug.Log($"Spawnable: SceneId={kvp.Key:X} name={kvp.Value.name}"); } //else Debug.Log($"Client spawn for [netId:{msg.netId}] [sceneId:{msg.sceneId:X}] obj:{identity}"); return identity; } static NetworkIdentity GetAndRemoveSceneObject(ulong sceneId) { if (spawnableObjects.TryGetValue(sceneId, out NetworkIdentity identity)) { spawnableObjects.Remove(sceneId); return identity; } return null; } // Checks if identity is not spawned yet, not hidden and has sceneId static bool ConsiderForSpawning(NetworkIdentity identity) { // not spawned yet, not hidden, etc.? return !identity.gameObject.activeSelf && identity.gameObject.hideFlags != HideFlags.NotEditable && identity.gameObject.hideFlags != HideFlags.HideAndDontSave && identity.sceneId != 0; } /// Call this after loading/unloading a scene in the client after connection to register the spawnable objects public static void PrepareToSpawnSceneObjects() { // remove existing items, they will be re-added below spawnableObjects.Clear(); // finds all NetworkIdentity currently loaded by unity (includes disabled objects) NetworkIdentity[] allIdentities = Resources.FindObjectsOfTypeAll(); foreach (NetworkIdentity identity in allIdentities) { // add all unspawned NetworkIdentities to spawnable objects if (ConsiderForSpawning(identity)) { spawnableObjects.Add(identity.sceneId, identity); } } } internal static void OnObjectSpawnStarted(ObjectSpawnStartedMessage _) { // Debug.Log("SpawnStarted"); PrepareToSpawnSceneObjects(); isSpawnFinished = false; } internal static void OnObjectSpawnFinished(ObjectSpawnFinishedMessage _) { //Debug.Log("SpawnFinished"); ClearNullFromSpawned(); // paul: Initialize the objects in the same order as they were // initialized in the server. This is important if spawned objects // use data from scene objects foreach (NetworkIdentity identity in NetworkIdentity.spawned.Values.OrderBy(uv => uv.netId)) { identity.NotifyAuthority(); identity.OnStartClient(); CheckForLocalPlayer(identity); } isSpawnFinished = true; } static readonly List removeFromSpawned = new List(); static void ClearNullFromSpawned() { // spawned has null objects after changing scenes on client using // NetworkManager.ServerChangeScene remove them here so that 2nd // loop below does not get NullReferenceException // see https://github.com/vis2k/Mirror/pull/2240 // TODO fix scene logic so that client scene doesn't have null objects foreach (KeyValuePair kvp in NetworkIdentity.spawned) { if (kvp.Value == null) { removeFromSpawned.Add(kvp.Key); } } // can't modify NetworkIdentity.spawned inside foreach so need 2nd loop to remove foreach (uint id in removeFromSpawned) { NetworkIdentity.spawned.Remove(id); } removeFromSpawned.Clear(); } // host mode callbacks ///////////////////////////////////////////////// static void OnHostClientObjectDestroy(ObjectDestroyMessage message) { //Debug.Log($"NetworkClient.OnLocalObjectObjDestroy netId:{message.netId}"); // TODO why do we do this? // in host mode, .spawned is shared between server and client. // removing it on client would remove it on server. // huh. NetworkIdentity.spawned.Remove(message.netId); } static void OnHostClientObjectHide(ObjectHideMessage message) { //Debug.Log($"ClientScene::OnLocalObjectObjHide netId:{message.netId}"); if (NetworkIdentity.spawned.TryGetValue(message.netId, out NetworkIdentity localObject) && localObject != null) { // obsolete legacy system support (for now) #pragma warning disable 618 if (localObject.visibility != null) localObject.visibility.OnSetHostVisibility(false); #pragma warning restore 618 else if (aoi != null) aoi.SetHostVisibility(localObject, false); } } internal static void OnHostClientSpawn(SpawnMessage message) { if (NetworkIdentity.spawned.TryGetValue(message.netId, out NetworkIdentity localObject) && localObject != null) { if (message.isLocalPlayer) InternalAddPlayer(localObject); localObject.hasAuthority = message.isOwner; localObject.NotifyAuthority(); localObject.OnStartClient(); // obsolete legacy system support (for now) #pragma warning disable 618 if (localObject.visibility != null) localObject.visibility.OnSetHostVisibility(true); #pragma warning restore 618 else if (aoi != null) aoi.SetHostVisibility(localObject, true); CheckForLocalPlayer(localObject); } } // client-only mode callbacks ////////////////////////////////////////// static void OnEntityStateMessage(EntityStateMessage message) { // Debug.Log("NetworkClient.OnUpdateVarsMessage " + msg.netId); if (NetworkIdentity.spawned.TryGetValue(message.netId, out NetworkIdentity localObject) && localObject != null) { using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(message.payload)) localObject.OnDeserializeAllSafely(networkReader, false); } else Debug.LogWarning("Did not find target for sync message for " + message.netId + " . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); } static void OnRPCMessage(RpcMessage message) { // Debug.Log("NetworkClient.OnRPCMessage hash:" + msg.functionHash + " netId:" + msg.netId); if (NetworkIdentity.spawned.TryGetValue(message.netId, out NetworkIdentity identity)) { using (PooledNetworkReader networkReader = NetworkReaderPool.GetReader(message.payload)) identity.HandleRemoteCall(message.componentIndex, message.functionHash, MirrorInvokeType.ClientRpc, networkReader); } } static void OnObjectHide(ObjectHideMessage message) => DestroyObject(message.netId); internal static void OnObjectDestroy(ObjectDestroyMessage message) => DestroyObject(message.netId); internal static void OnSpawn(SpawnMessage message) { // Debug.Log($"Client spawn handler instantiating netId={msg.netId} assetID={msg.assetId} sceneId={msg.sceneId:X} pos={msg.position}"); if (FindOrSpawnObject(message, out NetworkIdentity identity)) { ApplySpawnPayload(identity, message); } } internal static void CheckForLocalPlayer(NetworkIdentity identity) { if (identity == localPlayer) { // Set isLocalPlayer to true on this NetworkIdentity and trigger // OnStartLocalPlayer in all scripts on the same GO identity.connectionToServer = connection; identity.OnStartLocalPlayer(); // Debug.Log("NetworkClient.OnOwnerMessage - player=" + identity.name); } } // destroy ///////////////////////////////////////////////////////////// static void DestroyObject(uint netId) { // Debug.Log("NetworkClient.OnObjDestroy netId:" + netId); if (NetworkIdentity.spawned.TryGetValue(netId, out NetworkIdentity localObject) && localObject != null) { localObject.OnStopClient(); // user handling if (InvokeUnSpawnHandler(localObject.assetId, localObject.gameObject)) { // reset object after user's handler localObject.Reset(); } // default handling else if (localObject.sceneId == 0) { // don't call reset before destroy so that values are still set in OnDestroy GameObject.Destroy(localObject.gameObject); } // scene object.. disable it in scene instead of destroying else { localObject.gameObject.SetActive(false); spawnableObjects[localObject.sceneId] = localObject; // reset for scene objects localObject.Reset(); } // remove from dictionary no matter how it is unspawned NetworkIdentity.spawned.Remove(netId); } //else Debug.LogWarning("Did not find target for destroy message for " + netId); } // update ////////////////////////////////////////////////////////////// // NetworkEarlyUpdate called before any Update/FixedUpdate // (we add this to the UnityEngine in NetworkLoop) internal static void NetworkEarlyUpdate() { // process all incoming messages first before updating the world if (Transport.activeTransport != null) Transport.activeTransport.ClientEarlyUpdate(); } // NetworkLateUpdate called after any Update/FixedUpdate/LateUpdate // (we add this to the UnityEngine in NetworkLoop) internal static void NetworkLateUpdate() { // local connection? if (connection is LocalConnectionToServer localConnection) { localConnection.Update(); } // remote connection? else if (connection is NetworkConnectionToServer remoteConnection) { // only update things while connected if (active && connectState == ConnectState.Connected) { // update NetworkTime NetworkTime.UpdateClient(); // update connection to flush out batched messages remoteConnection.Update(); } } // process all outgoing messages after updating the world if (Transport.activeTransport != null) Transport.activeTransport.ClientLateUpdate(); } // obsolete to not break people's projects. Update was public. // Deprecated 2021-03-02 [Obsolete("NetworkClient.Update is now called internally from our custom update loop. No need to call Update manually anymore.")] public static void Update() => NetworkLateUpdate(); // shutdown //////////////////////////////////////////////////////////// /// Destroys all networked objects on the client. // Note: NetworkServer.CleanupNetworkIdentities does the same on server. public static void DestroyAllClientObjects() { // user can modify spawned lists which causes InvalidOperationException // list can modified either in UnSpawnHandler or in OnDisable/OnDestroy // we need the Try/Catch so that the rest of the shutdown does not get stopped try { foreach (NetworkIdentity identity in NetworkIdentity.spawned.Values) { if (identity != null && identity.gameObject != null) { identity.OnStopClient(); bool wasUnspawned = InvokeUnSpawnHandler(identity.assetId, identity.gameObject); if (!wasUnspawned) { // scene objects are reset and disabled. // they always stay in the scene, we don't destroy them. if (identity.sceneId != 0) { identity.Reset(); identity.gameObject.SetActive(false); } // spawned objects are destroyed else { GameObject.Destroy(identity.gameObject); } } } } NetworkIdentity.spawned.Clear(); } catch (InvalidOperationException e) { Debug.LogException(e); Debug.LogError("Could not DestroyAllClientObjects because spawned list was modified during loop, make sure you are not modifying NetworkIdentity.spawned by calling NetworkServer.Destroy or NetworkServer.Spawn in OnDestroy or OnDisable."); } } /// Shutdown the client. public static void Shutdown() { //Debug.Log("Shutting down client."); ClearSpawners(); spawnableObjects.Clear(); ready = false; isSpawnFinished = false; DestroyAllClientObjects(); connectState = ConnectState.None; handlers.Clear(); // disconnect the client connection. // we do NOT call Transport.Shutdown, because someone only called // NetworkClient.Shutdown. we can't assume that the server is // supposed to be shut down too! if (Transport.activeTransport != null) Transport.activeTransport.ClientDisconnect(); connection = null; // clear events. someone might have hooked into them before, but // we don't want to use those hooks after Shutdown anymore. OnConnectedEvent = null; OnDisconnectedEvent = null; } } }