using FishNet.Managing;
using FishNet.Connection;
using UnityEngine;
using FishNet.Serializing;
using FishNet.Transporting;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using FishNet.Utility.Performance;
using System;
using FishNet.Managing.Object;
using FishNet.Component.Ownership;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace FishNet.Object
{
[DisallowMultipleComponent]
public sealed partial class NetworkObject : MonoBehaviour
{
#region Public.
///
/// True if was nested during scene setup or within a prefab.
///
[field: SerializeField, HideInInspector]
public bool IsNested { get; private set; }
///
/// NetworkConnection which predicted spawned this object.
///
public NetworkConnection PredictedSpawner { get; private set; } = NetworkManager.EmptyConnection;
///
/// True if this NetworkObject was active during edit. Will be true if placed in scene during edit, and was in active state on run.
///
[System.NonSerialized]
internal bool ActiveDuringEdit;
///
/// Returns if this object was placed in the scene during edit-time.
///
///
public bool IsSceneObject => (SceneId > 0);
///
/// ComponentIndex for this NetworkBehaviour.
///
[field: SerializeField, HideInInspector]
public byte ComponentIndex { get; private set; }
///
/// Unique Id for this NetworkObject. This does not represent the object owner.
///
public int ObjectId { get; private set; }
///
/// True if this NetworkObject is deinitializing. Will also be true until Initialize is called. May be false until the object is cleaned up if object is destroyed without using Despawn.
///
internal bool IsDeinitializing { get; private set; } = true;
///
/// PredictedSpawn component on this object. Will be null if not added manually.
///
[field: SerializeField, HideInInspector]
public PredictedSpawn PredictedSpawn { get; private set; }
///
///
///
[field: SerializeField, HideInInspector]
private NetworkBehaviour[] _networkBehaviours;
///
/// NetworkBehaviours within the root and children of this object.
///
public NetworkBehaviour[] NetworkBehaviours
{
get => _networkBehaviours;
private set => _networkBehaviours = value;
}
///
/// NetworkObject parenting this instance. The parent NetworkObject will be null if there was no parent during serialization.
///
[field: SerializeField, HideInInspector]
public NetworkObject ParentNetworkObject { get; private set; }
///
/// NetworkObjects nested beneath this one. Recursive NetworkObjects may exist within each entry of this field.
///
[field: SerializeField, HideInInspector]
public List ChildNetworkObjects { get; private set; } = new List();
///
///
///
[SerializeField, HideInInspector]
internal TransformProperties SerializedTransformProperties = new TransformProperties();
///
/// Current state of the NetworkObject.
///
[System.NonSerialized]
internal NetworkObjectState State = NetworkObjectState.Unset;
#endregion
#region Serialized.
///
/// True if the object will always initialize as a networked object. When false the object will not automatically initialize over the network. Using Spawn() on an object will always set that instance as networked.
///
public bool IsNetworked
{
get => _isNetworked;
private set => _isNetworked = value;
}
///
/// Sets IsNetworked value. This method must be called before Start.
///
/// New IsNetworked value.
public void SetIsNetworked(bool value)
{
IsNetworked = value;
}
[Tooltip("True if the object will always initialize as a networked object. When false the object will not automatically initialize over the network. Using Spawn() on an object will always set that instance as networked.")]
[SerializeField]
private bool _isNetworked = true;
///
/// True to make this object global, and added to the DontDestroyOnLoad scene. This value may only be set for instantiated objects, and can be changed if done immediately after instantiating.
///
public bool IsGlobal
{
get => _isGlobal;
private set => _isGlobal = value;
}
///
/// Sets IsGlobal value.
///
/// New global value.
public void SetIsGlobal(bool value)
{
if (IsNested)
{
NetworkManager.StaticLogWarning($"Object {gameObject.name} cannot change IsGlobal because it is nested. Only root objects may be set global.");
return;
}
if (!IsDeinitializing)
{
NetworkManager.StaticLogWarning($"Object {gameObject.name} cannot change IsGlobal as it's already initialized. IsGlobal may only be changed immediately after instantiating.");
return;
}
if (IsSceneObject)
{
NetworkManager.StaticLogWarning($"Object {gameObject.name} cannot have be global because it is a scene object. Only instantiated objects may be global.");
return;
}
_networkObserverInitiliazed = false;
IsGlobal = value;
}
[Tooltip("True to make this object global, and added to the DontDestroyOnLoad scene. This value may only be set for instantiated objects, and can be changed if done immediately after instantiating.")]
[SerializeField]
private bool _isGlobal;
///
/// Order to initialize this object's callbacks when spawned with other NetworkObjects in the same tick. Default value is 0, negative values will execute callbacks first.
///
public sbyte GetInitializeOrder() => _initializeOrder;
[Tooltip("Order to initialize this object's callbacks when spawned with other NetworkObjects in the same tick. Default value is 0, negative values will execute callbacks first.")]
[SerializeField]
private sbyte _initializeOrder = 0;
///
/// How to handle this object when it despawns. Scene objects are never destroyed when despawning.
///
[SerializeField]
[Tooltip("How to handle this object when it despawns. Scene objects are never destroyed when despawning.")]
private DespawnType _defaultDespawnType = DespawnType.Destroy;
///
/// True to use configured ObjectPool rather than destroy this NetworkObject when being despawned. Scene objects are never destroyed.
///
public DespawnType GetDefaultDespawnType() => _defaultDespawnType;
///
/// Sets DespawnType value.
///
/// Default despawn type for this NetworkObject.
public void SetDefaultDespawnType(DespawnType despawnType)
{
_defaultDespawnType = despawnType;
}
#endregion
#region Private.
///
/// True if disabled NetworkBehaviours have been initialized.
///
private bool _disabledNetworkBehavioursInitialized;
#endregion
#region Const.
///
/// Value used when the ObjectId has not been set.
///
public const int UNSET_OBJECTID_VALUE = ushort.MaxValue;
///
/// Value used when the PrefabId has not been set.
///
public const int UNSET_PREFABID_VALUE = ushort.MaxValue;
#endregion
#region Editor Debug.
#if UNITY_EDITOR
private int _editorOwnerId;
#endif
#endregion
private void Awake()
{
SetChildDespawnedState();
}
private void Start()
{
TryStartDeactivation();
}
///
/// Initializes NetworkBehaviours if they are disabled.
///
private void InitializeNetworkBehavioursIfDisabled()
{
if (_disabledNetworkBehavioursInitialized)
return;
_disabledNetworkBehavioursInitialized = true;
for (int i = 0; i < NetworkBehaviours.Length; i++)
NetworkBehaviours[i].InitializeIfDisabled();
}
///
/// Sets Despawned on child NetworkObjects if they are not enabled.
///
private void SetChildDespawnedState()
{
NetworkObject nob;
for (int i = 0; i < ChildNetworkObjects.Count; i++)
{
nob = ChildNetworkObjects[i];
if (!nob.gameObject.activeSelf)
nob.State = NetworkObjectState.Despawned;
}
}
///
/// Deactivates this NetworkObject during it's start cycle if conditions are met.
///
internal void TryStartDeactivation()
{
if (!IsNetworked)
return;
//Global.
if (IsGlobal && !IsSceneObject)
DontDestroyOnLoad(gameObject);
if (NetworkManager == null || (!NetworkManager.IsClient && !NetworkManager.IsServer))
{
//ActiveDuringEdit is only used for scene objects.
if (IsSceneObject)
ActiveDuringEdit = true;
gameObject.SetActive(false);
}
}
private void OnDisable()
{
/* If deinitializing and an owner exist
* then remove object from owner. */
if (IsDeinitializing && Owner.IsValid)
Owner.RemoveObject(this);
/* If not nested then check to despawn this OnDisable.
* A nob may become disabled without being despawned if it's
* beneath another deinitializing nob. This can be true even while
* not nested because users may move a nob under another at runtime.
*
* This object must also be activeSelf, meaning that it became disabled
* because a parent was. If not activeSelf then it's possible the
* user simply deactivated the object themselves. */
else if (IsServer && !IsNested && gameObject.activeSelf)
{
bool canDespawn = false;
Transform nextParent = transform.parent;
while (nextParent != null)
{
if (nextParent.TryGetComponent(out NetworkObject pNob))
{
/* If pNob is not the same as ParentNetworkObject
* then that means this object was moved around. It could be
* that this was previously a child of something else
* or that was given a parent later on in it's life cycle.
^
* When this occurs do not send a despawn for this object.
* Rather, let it destroy from unity callbacks which will force
* the proper destroy/stop cycle. */
if (pNob != ParentNetworkObject)
break;
//If nob is deinitialized then this one cannot exist.
if (pNob.IsDeinitializing)
{
canDespawn = true;
break;
}
}
nextParent = nextParent.parent;
}
if (canDespawn)
Despawn();
}
}
private void OnDestroy()
{
//Does this need to be here? I'm thinking no, remove it and examine later. //todo
if (Owner.IsValid)
Owner.RemoveObject(this);
//Already being deinitialized by FishNet.
if (IsDeinitializing)
return;
if (NetworkManager != null)
{
//Was destroyed without going through the proper methods.
if (NetworkManager.IsServer)
NetworkManager.ServerManager.Objects.NetworkObjectUnexpectedlyDestroyed(this, true);
if (NetworkManager.IsClient)
NetworkManager.ClientManager.Objects.NetworkObjectUnexpectedlyDestroyed(this, false);
}
/* When destroyed unexpectedly it's
* impossible to know if this occurred on
* the server or client side, so send callbacks
* for both. */
if (IsServer)
InvokeStopCallbacks(true);
if (IsClient)
InvokeStopCallbacks(false);
/* If owner exist then remove object from owner.
* This has to be called here as well OnDisable because
* the OnDisable will only remove the object if
* deinitializing. This is because the object shouldn't
* be removed from owner if the object is simply being
* disabled, but not deinitialized. But in the scenario
* the object is unexpectedly destroyed, which is how we
* arrive here, the object needs to be removed from owner. */
if (Owner.IsValid)
Owner.RemoveObject(this);
Observers.Clear();
IsDeinitializing = true;
SetActiveStatus(false);
//Do not need to set state if being destroyed.
//Don't need to reset sync types if object is being destroyed.
}
///
/// Sets IsClient or IsServer to isActive.
///
private void SetActiveStatus(bool isActive, bool server)
{
if (server)
IsServer = isActive;
else
IsClient = isActive;
}
///
/// Sets IsClient and IsServer to isActive.
///
private void SetActiveStatus(bool isActive)
{
IsServer = isActive;
IsClient = isActive;
}
///
/// Initializes this script. This is only called once even when as host.
///
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Preinitialize_Internal(NetworkManager networkManager, int objectId, NetworkConnection owner, bool asServer)
{
State = NetworkObjectState.Spawned;
InitializeNetworkBehavioursIfDisabled();
IsDeinitializing = false;
//QOL references.
NetworkManager = networkManager;
ServerManager = networkManager.ServerManager;
ClientManager = networkManager.ClientManager;
ObserverManager = networkManager.ObserverManager;
TransportManager = networkManager.TransportManager;
TimeManager = networkManager.TimeManager;
SceneManager = networkManager.SceneManager;
PredictionManager = networkManager.PredictionManager;
RollbackManager = networkManager.RollbackManager;
SetOwner(owner);
ObjectId = objectId;
/* This must be called at the beginning
* so that all conditions are handled by the observer
* manager prior to the preinitialize call on networkobserver.
* The method called is dependent on NetworkManager being set. */
AddDefaultNetworkObserverConditions();
for (int i = 0; i < NetworkBehaviours.Length; i++)
NetworkBehaviours[i].InitializeOnce_Internal();
/* NetworkObserver uses some information from
* NetworkBehaviour so it must be preinitialized
* after NetworkBehaviours are. */
if (asServer)
NetworkObserver.PreInitialize(this);
_networkObserverInitiliazed = true;
//Add to connection objects if owner exist.
if (owner != null)
owner.AddObject(this);
}
///
/// Adds a NetworkBehaviour and serializes it's components.
///
internal T AddAndSerialize() where T : NetworkBehaviour //runtimeNB make public.
{
int startingLength = NetworkBehaviours.Length;
T result = gameObject.AddComponent();
//Add to network behaviours.
Array.Resize(ref _networkBehaviours, startingLength + 1);
_networkBehaviours[startingLength] = result;
//Serialize values and return.
result.SerializeComponents(this, (byte)startingLength);
return result;
}
///
/// Updates NetworkBehaviours and initializes them with serialized values.
///
/// True if this call originated from a prefab collection, such as during it's initialization.
internal void UpdateNetworkBehaviours(NetworkObject parentNob, ref byte componentIndex) //runtimeNB make public.
{
/* This method can be called by the developer initializing prefabs, the prefab collection doing it automatically,
* or when the networkobject is modified or added to an object.
*
* Prefab collections generally contain all prefabs, meaning they will not only call this on the topmost
* networkobject but also each child, as the child would be it's own prefab in the collection. This assumes
* that is, the child is a nested prefab.
*
* Because of this potential a check must be done where if the componentIndex is 0 we must look
* for a networkobject above this one. If there is a networkObject above this one then we know the prefab
* is being initialized individually, not part of a recursive check. In this case exit early
* as the parent would have already resolved the needed information. */
//If first componentIndex make sure there's no more than maximum allowed nested nobs.
if (componentIndex == 0)
{
//Not possible for index to be 0 and nested.
if (IsNested)
return;
byte maxNobs = 255;
if (GetComponentsInChildren(true).Length > maxNobs)
{
Debug.LogError($"The number of child NetworkObjects on {gameObject.name} exceeds the maximum of {maxNobs}.");
return;
}
}
PredictedSpawn = GetComponent();
ComponentIndex = componentIndex;
ParentNetworkObject = parentNob;
//Transforms which can be searched for networkbehaviours.
ListCache transformCache = ListCaches.GetTransformCache();
transformCache.Reset();
ChildNetworkObjects.Clear();
transformCache.AddValue(transform);
for (int z = 0; z < transformCache.Written; z++)
{
Transform currentT = transformCache.Collection[z];
for (int i = 0; i < currentT.childCount; i++)
{
Transform t = currentT.GetChild(i);
/* If contains a nob then do not add to transformsCache.
* Do add to ChildNetworkObjects so it can be initialized when
* parent is. */
if (t.TryGetComponent(out NetworkObject childNob))
{
/* Make sure both objects have the same value for
* IsSceneObject. It's possible the user instantiated
* an object and placed it beneath a scene object
* before the scene initialized. They may also
* add a scene object under an instantiated, even though
* this almost certainly will break things. */
if (IsSceneObject == childNob.IsSceneObject)
ChildNetworkObjects.Add(childNob);
}
else
{
transformCache.AddValue(t);
}
}
}
int written;
//Iterate all cached transforms and get networkbehaviours.
ListCache nbCache = ListCaches.GetNetworkBehaviourCache();
nbCache.Reset();
written = transformCache.Written;
List ts = transformCache.Collection;
//
for (int i = 0; i < written; i++)
nbCache.AddValues(ts[i].GetNetworkBehaviours());
//Copy to array.
written = nbCache.Written;
List nbs = nbCache.Collection;
NetworkBehaviours = new NetworkBehaviour[written];
//
for (int i = 0; i < written; i++)
{
NetworkBehaviours[i] = nbs[i];
NetworkBehaviours[i].SerializeComponents(this, (byte)i);
}
ListCaches.StoreCache(transformCache);
ListCaches.StoreCache(nbCache);
//Tell children nobs to update their NetworkBehaviours.
foreach (NetworkObject item in ChildNetworkObjects)
{
componentIndex++;
item.UpdateNetworkBehaviours(this, ref componentIndex);
}
}
///
/// Called after all data is synchronized with this NetworkObject.
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void Initialize(bool asServer, bool invokeSyncTypeCallbacks)
{
InitializeCallbacks(asServer, invokeSyncTypeCallbacks);
}
///
/// Called to prepare this object to be destroyed or disabled.
///
internal void Deinitialize(bool asServer)
{
InvokeStopCallbacks(asServer);
if (asServer)
{
IsDeinitializing = true;
}
else
{
ClientManager.Connection.LevelOfDetails.Remove(this);
//Client only.
if (!NetworkManager.IsServer)
IsDeinitializing = true;
RemoveClientRpcLinkIndexes();
}
SetActiveStatus(false, asServer);
if (asServer)
Observers.Clear();
}
///
/// Resets states for object to be pooled.
///
/// True if performing as server.
public void ResetForObjectPool()
{
int count = NetworkBehaviours.Length;
for (int i = 0; i < count; i++)
NetworkBehaviours[i].ResetForObjectPool();
State = NetworkObjectState.Unset;
SetOwner(NetworkManager.EmptyConnection);
NetworkObserver.Deinitialize();
//QOL references.
NetworkManager = null;
ServerManager = null;
ClientManager = null;
ObserverManager = null;
TransportManager = null;
TimeManager = null;
SceneManager = null;
RollbackManager = null;
//Misc sets.
ObjectId = 0;
ClientInitialized = false;
}
///
/// Removes ownership from all clients.
///
public void RemoveOwnership()
{
GiveOwnership(null, true);
}
///
/// Gives ownership to newOwner.
///
///
public void GiveOwnership(NetworkConnection newOwner)
{
GiveOwnership(newOwner, true);
}
///
/// Gives ownership to newOwner.
///
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void GiveOwnership(NetworkConnection newOwner, bool asServer)
{
/* Additional asServer checks. */
if (asServer)
{
if (!NetworkManager.IsServer)
{
NetworkManager.LogWarning($"Ownership cannot be given for object {gameObject.name}. Only server may give ownership.");
return;
}
//If the same owner don't bother sending a message, just ignore request.
if (newOwner == Owner && asServer)
return;
if (newOwner != null && newOwner.IsActive && !newOwner.LoadedStartScenes(true))
{
NetworkManager.LogWarning($"Ownership has been transfered to ConnectionId {newOwner.ClientId} but this is not recommended until after they have loaded start scenes. You can be notified when a connection loads start scenes by using connection.OnLoadedStartScenes on the connection, or SceneManager.OnClientLoadStartScenes.");
}
}
bool activeNewOwner = (newOwner != null && newOwner.IsActive);
//Set prevOwner, disallowing null.
NetworkConnection prevOwner = Owner;
if (prevOwner == null)
prevOwner = NetworkManager.EmptyConnection;
SetOwner(newOwner);
/* Only modify objects if asServer or not
* host. When host, server would
* have already modified objects
* collection so there is no need
* for client to as well. */
if (asServer || !NetworkManager.IsHost)
{
if (activeNewOwner)
newOwner.AddObject(this);
if (prevOwner.IsValid && prevOwner != newOwner)
prevOwner.RemoveObject(this);
}
//After changing owners invoke callbacks.
InvokeOwnership(prevOwner, asServer);
//If asServer send updates to clients as needed.
if (asServer)
{
if (activeNewOwner)
ServerManager.Objects.RebuildObservers(this, newOwner);
using (PooledWriter writer = WriterPool.GetWriter())
{
writer.WritePacketId(PacketId.OwnershipChange);
writer.WriteNetworkObject(this);
writer.WriteNetworkConnection(Owner);
//If sharing then send to all observers.
if (NetworkManager.ServerManager.ShareIds)
{
NetworkManager.TransportManager.SendToClients((byte)Channel.Reliable, writer.GetArraySegment(), this);
}
//Only sending to old / new.
else
{
if (prevOwner.IsActive)
NetworkManager.TransportManager.SendToClient((byte)Channel.Reliable, writer.GetArraySegment(), prevOwner);
if (activeNewOwner)
NetworkManager.TransportManager.SendToClient((byte)Channel.Reliable, writer.GetArraySegment(), newOwner);
}
}
if (prevOwner.IsActive)
ServerManager.Objects.RebuildObservers(prevOwner);
}
}
///
/// Initializes a predicted object for client.
///
internal void InitializePredictedObject_Server(NetworkManager manager, NetworkConnection predictedSpawner)
{
NetworkManager = manager;
PredictedSpawner = predictedSpawner;
}
///
/// Initializes a predicted object for client.
///
internal void PreinitializePredictedObject_Client(NetworkManager manager, int objectId, NetworkConnection owner, NetworkConnection predictedSpawner)
{
PredictedSpawner = predictedSpawner;
Preinitialize_Internal(manager, objectId, owner, false);
}
///
/// Deinitializes this predicted spawned object.
///
internal void DeinitializePredictedObject_Client()
{
/* For the time being we're just going to disable the object because
* deinitializing instead could present a lot of problems.
* For example: if client deinitializes rpc links are unregistered,
* and if server had a rpc on the way already the link would
* not be found. This would cause the reader length to be wrong
* resulting in packet corruption. */
gameObject.SetActive(false);
}
///
/// Sets the owner of this object.
///
///
///
private void SetOwner(NetworkConnection owner)
{
Owner = owner;
}
///
/// Returns if this NetworkObject is a scene object, and has changed.
///
///
internal ChangedTransformProperties GetTransformChanges(TransformProperties stp)
{
ChangedTransformProperties ctp = ChangedTransformProperties.Unset;
if (transform.localPosition != stp.Position)
ctp |= ChangedTransformProperties.LocalPosition;
if (transform.localRotation != stp.Rotation)
ctp |= ChangedTransformProperties.LocalRotation;
if (transform.localScale != stp.LocalScale)
ctp |= ChangedTransformProperties.LocalScale;
return ctp;
}
///
/// Returns if this NetworkObject is a scene object, and has changed.
///
///
internal ChangedTransformProperties GetTransformChanges(GameObject prefab)
{
Transform t = prefab.transform;
ChangedTransformProperties ctp = ChangedTransformProperties.Unset;
if (transform.position != t.position)
ctp |= ChangedTransformProperties.LocalPosition;
if (transform.rotation != t.rotation)
ctp |= ChangedTransformProperties.LocalRotation;
if (transform.localScale != t.localScale)
ctp |= ChangedTransformProperties.LocalScale;
return ctp;
}
#region Editor.
#if UNITY_EDITOR
///
/// Removes duplicate NetworkObject components on this object returning the removed count.
///
///
internal int RemoveDuplicateNetworkObjects()
{
NetworkObject[] nobs = GetComponents();
for (int i = 1; i < nobs.Length; i++)
DestroyImmediate(nobs[i]);
return (nobs.Length - 1);
}
///
/// Sets IsNested and returns the result.
///
///
private bool SetIsNestedThroughTraversal()
{
Transform parent = transform.parent;
//Iterate long as parent isn't null, and isnt self.
while (parent != null && parent != transform)
{
if (parent.TryGetComponent(out _))
{
IsNested = true;
return IsNested;
}
parent = parent.parent;
}
//No NetworkObject found in parents, meaning this is not nested.
IsNested = false;
return IsNested;
}
private void OnValidate()
{
SetIsNestedThroughTraversal();
SceneUpdateNetworkBehaviours();
ReferenceIds_OnValidate();
if (IsGlobal && IsSceneObject)
Debug.LogWarning($"Object {gameObject.name} will have it's IsGlobal state ignored because it is a scene object. Instantiated copies will still be global. This warning is informative only.");
}
private void Reset()
{
SetIsNestedThroughTraversal();
SerializeTransformProperties();
SceneUpdateNetworkBehaviours();
ReferenceIds_Reset();
}
private void SceneUpdateNetworkBehaviours()
{
//In a scene.
if (!string.IsNullOrEmpty(gameObject.scene.name))
{
if (IsNested)
return;
byte componentIndex = 0;
UpdateNetworkBehaviours(null, ref componentIndex);
}
}
private void OnDrawGizmosSelected()
{
_editorOwnerId = (Owner == null) ? -1 : Owner.ClientId;
SerializeTransformProperties();
}
///
/// Serializes TransformProperties to current transform properties.
///
private void SerializeTransformProperties()
{
/* Use this method to set scene data since it doesn't need to exist outside
* the editor and because its updated regularly while selected. */
//If a scene object.
if (!EditorApplication.isPlaying && !string.IsNullOrEmpty(gameObject.scene.name))
{
SerializedTransformProperties = new TransformProperties(
transform.localPosition, transform.localRotation, transform.localScale);
}
}
#endif
#endregion
}
}