#if FISHNET_STABLE_SYNCTYPES using FishNet.CodeGenerating; using FishNet.Documenting; using FishNet.Object.Synchronizing.Internal; using FishNet.Serializing; using System.Collections.Generic; using System.Runtime.CompilerServices; namespace FishNet.Object.Synchronizing { /// /// A SyncObject to efficiently synchronize Stopwatchs over the network. /// public class SyncStopwatch : SyncBase, ICustomSync { #region Type. /// /// Information about how the Stopwatch has changed. /// private struct ChangeData { public readonly SyncStopwatchOperation Operation; public readonly float Previous; public ChangeData(SyncStopwatchOperation operation, float previous) { Operation = operation; Previous = previous; } } #endregion #region Public. /// /// Delegate signature for when the Stopwatch operation occurs. /// /// Operation which was performed. /// Previous value of the Stopwatch. This will be -1f is the value is not available. /// True if occurring on server. public delegate void SyncTypeChanged(SyncStopwatchOperation op, float prev, bool asServer); /// /// Called when a Stopwatch operation occurs. /// public event SyncTypeChanged OnChange; /// /// How much time has passed since the Stopwatch started. /// public float Elapsed { get; private set; } = -1f; /// /// True if the SyncStopwatch 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(); #endregion #region Constructors public SyncStopwatch(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 Stopwatch. If called when a Stopwatch is already active then StopStopwatch will automatically be sent. /// /// Time in which the Stopwatch should start with. /// True to include remaining time when automatically sending StopStopwatch. public void StartStopwatch(bool sendElapsedOnStop = true) { if (!base.CanNetworkSetValues(true)) return; if (Elapsed > 0f) StopStopwatch(sendElapsedOnStop); Elapsed = 0f; AddOperation(SyncStopwatchOperation.Start, 0f); } /// /// Pauses the Stopwatch. Calling while already paused will be result in no action. /// /// True to send Remaining with this operation. public void PauseStopwatch(bool sendElapsed = false) { if (Elapsed < 0f) return; if (Paused) return; if (!base.CanNetworkSetValues(true)) return; Paused = true; float prev; SyncStopwatchOperation op; if (sendElapsed) { prev = Elapsed; op = SyncStopwatchOperation.PauseUpdated; } else { prev = -1f; op = SyncStopwatchOperation.Pause; } AddOperation(op, prev); } /// /// Unpauses the Stopwatch. Calling while already unpaused will be result in no action. /// public void UnpauseStopwatch() { if (Elapsed < 0f) return; if (!Paused) return; if (!base.CanNetworkSetValues(true)) return; Paused = false; AddOperation(SyncStopwatchOperation.Unpause, -1f); } /// /// Stops and resets the Stopwatch. /// public void StopStopwatch(bool sendElapsed = false) { if (Elapsed < 0f) return; if (!base.CanNetworkSetValues(true)) return; float prev = (sendElapsed) ? -1f : Elapsed; StopStopwatch_Internal(true); SyncStopwatchOperation op = (sendElapsed) ? SyncStopwatchOperation.StopUpdated : SyncStopwatchOperation.Stop; AddOperation(op, prev); } /// /// Adds an operation to synchronize. /// private void AddOperation(SyncStopwatchOperation operation, float prev) { if (!base.IsInitialized) return; bool asServerInvoke = (!base.IsNetworkInitialized || base.NetworkBehaviour.IsServerStarted); if (asServerInvoke) { if (base.Dirty()) { ChangeData change = new(operation, prev); _changed.Add(change); } } OnChange?.Invoke(operation, prev, 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 == SyncStopwatchOperation.Start) WriteStartStopwatch(writer, 0f, false); //Pause and unpause updated need current value written. //Updated stop also writes current value. else if (change.Operation == SyncStopwatchOperation.PauseUpdated || change.Operation == SyncStopwatchOperation.StopUpdated) writer.WriteSingle(change.Previous); } _changed.Clear(); } /// /// Writes all values. /// protected internal override void WriteFull(PooledWriter writer) { //Only write full if a Stopwatch is running. if (Elapsed < 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. WriteStartStopwatch(writer, Elapsed, true); if (Paused) writer.WriteUInt8Unpacked((byte)SyncStopwatchOperation.Pause); } /// /// Writers a start with elapsed time. /// /// private void WriteStartStopwatch(Writer w, float elapsed, bool includeOperationByte) { if (includeOperationByte) w.WriteUInt8Unpacked((byte)SyncStopwatchOperation.Start); w.WriteSingle(elapsed); } /// /// 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++) { SyncStopwatchOperation op = (SyncStopwatchOperation)reader.ReadUInt8Unpacked(); if (op == SyncStopwatchOperation.Start) { float elapsed = reader.ReadSingle(); if (canModifyValues) Elapsed = elapsed; if (newChangeId) InvokeOnChange(op, elapsed, asServer); } else if (op == SyncStopwatchOperation.Pause) { if (canModifyValues) Paused = true; if (newChangeId) InvokeOnChange(op, -1f, asServer); } else if (op == SyncStopwatchOperation.PauseUpdated) { float prev = reader.ReadSingle(); if (newChangeId) Paused = true; if (newChangeId) InvokeOnChange(op, prev, asServer); } else if (op == SyncStopwatchOperation.Unpause) { if (canModifyValues) Paused = false; if (newChangeId) InvokeOnChange(op, -1f, asServer); } else if (op == SyncStopwatchOperation.Stop) { if (canModifyValues) StopStopwatch_Internal(asServer); if (newChangeId) InvokeOnChange(op, -1f, false); } else if (op == SyncStopwatchOperation.StopUpdated) { float prev = reader.ReadSingle(); if (canModifyValues) StopStopwatch_Internal(asServer); if (newChangeId) InvokeOnChange(op, prev, asServer); } } if (newChangeId && changes > 0) InvokeOnChange(SyncStopwatchOperation.Complete, -1f, asServer); } /// /// Stops the Stopwatch and resets. /// private void StopStopwatch_Internal(bool asServer) { Paused = false; Elapsed = -1f; } /// /// Invokes OnChanged callback. /// private void InvokeOnChange(SyncStopwatchOperation operation, float prev, bool asServer) { if (asServer) { if (base.NetworkBehaviour.OnStartServerCalled) OnChange?.Invoke(operation, prev, asServer); else _serverOnChanges.Add(new(operation, prev)); } else { if (base.NetworkBehaviour.OnStartClientCalled) OnChange?.Invoke(operation, prev, asServer); else _clientOnChanges.Add(new(operation, prev)); } } /// /// 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, asServer); } collection.Clear(); } /// /// Adds delta from Remaining for server and client. /// /// Value to remove from Remaining. public void Update(float delta) { //Not enabled. if (Elapsed == -1f) return; if (Paused) return; Elapsed += delta; } /// /// Return the serialized type. /// /// public object GetSerializedType() => null; } } #endif