using FishNet.Connection; using FishNet.Documenting; using FishNet.Managing.Server; using FishNet.Object; using FishNet.Transporting; using GameKit.Dependencies.Utilities; using System.Collections.Generic; using FishNet.Managing; using UnityEngine; namespace FishNet.Observing { /// /// Controls which clients can see and get messages for an object. /// [DisallowMultipleComponent] [RequireComponent(typeof(NetworkObject))] [AddComponentMenu("FishNet/Component/NetworkObserver")] public sealed class NetworkObserver : MonoBehaviour { #region Types. /// /// How ObserverManager conditions are used. /// public enum ConditionOverrideType { /// /// Keep current conditions, add new conditions from manager. /// AddMissing = 1, /// /// Replace current conditions with manager conditions. /// UseManager = 2, /// /// Keep current conditions, ignore manager conditions. /// IgnoreManager = 3, } #endregion #region Internal. /// /// True if the ObserverManager had already added conditions for this component. /// internal bool ConditionsSetByObserverManager; #endregion #region Serialized. /// /// /// [Tooltip("How ObserverManager conditions are used.")] [SerializeField] private ConditionOverrideType _overrideType = ConditionOverrideType.IgnoreManager; /// /// How ObserverManager conditions are used. /// public ConditionOverrideType OverrideType { get => _overrideType; internal set => _overrideType = value; } /// /// /// [Tooltip("True to update visibility for clientHost based on if they are an observer or not.")] [SerializeField] private bool _updateHostVisibility = true; /// /// True to update visibility for clientHost based on if they are an observer or not. /// public bool UpdateHostVisibility { get => _updateHostVisibility; private set => _updateHostVisibility = value; } /// /// /// [Tooltip("Conditions connections must met to be added as an observer. Multiple conditions may be used.")] [SerializeField] internal List _observerConditions = new(); /// /// Conditions connections must met to be added as an observer. Multiple conditions may be used. /// public IReadOnlyList ObserverConditions => _observerConditions; [APIExclude] #if MIRROR public List ObserverConditionsInternal #else internal List ObserverConditionsInternal #endif { get => _observerConditions; set => _observerConditions = value; } #endregion #region Private. /// /// Conditions under this component which are timed. /// private List _timedConditions; /// /// Connections which have all non-timed conditions met. /// private HashSet _nonTimedMet; /// /// NetworkObject this belongs to. /// private NetworkObject _networkObject; /// /// Becomes true when registered with ServerObjects as Timed observers. /// private bool _registeredAsTimed; /// /// True if was initialized previously. /// private bool _conditionsInitializedPreviously; /// /// True if currently initialized. /// private bool _initialized; /// /// True if ParentNetworkObject was visible last iteration. /// This value will also be true if there is no ParentNetworkObject. /// private bool _lastParentVisible; /// /// ServerManager for this script. /// private ServerManager _serverManager; /// /// Becomes true if there are non-timed, normal conditions. /// private bool _hasNormalConditions; #endregion /// /// Deinitializes for reuse or clean up. /// /// internal void Deinitialize(bool destroyed) { _lastParentVisible = false; if (_nonTimedMet != null) _nonTimedMet.Clear(); UnregisterTimedConditions(); if (_serverManager != null) _serverManager.OnRemoteConnectionState -= ServerManager_OnRemoteConnectionState; if (_conditionsInitializedPreviously) { _hasNormalConditions = false; foreach (ObserverCondition item in _observerConditions) { item.Deinitialize(destroyed); /* Use GetInstanceId to ensure the object is actually * instantiated. If Id is negative, then it's instantiated * and not a reference to the original object. */ if (destroyed && item.GetInstanceID() < 0) Destroy(item); } //Clean up lists. if (destroyed) { _observerConditions.Clear(); CollectionCaches.Store(_timedConditions); CollectionCaches.Store(_nonTimedMet); } } _serverManager = null; _networkObject = null; _initialized = false; } /// /// Initializes this script for use. /// internal void Initialize(NetworkObject networkObject) { if (_initialized) return; _networkObject = networkObject; _serverManager = _networkObject.ServerManager; _serverManager.OnRemoteConnectionState += ServerManager_OnRemoteConnectionState; bool observerFound = _conditionsInitializedPreviously; if (!_conditionsInitializedPreviously) { _conditionsInitializedPreviously = true; bool ignoringManager = (OverrideType == ConditionOverrideType.IgnoreManager); //Check to override SetHostVisibility. if (!ignoringManager) UpdateHostVisibility = networkObject.ObserverManager.UpdateHostVisibility; /* Sort the conditions so that normal conditions are first. * This prevents normal conditions from being skipped if a timed * condition fails before the normal passed. * * Example: Let's say an object has a distance and scene condition, with * the distance condition being first. Normal conditions are only checked * as the change occurs, such as when the scene was loaded. So if the client * loaded into the scene and they were not within the distance the condition * iterations would skip remaining, which would be the scene condition. As * result normal conditions (non timed) would never be met since they are only * checked as-needed, in this case during a scene change. * * By moving normal conditions to the front they will always be checked first * and timed can update at intervals per expectancy. This could also be resolved * by simply not exiting early when a condition fails but that's going to * cost hotpath performance where sorting is only done once. */ //Initialize collections. _nonTimedMet = CollectionCaches.RetrieveHashSet(); //Caches for ordering. List nonTimedConditions = CollectionCaches.RetrieveList(); List timedConditions = CollectionCaches.RetrieveList(); foreach (ObserverCondition condition in _observerConditions) { if (condition == null) continue; observerFound = true; /* Make an instance of each condition so values are * not overwritten when the condition exist more than * once in the scene. Double-edged sword of using scriptable * objects for conditions. */ ObserverCondition ocCopy = Instantiate(condition); //Condition type. ObserverConditionType oct = ocCopy.GetConditionType(); if (oct == ObserverConditionType.Timed) { timedConditions.AddOrdered(ocCopy); } else { _hasNormalConditions = true; nonTimedConditions.AddOrdered(ocCopy); } } //Add to condition collection as ordered now. _observerConditions.Clear(); //Non timed. for (int i = 0; i < nonTimedConditions.Count; i++) _observerConditions.Add(nonTimedConditions[i]); //Timed. _timedConditions = CollectionCaches.RetrieveList(); foreach (ObserverCondition timedCondition in timedConditions) { _observerConditions.Add(timedCondition); _timedConditions.Add(timedCondition); } //Store caches. CollectionCaches.Store(nonTimedConditions); CollectionCaches.Store(timedConditions); } if (observerFound) { //Initialize conditions. for (int i = 0; i < _observerConditions.Count; i++) _observerConditions[i].Initialize(_networkObject); RegisterTimedConditions(); } _initialized = true; } /// /// Returns a condition if found within Conditions. /// /// public ObserverCondition GetObserverCondition() where T : ObserverCondition { /* Do not bother setting local variables, * condition collections aren't going to be long * enough to make doing so worth while. */ System.Type conditionType = typeof(T); for (int i = 0; i < _observerConditions.Count; i++) { if (_observerConditions[i].GetType() == conditionType) return _observerConditions[i]; } //Fall through, not found. return null; } /// /// Returns ObserverStateChange by comparing conditions for a connection. /// /// True if added to Observers. internal ObserverStateChange RebuildObservers(NetworkConnection connection, bool timedOnly) { if (!_initialized) { string goName = (gameObject == null) ? "Empty" : gameObject.name; NetworkManagerExtensions.LogError($"{GetType().Name} is not initialized on NetworkObject [{goName}]. RebuildObservers should not be called. If you are able to reproduce this error consistently please report this issue."); return ObserverStateChange.Unchanged; } bool currentlyAdded = (_networkObject.Observers.Contains(connection)); //True if all conditions are met. bool allConditionsMet = true; /* If cnnection is owner then they can see the object. */ bool notOwner = (connection != _networkObject.Owner); /* Only check conditions if not owner. Owner will always * have visibility. */ if (notOwner) { bool parentVisible = true; if (_networkObject.CurrentParentNetworkBehaviour != null) parentVisible = _networkObject.CurrentParentNetworkBehaviour.NetworkObject.Observers.Contains(connection); /* If parent is visible but was not previously * then unset timedOnly to make sure all conditions * are checked again. This ensures that the _nonTimedMet * collection is updated. */ if (parentVisible && !_lastParentVisible) timedOnly = false; _lastParentVisible = parentVisible; //If parent is not visible no further checks are required. if (!parentVisible) { allConditionsMet = false; } //Parent is visible, perform checks. else { //Only need to check beyond this if conditions exist. if (_observerConditions.Count > 0) { /* True if all conditions are timed or * if connection has met non timed. */ bool startNonTimedMet = (!_hasNormalConditions || _nonTimedMet.Contains(connection)); /* If a timed update an1d nonTimed * have not been met then there's * no reason to check timed. */ if (timedOnly && !startNonTimedMet) { allConditionsMet = false; } else { //Becomes true if a non-timed condition fails. bool nonTimedMet = true; List collection = (timedOnly) ? _timedConditions : _observerConditions; for (int i = 0; i < collection.Count; i++) { ObserverCondition condition = collection[i]; /* If any observer returns removed then break * from loop and return removed. If one observer has * removed then there's no reason to iterate * the rest. * * A condition is automatically met if it's not enabled. */ bool notProcessed = false; bool conditionMet = (!condition.GetIsEnabled() || condition.ConditionMet(connection, currentlyAdded, out notProcessed)); if (notProcessed) conditionMet = currentlyAdded; //Condition not met. if (!conditionMet) { allConditionsMet = false; if (condition.GetConditionType() != ObserverConditionType.Timed) nonTimedMet = false; break; } } //If nonTimedMet changed. if (startNonTimedMet != nonTimedMet) { /* If the collection was iterated without breaks * then add to nontimed met. */ if (nonTimedMet) _nonTimedMet.Add(connection); //If there were breaks not all conditions were checked. else _nonTimedMet.Remove(connection); } } } } } //If all conditions met. if (allConditionsMet) return ReturnPassedConditions(currentlyAdded); else return ReturnFailedCondition(currentlyAdded); } /// /// Registers timed observer conditions. /// private void RegisterTimedConditions() { if (_timedConditions == null || _timedConditions.Count == 0) return; if (_registeredAsTimed) return; _registeredAsTimed = true; if (_serverManager == null) return; _serverManager.Objects.AddTimedNetworkObserver(_networkObject); } /// /// Unregisters timed conditions. /// private void UnregisterTimedConditions() { if (_timedConditions == null || _timedConditions.Count == 0) return; if (!_registeredAsTimed) return; _registeredAsTimed = false; if (_serverManager == null) return; _serverManager.Objects.RemoveTimedNetworkObserver(_networkObject); } /// /// Returns an ObserverStateChange when a condition fails. /// /// /// private ObserverStateChange ReturnFailedCondition(bool currentlyAdded) { if (currentlyAdded) return ObserverStateChange.Removed; else return ObserverStateChange.Unchanged; } /// /// Returns an ObserverStateChange when all conditions pass. /// /// /// private ObserverStateChange ReturnPassedConditions(bool currentlyAdded) { if (currentlyAdded) return ObserverStateChange.Unchanged; else return ObserverStateChange.Added; } /// /// Called when a remote client state changes with the server. /// private void ServerManager_OnRemoteConnectionState(NetworkConnection conn, RemoteConnectionStateArgs arg2) { if (arg2.ConnectionState == RemoteConnectionState.Stopped) _nonTimedMet.Remove(conn); } /// /// Sets a new value for UpdateHostVisibility. /// This does not immediately update renderers. /// You may need to combine with NetworkObject.SetRenderersVisible(bool). /// /// New value. public void SetUpdateHostVisibility(bool value) { //Unchanged. if (value == UpdateHostVisibility) return; UpdateHostVisibility = value; } } }