Unity-WebSocket/Assets/FishNet/Runtime/Object/Synchronizing/SyncBase.cs
2025-06-28 11:28:54 +03:30

552 lines
20 KiB
C#

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.
/// <summary>
/// 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.
/// </summary>
public bool IsInitialized { get; private set; }
/// <summary>
/// True if the object for which this SyncType is for has been initialized for the network.
/// </summary>
public bool IsNetworkInitialized => (IsInitialized && (NetworkBehaviour.IsServerStarted || NetworkBehaviour.IsClientStarted));
/// <summary>
/// True if a SyncObject, false if a SyncVar.
/// </summary>
public bool IsSyncObject { get; private set; }
/// <summary>
/// The settings for this SyncVar.
/// </summary>
[MakePublic]
internal SyncTypeSettings Settings;
/// <summary>
/// How often updates may send.
/// </summary>
[MakePublic]
internal float SendRate => Settings.SendRate;
/// <summary>
/// True if this SyncVar needs to send data.
/// </summary>
public bool IsDirty { get; private set; }
/// <summary>
/// NetworkManager this uses.
/// </summary>
public NetworkManager NetworkManager = null;
/// <summary>
/// NetworkBehaviour this SyncVar belongs to.
/// </summary>
public NetworkBehaviour NetworkBehaviour = null;
/// <summary>
/// True if the server side has initialized this SyncType.
/// </summary>
public bool OnStartServerCalled { get; private set; }
/// <summary>
/// True if the client side has initialized this SyncType.
/// </summary>
public bool OnStartClientCalled { get; private set; }
/// <summary>
/// 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.
/// </summary>
[MakePublic]
internal uint NextSyncTick = 0;
/// <summary>
/// Index within the sync collection.
/// </summary>
public uint SyncIndex { get; protected set; } = 0;
/// <summary>
/// Channel to send on.
/// </summary>
internal Channel Channel => _currentChannel;
/// <summary>
/// Sets a new currentChannel.
/// </summary>
/// <param name="channel"></param>
internal void SetCurrentChannel(Channel channel) => _currentChannel = channel;
#endregion
#region Private.
/// <summary>
/// Sync interval converted to ticks.
/// </summary>
private uint _timeToTicks;
/// <summary>
/// Channel to use for next write. To ensure eventual consistency this eventually changes to reliable when Settings are unreliable.
/// </summary>
private Channel _currentChannel;
/// <summary>
/// Last changerId read from sender.
/// </summary>
private ushort _lastReadChangeId = UNSET_CHANGE_ID;
/// <summary>
/// Last changeId that was sent to receivers.
/// </summary>
private ushort _lastWrittenChangeId = UNSET_CHANGE_ID;
#endregion
#region Consts.
/// <summary>
/// Value to use when readId is unset.
/// </summary>
private const ushort UNSET_CHANGE_ID = 0;
/// <summary>
/// Maximum value readId can be before resetting to the beginning.
/// </summary>
private const ushort MAXIMUM_CHANGE_ID = ushort.MaxValue;
#endregion
#region Constructors
public SyncBase() : this(new()) { }
public SyncBase(SyncTypeSettings settings)
{
Settings = settings;
}
#endregion
/// <summary>
/// Updates settings with new values.
/// </summary>
public void UpdateSettings(SyncTypeSettings settings)
{
Settings = settings;
SetTimeToTicks();
}
/// <summary>
/// Updates settings with new values.
/// </summary>
public void UpdatePermissions(WritePermission writePermissions, ReadPermission readPermissions)
{
UpdatePermissions(writePermissions);
UpdatePermissions(readPermissions);
}
/// <summary>
/// Updates settings with new values.
/// </summary>
public void UpdatePermissions(WritePermission writePermissions) => Settings.WritePermission = writePermissions;
/// <summary>
/// Updates settings with new values.
/// </summary>
public void UpdatePermissions(ReadPermission readPermissions) => Settings.ReadPermission = readPermissions;
/// <summary>
/// Updates settings with new values.
/// </summary>
public void UpdateSendRate(float sendRate)
{
Settings.SendRate = sendRate;
SetTimeToTicks();
}
/// <summary>
/// Updates settings with new values.
/// </summary>
public void UpdateSettings(Channel channel)
{
CheckChannel(ref channel);
_currentChannel = channel;
}
/// <summary>
/// Updates settings with new values.
/// </summary>
public void UpdateSettings(WritePermission writePermissions, ReadPermission readPermissions, float sendRate, Channel channel)
{
CheckChannel(ref channel);
_currentChannel = channel;
Settings = new(writePermissions, readPermissions, sendRate, channel);
SetTimeToTicks();
}
/// <summary>
/// Checks channel and corrects if not valid.
/// </summary>
/// <param name="c"></param>
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);
}
}
/// <summary>
/// Initializes this SyncBase before user Awake code.
/// </summary>
[MakePublic]
internal void InitializeEarly(NetworkBehaviour nb, uint syncIndex, bool isSyncObject)
{
NetworkBehaviour = nb;
SyncIndex = syncIndex;
IsSyncObject = isSyncObject;
NetworkBehaviour.RegisterSyncType(this, SyncIndex);
}
/// <summary>
/// Called during InitializeLate in NetworkBehaviours to indicate user Awake code has executed.
/// </summary>
[MakePublic]
internal void InitializeLate()
{
Initialized();
}
/// <summary>
/// Called when the SyncType has been registered, but not yet initialized over the network.
/// </summary>
protected virtual void Initialized()
{
IsInitialized = true;
}
/// <summary>
/// PreInitializes this for use with the network.
/// </summary>
[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();
}
/// <summary>
/// Sets ticks needed to pass for send rate.
/// </summary>
private void SetTimeToTicks()
{
if (NetworkManager == null)
return;
_timeToTicks = NetworkManager.TimeManager.TimeToTicks(Settings.SendRate, TickRounding.RoundUp);
}
/// <summary>
/// Called after OnStartXXXX has occurred for the NetworkBehaviour.
/// </summary>
/// <param name="asServer">True if OnStartServer was called, false if OnStartClient.</param>
[MakePublic]
protected internal virtual void OnStartCallback(bool asServer)
{
if (asServer)
OnStartServerCalled = true;
else
OnStartClientCalled = true;
}
/// <summary>
/// Called before OnStopXXXX has occurred for the NetworkBehaviour.
/// </summary>
/// <param name="asServer">True if OnStopServer was called, false if OnStopClient.</param>
[MakePublic]
protected internal virtual void OnStopCallback(bool asServer)
{
if (asServer)
OnStartServerCalled = false;
else
OnStartClientCalled = false;
}
/// <summary>
/// True if can set values and send them over the network.
/// </summary>
/// <param name="log"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Logs that the operation could not be completed because the server is not active.
/// </summary>
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()}.");
}
/// <summary>
/// Dirties this Sync and the NetworkBehaviour.
/// </summary>
/// <param name="sendRpc">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.</param>
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;
}
/// <summary>
/// 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.
/// </summary>
/// <returns></returns>
protected bool CanInvokeCallbackAsServer() => (!IsNetworkInitialized || NetworkBehaviour.IsServerStarted);
/// <summary>
/// Reads a change Id and returns true if the change is new.
/// </summary>
/// <remarks>This method is currently under evaluation and may change at any time.</remarks>
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;
}
/// <summary>
/// Writes the readId for a change.
/// </summary>
/// <remarks>This method is currently under evaluation and may change at any time.</remarks>
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
/// <summary>
/// Returns true if values are being read as clientHost.
/// </summary>
/// <param name="asServer">True if reading as server.</param>
/// <remarks>This method is currently under evaluation and may change at any time.</remarks>
protected bool IsReadAsClientHost(bool asServer) => (!asServer && NetworkManager.IsServerStarted);
/// <summary>
/// Returns true if values are being read as clientHost.
/// </summary>
/// <param name="asServer">True if reading as server.</param>
/// <remarks>This method is currently under evaluation and may change at any time.</remarks>
protected bool CanReset(bool asServer)
{
bool clientStarted = (IsNetworkInitialized && NetworkManager.IsClientStarted);
return (asServer && !clientStarted) || (!asServer && NetworkBehaviour.IsDeinitializing);
}
#else
/// <summary>
/// Returns true if values are being read as clientHost.
/// </summary>
/// <param name="asServer">True if reading as server.</param>
/// <remarks>This method is currently under evaluation and may change at any time.</remarks>
protected bool IsReadAsClientHost(bool asServer) => (!asServer && (NetworkManager != null && NetworkManager.IsServerStarted));
#endif
/// <summary>
/// Outputs values which may be helpful on how to process a read operation.
/// </summary>
/// <param name="newChangeId">True if the changeId read is not old data.</param>
/// <param name="asClientHost">True if being read as clientHost.</param>
/// <param name="canModifyValues">True if can modify values from the read, typically when asServer or not asServer and not clientHost.</param>
/// <remarks>This method is currently under evaluation and may change at any time.</remarks>
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);
}
/// <summary>
/// Sets IsDirty to false.
/// </summary>
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;
}
}
/// <summary>
/// True if dirty and enough time has passed to write changes.
/// </summary>
internal bool IsNextSyncTimeMet(uint tick) => (IsDirty && tick >= NextSyncTick);
[Obsolete("Use IsNextSyncTimeMet.")] //Remove on V5
internal bool SyncTimeMet(uint tick) => IsNextSyncTimeMet(tick);
/// <summary>
/// Writes current value.
/// </summary>
/// <param name="resetSyncTick">True to set the next time data may sync.</param>
[MakePublic]
protected internal virtual void WriteDelta(PooledWriter writer, bool resetSyncTick = true)
{
WriteHeader(writer, resetSyncTick);
}
/// <summary>
/// Writes the header for this SyncType.
/// </summary>
protected virtual void WriteHeader(PooledWriter writer, bool resetSyncTick = true)
{
if (resetSyncTick)
NextSyncTick = NetworkManager.TimeManager.LocalTick + _timeToTicks;
writer.WriteUInt8Unpacked((byte)SyncIndex);
WriteChangeId(writer);
}
/// <summary>
/// Indicates that a full write has occurred.
/// This is called from WriteFull, or can be called manually.
/// </summary>
[Obsolete("This method no longer functions. You may remove it from your code.")] //Remove on V5.
protected void FullWritten() { }
/// <summary>
/// Writes all values for the SyncType.
/// </summary>
[MakePublic]
protected internal virtual void WriteFull(PooledWriter writer) { }
/// <summary>
/// Sets current value as server or client through deserialization.
/// </summary>
[MakePublic]
protected internal virtual void Read(PooledReader reader, bool asServer) { }
/// <summary>
/// Resets initialized values for server and client.
/// </summary>
protected internal virtual void ResetState()
{
ResetState(true);
ResetState(false);
}
/// <summary>
/// Resets initialized values for server or client.
/// </summary>
[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;
}
}
}