#if FISHNET_STABLE_SYNCTYPES using FishNet.CodeGenerating; using FishNet.Documenting; using FishNet.Object.Synchronizing.Internal; using FishNet.Serializing; using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using UnityEngine; namespace FishNet.Object.Synchronizing { /// /// A SyncObject to efficiently synchronize timers over the network. /// public class SyncTimer : SyncBase, ICustomSync { #region Type. /// /// Information about how the timer has changed. /// private struct ChangeData { public readonly SyncTimerOperation Operation; public readonly float Previous; public readonly float Next; public ChangeData(SyncTimerOperation operation, float previous, float next) { Operation = operation; Previous = previous; Next = next; } } #endregion #region Public. /// /// Delegate signature for when the timer operation occurs. /// /// Operation which was performed. /// Previous value of the timer. This will be -1f is the value is not available. /// Value of the timer. This will be -1f is the value is not available. /// True if occurring on server. public delegate void SyncTypeChanged(SyncTimerOperation op, float prev, float next, bool asServer); /// /// Called when a timer operation occurs. /// public event SyncTypeChanged OnChange; /// /// Time remaining on the timer. When the timer is expired this value will be 0f. /// public float Remaining { get; private set; } /// /// How much time has passed since the timer started. /// public float Elapsed => (Duration - Remaining); /// /// Starting duration of the timer. /// public float Duration { get; private set; } /// /// True if the SyncTimer is currently paused. Calls to Update(float) will be ignored when paused. /// public bool Paused { get; private set; } #endregion #region Private. /// /// Changed data which will be sent next tick. /// private List _changed = new(); /// /// Server OnChange events waiting for start callbacks. /// private List _serverOnChanges = new(); /// /// Client OnChange events waiting for start callbacks. /// private List _clientOnChanges = new(); /// /// Last Time.unscaledTime the timer delta was updated. /// private float _updateTime; #endregion #region Constructors public SyncTimer(SyncTypeSettings settings = new()) : base(settings) { } #endregion /// /// Called when the SyncType has been registered, but not yet initialized over the network. /// protected override void Initialized() { base.Initialized(); //Initialize collections if needed. OdinInspector can cause them to become deinitialized. #if ODIN_INSPECTOR if (_changed == null) _changed = new(); if (_serverOnChanges == null) _serverOnChanges = new(); if (_clientOnChanges == null) _clientOnChanges = new(); #endif } /// /// Starts a timer. If called when a timer is already active then StopTimer will automatically be sent. /// /// Time in which the timer should start with. /// True to include remaining time when automatically sending StopTimer. public void StartTimer(float remaining, bool sendRemainingOnStop = true) { if (!base.CanNetworkSetValues(true)) return; if (Remaining > 0f) StopTimer(sendRemainingOnStop); Paused = false; Remaining = remaining; Duration = remaining; SetUpdateTime(); AddOperation(SyncTimerOperation.Start, -1f, remaining); } /// /// Pauses the timer. Calling while already paused will be result in no action. /// /// True to send Remaining with this operation. public void PauseTimer(bool sendRemaining = false) { if (Remaining <= 0f) return; if (Paused) return; if (!base.CanNetworkSetValues(true)) return; Paused = true; SyncTimerOperation op = (sendRemaining) ? SyncTimerOperation.PauseUpdated : SyncTimerOperation.Pause; AddOperation(op, Remaining, Remaining); } /// /// Unpauses the timer. Calling while already unpaused will be result in no action. /// public void UnpauseTimer() { if (Remaining <= 0f) return; if (!Paused) return; if (!base.CanNetworkSetValues(true)) return; Paused = false; SetUpdateTime(); AddOperation(SyncTimerOperation.Unpause, Remaining, Remaining); } /// /// Stops and resets the timer. /// public void StopTimer(bool sendRemaining = false) { if (Remaining <= 0f) return; if (!base.CanNetworkSetValues(log: true)) return; bool asServer = true; float prev = Remaining; StopTimer_Internal(asServer); SyncTimerOperation op = (sendRemaining) ? SyncTimerOperation.StopUpdated : SyncTimerOperation.Stop; AddOperation(op, prev, 0f); } /// /// Adds an operation to synchronize. /// private void AddOperation(SyncTimerOperation operation, float prev, float next) { if (!base.IsInitialized) return; bool asServerInvoke = (!base.IsNetworkInitialized || base.NetworkBehaviour.IsServerStarted); if (asServerInvoke) { if (base.Dirty()) { ChangeData change = new(operation, prev, next); _changed.Add(change); } } OnChange?.Invoke(operation, prev, next, asServerInvoke); } /// /// Writes all changed values. /// ///True to set the next time data may sync. protected internal override void WriteDelta(PooledWriter writer, bool resetSyncTick = true) { base.WriteDelta(writer, resetSyncTick); writer.WriteInt32(_changed.Count); for (int i = 0; i < _changed.Count; i++) { ChangeData change = _changed[i]; writer.WriteUInt8Unpacked((byte)change.Operation); if (change.Operation == SyncTimerOperation.Start) { WriteStartTimer(writer, false); } //Pause and unpause updated need current value written. //Updated stop also writes current value. else if (change.Operation == SyncTimerOperation.PauseUpdated || change.Operation == SyncTimerOperation.StopUpdated) { writer.WriteSingle(change.Next); } } _changed.Clear(); } /// /// Writes all values. /// protected internal override void WriteFull(PooledWriter writer) { //Only write full if a timer is running. if (Remaining <= 0f) return; base.WriteDelta(writer, false); //There will be 1 or 2 entries. If paused 2, if not 1. int entries = (Paused) ? 2 : 1; writer.WriteInt32(entries); //And the operations. WriteStartTimer(writer, true); if (Paused) writer.WriteUInt8Unpacked((byte)SyncTimerOperation.Pause); } /// /// Writes a StartTimer operation. /// /// /// private void WriteStartTimer(Writer w, bool includeOperationByte) { if (includeOperationByte) w.WriteUInt8Unpacked((byte)SyncTimerOperation.Start); w.WriteSingle(Remaining); w.WriteSingle(Duration); } /// /// Reads and sets the current values for server or client. /// [APIExclude] protected internal override void Read(PooledReader reader, bool asServer) { base.SetReadArguments(reader, asServer, out bool newChangeId, out bool asClientHost, out bool canModifyValues); int changes = reader.ReadInt32(); for (int i = 0; i < changes; i++) { SyncTimerOperation op = (SyncTimerOperation)reader.ReadUInt8Unpacked(); if (op == SyncTimerOperation.Start) { float next = reader.ReadSingle(); float duration = reader.ReadSingle(); if (canModifyValues) { Paused = false; Remaining = next; Duration = duration; } if (newChangeId) InvokeOnChange(op, -1f, next, asServer); } else if (op == SyncTimerOperation.Pause || op == SyncTimerOperation.PauseUpdated || op == SyncTimerOperation.Unpause) { if (canModifyValues) UpdatePauseState(op); } else if (op == SyncTimerOperation.Stop) { float prev = Remaining; if (canModifyValues) StopTimer_Internal(asServer); if (newChangeId) InvokeOnChange(op, prev, 0f, false); } // else if (op == SyncTimerOperation.StopUpdated) { float prev = Remaining; float next = reader.ReadSingle(); if (canModifyValues) StopTimer_Internal(asServer); if (newChangeId) InvokeOnChange(op, prev, next, asServer); } } //Updates a pause state with a pause or unpause operation. void UpdatePauseState(SyncTimerOperation op) { bool newPauseState = (op == SyncTimerOperation.Pause || op == SyncTimerOperation.PauseUpdated); float prev = Remaining; float next; //If updated time as well. if (op == SyncTimerOperation.PauseUpdated) { next = reader.ReadSingle(); Remaining = next; } else { next = Remaining; } Paused = newPauseState; InvokeOnChange(op, prev, next, asServer); } if (newChangeId && changes > 0) InvokeOnChange(SyncTimerOperation.Complete, -1f, -1f, false); } /// /// Stops the timer and resets. /// private void StopTimer_Internal(bool asServer) { Paused = false; Remaining = 0f; } /// /// Invokes OnChanged callback. /// private void InvokeOnChange(SyncTimerOperation operation, float prev, float next, bool asServer) { if (asServer) { if (base.NetworkBehaviour.OnStartServerCalled) OnChange?.Invoke(operation, prev, next, asServer); else _serverOnChanges.Add(new(operation, prev, next)); } else { if (base.NetworkBehaviour.OnStartClientCalled) OnChange?.Invoke(operation, prev, next, asServer); else _clientOnChanges.Add(new(operation, prev, next)); } } /// /// Called after OnStartXXXX has occurred. /// /// True if OnStartServer was called, false if OnStartClient. protected internal override void OnStartCallback(bool asServer) { base.OnStartCallback(asServer); List collection = (asServer) ? _serverOnChanges : _clientOnChanges; if (OnChange != null) { foreach (ChangeData item in collection) OnChange.Invoke(item.Operation, item.Previous, item.Next, asServer); } collection.Clear(); } /// /// Sets updateTime to current values. /// private void SetUpdateTime() { _updateTime = Time.unscaledTime; } /// /// Removes time passed from Remaining since the last unscaled time using this method. /// public void Update() { float delta = (Time.unscaledTime - _updateTime); Update(delta); } /// /// Removes delta from Remaining for server and client. /// This also resets unscaledTime delta for Update(). /// /// Value to remove from Remaining. public void Update(float delta) { //Not enabled. if (Remaining <= 0f) return; if (Paused) return; SetUpdateTime(); if (delta < 0) delta *= -1f; float prev = Remaining; Remaining -= delta; //Still time left. if (Remaining > 0f) return; /* If here then the timer has * ended. Invoking the events is tricky * here because both the server and the client * would share the same value. Because of this check * if each socket is started and if so invoke for that * side. There's a chance down the road this may need to be improved * for some but at this time I'm unable to think of any * problems. */ Remaining = 0f; if (base.NetworkManager.IsServerStarted) OnChange?.Invoke(SyncTimerOperation.Finished, prev, 0f, true); if (base.NetworkManager.IsClientStarted) OnChange?.Invoke(SyncTimerOperation.Finished, prev, 0f, false); } /// /// Return the serialized type. /// /// public object GetSerializedType() => null; } } #endif