#if !FISHNET_STABLE_SYNCTYPES
using FishNet.Documenting;
using FishNet.Object.Synchronizing.Internal;
using FishNet.Serializing;
using System.Collections.Generic;
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(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();
//Has previous value if should invoke finished.
float? finishedPrevious = null;
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)
{
SetUpdateTime();
Paused = false;
Remaining = next;
Duration = duration;
}
if (newChangeId)
{
InvokeOnChange(op, -1f, next, asServer);
/* If next is 0 then that means the timer
* expired on the same tick it was started.
* This can be true depending on when in code
* the server starts the timer.
*
* When 0 also invoke finished. */
if (next == 0)
finishedPrevious = duration;
}
}
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;
if (!Paused)
SetUpdateTime();
if (newChangeId)
InvokeOnChange(op, prev, next, asServer);
}
if (newChangeId && changes > 0)
InvokeOnChange(SyncTimerOperation.Complete, -1f, -1f, false);
if (finishedPrevious.HasValue)
InvokeFinished(finishedPrevious.Value);
}
///
/// 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;
InvokeFinished(prev);
}
///
/// Invokes SyncTimer finished a previous value.
///
///
private void InvokeFinished(float prev)
{
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