using System; using FishNet.CodeGenerating; using FishNet.Managing; using FishNet.Managing.Timing; using FishNet.Serializing; using FishNet.Transporting; using System.Runtime.CompilerServices; using UnityEngine; namespace FishNet.Object.Synchronizing.Internal { public class SyncBase { #region Public. /// /// True if this SyncBase has been initialized on its NetworkBehaviour. /// Being true does not mean that the NetworkBehaviour has been initialized on the network, but rather that this SyncBase has been configured with the basics to be networked. /// public bool IsInitialized { get; private set; } /// /// True if the object for which this SyncType is for has been initialized for the network. /// public bool IsNetworkInitialized => (IsInitialized && (NetworkBehaviour.IsServerStarted || NetworkBehaviour.IsClientStarted)); /// /// True if a SyncObject, false if a SyncVar. /// public bool IsSyncObject { get; private set; } /// /// The settings for this SyncVar. /// [MakePublic] internal SyncTypeSettings Settings; /// /// How often updates may send. /// [MakePublic] internal float SendRate => Settings.SendRate; /// /// True if this SyncVar needs to send data. /// public bool IsDirty { get; private set; } /// /// NetworkManager this uses. /// public NetworkManager NetworkManager = null; /// /// NetworkBehaviour this SyncVar belongs to. /// public NetworkBehaviour NetworkBehaviour = null; /// /// True if the server side has initialized this SyncType. /// public bool OnStartServerCalled { get; private set; } /// /// True if the client side has initialized this SyncType. /// public bool OnStartClientCalled { get; private set; } /// /// Next time this SyncType may send data. /// This is also the next time a client may send to the server when using client-authoritative SyncTypes. /// [MakePublic] internal uint NextSyncTick = 0; /// /// Index within the sync collection. /// public uint SyncIndex { get; protected set; } = 0; /// /// Channel to send on. /// internal Channel Channel => _currentChannel; /// /// Sets a new currentChannel. /// /// internal void SetCurrentChannel(Channel channel) => _currentChannel = channel; #endregion #region Private. /// /// Sync interval converted to ticks. /// private uint _timeToTicks; /// /// Channel to use for next write. To ensure eventual consistency this eventually changes to reliable when Settings are unreliable. /// private Channel _currentChannel; /// /// Last changerId read from sender. /// private ushort _lastReadChangeId = UNSET_CHANGE_ID; /// /// Last changeId that was sent to receivers. /// private ushort _lastWrittenChangeId = UNSET_CHANGE_ID; #endregion #region Consts. /// /// Value to use when readId is unset. /// private const ushort UNSET_CHANGE_ID = 0; /// /// Maximum value readId can be before resetting to the beginning. /// private const ushort MAXIMUM_CHANGE_ID = ushort.MaxValue; #endregion #region Constructors public SyncBase() : this(new()) { } public SyncBase(SyncTypeSettings settings) { Settings = settings; } #endregion /// /// Updates settings with new values. /// public void UpdateSettings(SyncTypeSettings settings) { Settings = settings; SetTimeToTicks(); } /// /// Updates settings with new values. /// public void UpdatePermissions(WritePermission writePermissions, ReadPermission readPermissions) { UpdatePermissions(writePermissions); UpdatePermissions(readPermissions); } /// /// Updates settings with new values. /// public void UpdatePermissions(WritePermission writePermissions) => Settings.WritePermission = writePermissions; /// /// Updates settings with new values. /// public void UpdatePermissions(ReadPermission readPermissions) => Settings.ReadPermission = readPermissions; /// /// Updates settings with new values. /// public void UpdateSendRate(float sendRate) { Settings.SendRate = sendRate; SetTimeToTicks(); } /// /// Updates settings with new values. /// public void UpdateSettings(Channel channel) { CheckChannel(ref channel); _currentChannel = channel; } /// /// Updates settings with new values. /// public void UpdateSettings(WritePermission writePermissions, ReadPermission readPermissions, float sendRate, Channel channel) { CheckChannel(ref channel); _currentChannel = channel; Settings = new(writePermissions, readPermissions, sendRate, channel); SetTimeToTicks(); } /// /// Checks channel and corrects if not valid. /// /// private void CheckChannel(ref Channel c) { if (c == Channel.Unreliable && IsSyncObject) { c = Channel.Reliable; string warning = $"Channel cannot be unreliable for SyncObjects. Channel has been changed to reliable."; NetworkManager.LogWarning(warning); } } /// /// Initializes this SyncBase before user Awake code. /// [MakePublic] internal void InitializeEarly(NetworkBehaviour nb, uint syncIndex, bool isSyncObject) { NetworkBehaviour = nb; SyncIndex = syncIndex; IsSyncObject = isSyncObject; NetworkBehaviour.RegisterSyncType(this, SyncIndex); } /// /// Called during InitializeLate in NetworkBehaviours to indicate user Awake code has executed. /// [MakePublic] internal void InitializeLate() { Initialized(); } /// /// Called when the SyncType has been registered, but not yet initialized over the network. /// protected virtual void Initialized() { IsInitialized = true; } /// /// PreInitializes this for use with the network. /// [MakePublic] protected internal void PreInitialize(NetworkManager networkManager, bool asServer) { NetworkManager = networkManager; if (Settings.IsDefault()) { float sendRate = Mathf.Max(networkManager.ServerManager.GetSyncTypeRate(), (float)networkManager.TimeManager.TickDelta); Settings = new(sendRate); } SetTimeToTicks(); } /// /// Sets ticks needed to pass for send rate. /// private void SetTimeToTicks() { if (NetworkManager == null) return; _timeToTicks = NetworkManager.TimeManager.TimeToTicks(Settings.SendRate, TickRounding.RoundUp); } /// /// Called after OnStartXXXX has occurred for the NetworkBehaviour. /// /// True if OnStartServer was called, false if OnStartClient. [MakePublic] protected internal virtual void OnStartCallback(bool asServer) { if (asServer) OnStartServerCalled = true; else OnStartClientCalled = true; } /// /// Called before OnStopXXXX has occurred for the NetworkBehaviour. /// /// True if OnStopServer was called, false if OnStopClient. [MakePublic] protected internal virtual void OnStopCallback(bool asServer) { if (asServer) OnStartServerCalled = false; else OnStartClientCalled = false; } /// /// True if can set values and send them over the network. /// /// /// protected bool CanNetworkSetValues(bool log = true) { /* If not registered then values can be set * since at this point the object is still being initialized * in awake so we want those values to be applied. */ if (!IsInitialized) return true; /* If the network is not initialized yet then let * values be set. Values set here will not synchronize * to the network. We are assuming the user is setting * these values on client and server appropriately * since they are being applied prior to this object * being networked. */ if (!IsNetworkInitialized) return true; //If server is active then values can be set no matter what. if (NetworkBehaviour.IsServerStarted) return true; /* If here then server is not active and additional * checks must be performed. */ bool result = (Settings.WritePermission == WritePermission.ClientUnsynchronized) || (Settings.ReadPermission == ReadPermission.ExcludeOwner && NetworkBehaviour.IsOwner); if (!result && log) LogServerNotActiveWarning(); return result; } /// /// Logs that the operation could not be completed because the server is not active. /// protected void LogServerNotActiveWarning() { if (NetworkManager != null) NetworkManager.LogWarning($"Cannot complete operation as server when server is not active. You can disable this warning by setting WritePermissions to {WritePermission.ClientUnsynchronized.ToString()}."); } /// /// Dirties this Sync and the NetworkBehaviour. /// /// True to send current dirtied values immediately as a RPC. When this occurs values will arrive in the order they are sent and interval is ignored. protected bool Dirty() //bool sendRpc = false) { //if (sendRpc) // NextSyncTick = 0; /* Reset channel even if already dirty. * This is because the value might have changed * which will reset the eventual consistency state. */ _currentChannel = Settings.Channel; /* Once dirty don't undirty until it's * processed. This ensures that data * is flushed. */ bool canDirty = NetworkBehaviour.DirtySyncType(); IsDirty |= canDirty; return canDirty; } /// /// Returns if callbacks can be invoked with asServer ture. /// This is typically used when the value is changing through user code, causing supplier to be unknown. /// /// protected bool CanInvokeCallbackAsServer() => (!IsNetworkInitialized || NetworkBehaviour.IsServerStarted); /// /// Reads a change Id and returns true if the change is new. /// /// This method is currently under evaluation and may change at any time. protected virtual bool ReadChangeId(Reader reader) { if (NetworkManager == null) { NetworkManager.LogWarning($"NetworkManager is unexpectedly null during a SyncType read."); return false; } bool rolledOver = reader.ReadBoolean(); ushort id = reader.ReadUInt16(); //Only check lastReadId if its not unset. if (_lastReadChangeId != UNSET_CHANGE_ID) { /* If not rolledOver then Id should always be larger * than the last read. If it's not then the data is * old. * * If Id is smaller then rolledOver should be normal, * as rolling over means to restart the Id from the lowest * value. */ if (rolledOver) { if (id >= _lastReadChangeId) return false; } else { if (id <= _lastReadChangeId) return false; } } _lastReadChangeId = id; return true; } /// /// Writes the readId for a change. /// /// This method is currently under evaluation and may change at any time. protected virtual void WriteChangeId(PooledWriter writer) { bool rollOver; if (_lastWrittenChangeId >= MAXIMUM_CHANGE_ID) { rollOver = true; _lastWrittenChangeId = UNSET_CHANGE_ID; } else { rollOver = false; } _lastWrittenChangeId++; writer.WriteBoolean(rollOver); writer.WriteUInt16(_lastWrittenChangeId); } #if !FISHNET_STABLE_SYNCTYPES /// /// Returns true if values are being read as clientHost. /// /// True if reading as server. /// This method is currently under evaluation and may change at any time. protected bool IsReadAsClientHost(bool asServer) => (!asServer && NetworkManager.IsServerStarted); /// /// Returns true if values are being read as clientHost. /// /// True if reading as server. /// This method is currently under evaluation and may change at any time. protected bool CanReset(bool asServer) { bool clientStarted = (IsNetworkInitialized && NetworkManager.IsClientStarted); return (asServer && !clientStarted) || (!asServer && NetworkBehaviour.IsDeinitializing); } #else /// /// Returns true if values are being read as clientHost. /// /// True if reading as server. /// This method is currently under evaluation and may change at any time. protected bool IsReadAsClientHost(bool asServer) => (!asServer && (NetworkManager != null && NetworkManager.IsServerStarted)); #endif /// /// Outputs values which may be helpful on how to process a read operation. /// /// True if the changeId read is not old data. /// True if being read as clientHost. /// True if can modify values from the read, typically when asServer or not asServer and not clientHost. /// This method is currently under evaluation and may change at any time. protected void SetReadArguments(PooledReader reader, bool asServer, out bool newChangeId, out bool asClientHost, out bool canModifyValues) { newChangeId = ReadChangeId(reader); asClientHost = IsReadAsClientHost(asServer); canModifyValues = (newChangeId && !asClientHost); } /// /// Sets IsDirty to false. /// internal void ResetDirty() { //If not a sync object and using unreliable channel. if (!IsSyncObject && Settings.Channel == Channel.Unreliable) { //Check if dirty can be unset or if another tick must be run using reliable. if (_currentChannel == Channel.Unreliable) _currentChannel = Channel.Reliable; //Already sent reliable, can undirty. Channel will reset next time this dirties. else IsDirty = false; } //If syncObject or using reliable unset dirty. else { IsDirty = false; } } /// /// True if dirty and enough time has passed to write changes. /// internal bool IsNextSyncTimeMet(uint tick) => (IsDirty && tick >= NextSyncTick); [Obsolete("Use IsNextSyncTimeMet.")] //Remove on V5 internal bool SyncTimeMet(uint tick) => IsNextSyncTimeMet(tick); /// /// Writes current value. /// /// True to set the next time data may sync. [MakePublic] protected internal virtual void WriteDelta(PooledWriter writer, bool resetSyncTick = true) { WriteHeader(writer, resetSyncTick); } /// /// Writes the header for this SyncType. /// protected virtual void WriteHeader(PooledWriter writer, bool resetSyncTick = true) { if (resetSyncTick) NextSyncTick = NetworkManager.TimeManager.LocalTick + _timeToTicks; writer.WriteUInt8Unpacked((byte)SyncIndex); WriteChangeId(writer); } /// /// Indicates that a full write has occurred. /// This is called from WriteFull, or can be called manually. /// [Obsolete("This method no longer functions. You may remove it from your code.")] //Remove on V5. protected void FullWritten() { } /// /// Writes all values for the SyncType. /// [MakePublic] protected internal virtual void WriteFull(PooledWriter writer) { } /// /// Sets current value as server or client through deserialization. /// [MakePublic] protected internal virtual void Read(PooledReader reader, bool asServer) { } /// /// Resets initialized values for server and client. /// protected internal virtual void ResetState() { ResetState(true); ResetState(false); } /// /// Resets initialized values for server or client. /// [MakePublic] protected internal virtual void ResetState(bool asServer) { if (asServer) { NextSyncTick = 0; SetCurrentChannel(Settings.Channel); IsDirty = false; } /* This only needs to be reset for clients, since * it only applies to clients. But if the server is resetting * that means the object is deinitializing, and won't have any * client observers anyway. Because of this it's safe to reset * with asServer true, or false. * * This change is made to resolve a bug where asServer:false * sometimes does not invoke when stopping clientHost while not * also stopping play mode. */ _lastReadChangeId = UNSET_CHANGE_ID; } } }