using FishNet.Documenting; using FishNet.Object.Helping; using FishNet.Object.Synchronizing; using FishNet.Object.Synchronizing.Internal; using FishNet.Serializing; using FishNet.Serializing.Helping; using FishNet.Transporting; using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using UnityEngine; namespace FishNet.Object.Synchronizing { [APIExclude] [StructLayout(LayoutKind.Auto, CharSet = CharSet.Auto)] public class SyncVar : SyncBase { #region Types. /// /// Information needed to invoke a callback. /// private struct CachedOnChange { internal readonly T Previous; internal readonly T Next; public CachedOnChange(T previous, T next) { Previous = previous; Next = next; } } #endregion #region Public. /// /// Called when the SyncDictionary changes. /// public event Action OnChange; #endregion #region Private. /// /// Server OnChange event waiting for start callbacks. /// private CachedOnChange? _serverOnChange; /// /// Client OnChange event waiting for start callbacks. /// private CachedOnChange? _clientOnChange; /// /// Value before the network is initialized on the containing object. /// private T _initialValue; /// /// Previous value on the client. /// private T _previousClientValue; /// /// Current value on the server, or client. /// private T _value; #endregion [MethodImpl(MethodImplOptions.AggressiveInlining)] public SyncVar(NetworkBehaviour nb, uint syncIndex, WritePermission writePermission, ReadPermission readPermission, float sendRate, Channel channel, T value) { SetInitialValues(value); base.InitializeInstance(nb, syncIndex, writePermission, readPermission, sendRate, channel, false); } /// /// Called when the SyncType has been registered, but not yet initialized over the network. /// protected override void Registered() { base.Registered(); _initialValue = _value; } /// /// Sets initial values to next. /// /// private void SetInitialValues(T next) { _initialValue = next; UpdateValues(next); } /// /// Sets current and previous values. /// /// private void UpdateValues(T next) { _previousClientValue = next; _value = next; } /// /// Sets current value and marks the SyncVar dirty when able to. Returns if able to set value. /// /// True if SetValue was called in response to user code. False if from automated code. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void SetValue(T nextValue, bool calledByUser) { /* If not registered then that means Awake * has not completed on the owning class. This would be true * when setting values within awake on the owning class. Registered * is called at the end of awake, so it would be unset until awake completed. * * Registered however will be true when setting from another script, * even if the owning class of this was just spawned. This is because * the unity cycle will fire awake on the object soon as it's spawned, * completing awake, and the user would set the value after. */ if (!base.IsRegistered) return; /* If not client or server then set skipChecks * as true. When neither is true it's likely user is changing * value before object is initialized. This is allowed * but checks cannot be processed because they would otherwise * stop setting the value. */ bool isNetworkInitialized = base.IsNetworkInitialized; //Object is deinitializing. if (isNetworkInitialized && CodegenHelper.NetworkObject_Deinitializing(this.NetworkBehaviour)) return; //If being set by user code. if (calledByUser) { if (!base.CanNetworkSetValues(true)) return; /* We will only be this far if the network is not active yet, * server is active, or client has setting permissions. * We only need to set asServerInvoke to false if the network * is initialized and the server is not active. */ bool asServerInvoke = (!isNetworkInitialized || base.NetworkBehaviour.IsServer); /* If the network has not been network initialized then * Value is expected to be set on server and client since * it's being set before the object is initialized. */ if (!isNetworkInitialized) { T prev = _value; UpdateValues(nextValue); //Still call invoke because change will be cached for when the network initializes. InvokeOnChange(prev, _value, calledByUser); } else { if (Comparers.EqualityCompare(_value, nextValue)) return; T prev = _value; _value = nextValue; InvokeOnChange(prev, _value, asServerInvoke); } TryDirty(asServerInvoke); } //Not called by user. else { /* Previously clients were not allowed to set values * but this has been changed because clients may want * to update values locally while occasionally * letting the syncvar adjust their side. */ T prev = _previousClientValue; if (Comparers.EqualityCompare(prev, nextValue)) return; /* If also server do not update value. * Server side has say of the current value. */ if (!base.NetworkManager.IsServer) UpdateValues(nextValue); else _previousClientValue = nextValue; InvokeOnChange(prev, nextValue, calledByUser); } /* Tries to dirty so update * is sent over network. This needs to be called * anytime the data changes because there is no way * to know if the user set the value on both server * and client or just one side. */ void TryDirty(bool asServer) { //Cannot dirty when network is not initialized. if (!isNetworkInitialized) return; if (asServer) base.Dirty(); } } /// /// Invokes OnChanged callback. /// private void InvokeOnChange(T prev, T next, bool asServer) { if (asServer) { if (base.NetworkBehaviour.OnStartServerCalled) OnChange?.Invoke(prev, next, asServer); else _serverOnChange = new CachedOnChange(prev, next); } else { if (base.NetworkBehaviour.OnStartClientCalled) OnChange?.Invoke(prev, next, asServer); else _clientOnChange = new CachedOnChange(prev, next); } } /// /// Called after OnStartXXXX has occurred. /// /// True if OnStartServer was called, false if OnStartClient. [MethodImpl(MethodImplOptions.AggressiveInlining)] public override void OnStartCallback(bool asServer) { base.OnStartCallback(asServer); if (OnChange != null) { CachedOnChange? change = (asServer) ? _serverOnChange : _clientOnChange; if (change != null) InvokeOnChange(change.Value.Previous, change.Value.Next, asServer); } if (asServer) _serverOnChange = null; else _clientOnChange = null; } /// /// Writes current value. /// /// True to set the next time data may sync. public override void WriteDelta(PooledWriter writer, bool resetSyncTick = true) { base.WriteDelta(writer, resetSyncTick); writer.Write(_value); } /// /// Writes current value if not initialized value. /// m> public override void WriteFull(PooledWriter obj0) { if (Comparers.EqualityCompare(_initialValue, _value)) return; /* SyncVars only hold latest value, so just * write current delta. */ WriteDelta(obj0, false); } //Read isn't used by SyncVar, it's done within the NB. //public override void Read(PooledReader reader) { } /// /// Gets current value. /// /// /// public T GetValue(bool calledByUser) => (calledByUser) ? _value : _previousClientValue; /// /// Resets to initialized values. /// public override void Reset() { base.Reset(); _value = _initialValue; _previousClientValue = _initialValue; } } }