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;
}
}
}