[Add] FishNet

This commit is contained in:
2026-03-30 20:11:57 +07:00
parent ee793a3361
commit c22c08753a
1797 changed files with 197950 additions and 1 deletions
@@ -0,0 +1,16 @@
namespace FishNet.Object.Synchronizing
{
/// <summary>
/// Custom SyncObjects must inherit from SyncBase and implement this interface.
/// </summary>
public interface ICustomSync
{
/// <summary>
/// Get the serialized type.
/// This must return the value type you are synchronizing, for example a struct or class.
/// If you are not synchronizing a particular value but instead of supported values such as int, bool, ect, then you may return null on this method.
/// </summary>
/// <returns></returns>
object GetSerializedType();
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 2024b0be0cd1cc744a442f3e2e6ba483
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/ICustomSync.cs
uploadId: 866910
@@ -0,0 +1,173 @@
using UnityEngine;
namespace FishNet.Object.Synchronizing
{
/// <summary>
/// Implements features specific for a typed SyncVar.
/// </summary>
[System.Serializable]
public class FloatSyncVar : SyncVar<float>, ICustomSync
{
public object GetSerializedType() => typeof(float);
protected override float Interpolate(float previous, float current, float percent) => Mathf.Lerp(previous, current, percent);
}
/// <summary>
/// Implements features specific for a typed SyncVar.
/// </summary>
[System.Serializable]
public class DoubleSyncVar : SyncVar<double>, ICustomSync
{
public object GetSerializedType() => typeof(double);
protected override double Interpolate(double previous, double current, float percent)
{
float a = (float)previous;
float b = (float)current;
return Mathf.Lerp(a, b, percent);
}
}
/// <summary>
/// Implements features specific for a typed SyncVar.
/// </summary>
[System.Serializable]
public class SbyteSyncVar : SyncVar<sbyte>, ICustomSync
{
public object GetSerializedType() => typeof(sbyte);
protected override sbyte Interpolate(sbyte previous, sbyte current, float percent) => (sbyte)Mathf.Lerp(previous, current, percent);
}
/// <summary>
/// Implements features specific for a typed SyncVar.
/// </summary>
[System.Serializable]
public class ByteSyncVar : SyncVar<byte>, ICustomSync
{
public object GetSerializedType() => typeof(byte);
protected override byte Interpolate(byte previous, byte current, float percent) => (byte)Mathf.Lerp(previous, current, percent);
}
/// <summary>
/// Implements features specific for a typed SyncVar.
/// </summary>
[System.Serializable]
public class ShortSyncVar : SyncVar<short>, ICustomSync
{
public object GetSerializedType() => typeof(short);
protected override short Interpolate(short previous, short current, float percent) => (short)Mathf.Lerp(previous, current, percent);
}
/// <summary>
/// Implements features specific for a typed SyncVar.
/// </summary>
[System.Serializable]
public class UShortSyncVar : SyncVar<ushort>, ICustomSync
{
public object GetSerializedType() => typeof(ushort);
protected override ushort Interpolate(ushort previous, ushort current, float percent) => (ushort)Mathf.Lerp(previous, current, percent);
}
/// <summary>
/// Implements features specific for a typed SyncVar.
/// </summary>
[System.Serializable]
public class IntSyncVar : SyncVar<int>, ICustomSync
{
public object GetSerializedType() => typeof(int);
protected override int Interpolate(int previous, int current, float percent) => (int)Mathf.Lerp(previous, current, percent);
}
/// <summary>
/// Implements features specific for a typed SyncVar.
/// </summary>
[System.Serializable]
public class UIntSyncVar : SyncVar<uint>, ICustomSync
{
public object GetSerializedType() => typeof(uint);
protected override uint Interpolate(uint previous, uint current, float percent) => (uint)Mathf.Lerp(previous, current, percent);
}
/// <summary>
/// Implements features specific for a typed SyncVar.
/// </summary>
[System.Serializable]
public class LongSyncVar : SyncVar<long>, ICustomSync
{
public object GetSerializedType() => typeof(long);
protected override long Interpolate(long previous, long current, float percent) => (long)Mathf.Lerp(previous, current, percent);
}
/// <summary>
/// Implements features specific for a typed SyncVar.
/// </summary>
[System.Serializable]
public class ULongSyncVar : SyncVar<ulong>, ICustomSync
{
public object GetSerializedType() => typeof(ulong);
protected override ulong Interpolate(ulong previous, ulong current, float percent) => (ulong)Mathf.Lerp(previous, current, percent);
}
/// <summary>
/// Implements features specific for a typed SyncVar.
/// </summary>
[System.Serializable]
public class Vector2SyncVar : SyncVar<Vector2>, ICustomSync
{
public object GetSerializedType() => typeof(Vector2);
protected override Vector2 Interpolate(Vector2 previous, Vector2 current, float percent) => Vector2.Lerp(previous, current, percent);
}
/// <summary>
/// Implements features specific for a typed SyncVar.
/// </summary>
[System.Serializable]
public class Vector3SyncVar : SyncVar<Vector3>, ICustomSync
{
public object GetSerializedType() => typeof(Vector3);
protected override Vector3 Interpolate(Vector3 previous, Vector3 current, float percent) => Vector3.Lerp(previous, current, percent);
}
/// <summary>
/// Implements features specific for a typed SyncVar.
/// </summary>
[System.Serializable]
public class Vector4SyncVar : SyncVar<Vector4>, ICustomSync
{
public object GetSerializedType() => typeof(Vector4);
protected override Vector4 Interpolate(Vector4 previous, Vector4 current, float percent) => Vector4.Lerp(previous, current, percent);
}
/// <summary>
/// Implements features specific for a typed SyncVar.
/// </summary>
[System.Serializable]
public class Vector2IntSyncVar : SyncVar<Vector2Int>, ICustomSync
{
public object GetSerializedType() => typeof(Vector2);
protected override Vector2Int Interpolate(Vector2Int previous, Vector2Int current, float percent)
{
int x = (int)Mathf.Lerp(previous.x, current.x, percent);
int y = (int)Mathf.Lerp(previous.y, current.y, percent);
return new(x, y);
}
}
/// <summary>
/// Implements features specific for a typed SyncVar.
/// </summary>
[System.Serializable]
public class Vector3IntSyncVar : SyncVar<Vector3Int>, ICustomSync
{
public object GetSerializedType() => typeof(Vector3Int);
protected override Vector3Int Interpolate(Vector3Int previous, Vector3Int current, float percent)
{
int x = (int)Mathf.Lerp(previous.x, current.x, percent);
int y = (int)Mathf.Lerp(previous.y, current.y, percent);
int z = (int)Mathf.Lerp(previous.z, current.z, percent);
return new(x, y, z);
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 727355d27ffb19747a43beb6299f7b98
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/InterpolatedSyncVars.cs
uploadId: 866910
@@ -0,0 +1,8 @@
namespace FishNet.Object
{
internal enum MissingObjectPacketLength : int
{
Reliable = -1,
PurgeRemaiming = -2
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 3d177496f9519e246b8e3ef199d83437
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/MissingObjectPacketLength.cs
uploadId: 866910
@@ -0,0 +1,22 @@
namespace FishNet.Object.Synchronizing
{
/// <summary>
/// Which clients may receive synchronization updates.
/// </summary>
/// // Remove on V5. Just rename file to ReadPermission.cs, do not remove.
public enum ReadPermission : byte
{
/// <summary>
/// All observers will receive updates.
/// </summary>
Observers = 0,
/// <summary>
/// Only owner will receive updates.
/// </summary>
OwnerOnly = 1,
/// <summary>
/// Send to all observers except owner.
/// </summary>
ExcludeOwner = 2
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 8050ef114e01f74409d8e29b821b6fc0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/ReadPermissions.cs
uploadId: 866910
@@ -0,0 +1,543 @@
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.IsServerInitialized || NetworkBehaviour.IsClientInitialized);
/// <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);
}
/// <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);
}
/// <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;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 1a6f26e3f8016cc499b3fa99e7368fbc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/SyncBase.cs
uploadId: 866910
@@ -0,0 +1,680 @@
using FishNet.Documenting;
using FishNet.Managing;
using FishNet.Object.Synchronizing.Internal;
using FishNet.Serializing;
using GameKit.Dependencies.Utilities;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace FishNet.Object.Synchronizing
{
[System.Serializable]
public class SyncDictionary<TKey, TValue> : SyncBase, IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>
{
#region Types.
/// <summary>
/// Information needed to invoke a callback.
/// </summary>
private struct CachedOnChange
{
internal readonly SyncDictionaryOperation Operation;
internal readonly TKey Key;
internal readonly TValue Value;
public CachedOnChange(SyncDictionaryOperation operation, TKey key, TValue value)
{
Operation = operation;
Key = key;
Value = value;
}
}
/// <summary>
/// Information about how the collection has changed.
/// </summary>
private struct ChangeData
{
internal readonly SyncDictionaryOperation Operation;
internal readonly TKey Key;
internal readonly TValue Value;
internal readonly int CollectionCountAfterChange;
public ChangeData(SyncDictionaryOperation operation, TKey key, TValue value, int collectionCountAfterChange)
{
Operation = operation;
Key = key;
Value = value;
CollectionCountAfterChange = collectionCountAfterChange;
}
}
#endregion
#region Public.
/// <summary>
/// Implementation from Dictionary<TKey, TValue>. Not used.
/// </summary>
[APIExclude]
public bool IsReadOnly => false;
/// <summary>
/// Delegate signature for when SyncDictionary changes.
/// </summary>
/// <param name = "op">Operation being completed, such as Add, Set, Remove.</param>
/// <param name = "key">Key being modified.</param>
/// <param name = "value">Value of operation.</param>
/// <param name = "asServer">True if callback is on the server side. False is on the client side.</param>
[APIExclude]
public delegate void SyncDictionaryChanged(SyncDictionaryOperation op, TKey key, TValue value, bool asServer);
/// <summary>
/// Called when the SyncDictionary changes.
/// </summary>
public event SyncDictionaryChanged OnChange;
/// <summary>
/// Collection of objects.
/// </summary>
public Dictionary<TKey, TValue> Collection;
/// <summary>
/// Number of objects in the collection.
/// </summary>
public int Count => Collection.Count;
/// <summary>
/// Keys within the collection.
/// </summary>
public ICollection<TKey> Keys => Collection.Keys;
[APIExclude]
IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => Collection.Keys;
/// <summary>
/// Values within the collection.
/// </summary>
public ICollection<TValue> Values => Collection.Values;
[APIExclude]
IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => Collection.Values;
#endregion
#region Private.
/// <summary>
/// Initial values for the dictionary.
/// </summary>
private Dictionary<TKey, TValue> _initialValues = new();
/// <summary>
/// Changed data which will be sent next tick.
/// </summary>
private List<ChangeData> _changed = new();
/// <summary>
/// Server OnChange events waiting for start callbacks.
/// </summary>
private List<CachedOnChange> _serverOnChanges = new();
/// <summary>
/// Client OnChange events waiting for start callbacks.
/// </summary>
private List<CachedOnChange> _clientOnChanges = new();
/// <summary>
/// True if values have changed since initialization.
/// The only reasonable way to reset this during a Reset call is by duplicating the original list and setting all values to it on reset.
/// </summary>
private bool _valuesChanged;
/// <summary>
/// True to send all values in the next WriteDelta.
/// </summary>
private bool _sendAll;
#endregion
#region Constructors.
public SyncDictionary(SyncTypeSettings settings = new()) : this(CollectionCaches<TKey, TValue>.RetrieveDictionary(), settings) { }
public SyncDictionary(Dictionary<TKey, TValue> collection, SyncTypeSettings settings = new()) : base(settings)
{
Collection = collection == null ? CollectionCaches<TKey, TValue>.RetrieveDictionary() : collection;
_initialValues = CollectionCaches<TKey, TValue>.RetrieveDictionary();
_changed = CollectionCaches<ChangeData>.RetrieveList();
_serverOnChanges = CollectionCaches<CachedOnChange>.RetrieveList();
_clientOnChanges = CollectionCaches<CachedOnChange>.RetrieveList();
}
#endregion
#region Deconstructor.
~SyncDictionary()
{
CollectionCaches<TKey, TValue>.StoreAndDefault(ref Collection);
CollectionCaches<TKey, TValue>.StoreAndDefault(ref _initialValues);
CollectionCaches<ChangeData>.StoreAndDefault(ref _changed);
CollectionCaches<CachedOnChange>.StoreAndDefault(ref _serverOnChanges);
CollectionCaches<CachedOnChange>.StoreAndDefault(ref _clientOnChanges);
}
#endregion
/// <summary>
/// Gets the collection being used within this SyncList.
/// </summary>
/// <param name = "asServer">True if returning the server value, false if client value. The values will only differ when running as host. While asServer is true the most current values on server will be returned, and while false the latest values received by client will be returned.</param>
/// <returns>The used collection.</returns>
public Dictionary<TKey, TValue> GetCollection(bool asServer)
{
return Collection;
}
/// <summary>
/// Called when the SyncType has been registered, but not yet initialized over the network.
/// </summary>
protected override void Initialized()
{
base.Initialized();
// Initialize collections if needed. OdinInspector can cause them to become deinitialized.
#if ODIN_INSPECTOR
if (_initialValues == null)
_initialValues = new();
if (_changed == null)
_changed = new();
if (_serverOnChanges == null)
_serverOnChanges = new();
if (_clientOnChanges == null)
_clientOnChanges = new();
#endif
foreach (KeyValuePair<TKey, TValue> item in Collection)
_initialValues[item.Key] = item.Value;
}
/// <summary>
/// Adds an operation and invokes callback locally.
/// Internal use.
/// May be used for custom SyncObjects.
/// </summary>
/// <param name = "operation"></param>
/// <param name = "key"></param>
/// <param name = "value"></param>
[APIExclude]
private void AddOperation(SyncDictionaryOperation operation, TKey key, TValue value, int collectionCountAfterChange)
{
if (!IsInitialized)
return;
/* asServer might be true if the client is setting the value
* through user code. Typically synctypes can only be set
* by the server, that's why it is assumed asServer via user code.
* However, when excluding owner for the synctype the client should
* have permission to update the value locally for use with
* prediction. */
bool asServerInvoke = !IsNetworkInitialized || NetworkBehaviour.IsServerStarted;
if (asServerInvoke)
{
_valuesChanged = true;
if (base.Dirty())
{
ChangeData change = new(operation, key, value, collectionCountAfterChange);
_changed.Add(change);
}
}
InvokeOnChange(operation, key, value, asServerInvoke);
}
/// <summary>
/// Called after OnStartXXXX has occurred.
/// </summary>
/// <param name = "asServer">True if OnStartServer was called, false if OnStartClient.</param>
protected internal override void OnStartCallback(bool asServer)
{
base.OnStartCallback(asServer);
List<CachedOnChange> collection = asServer ? _serverOnChanges : _clientOnChanges;
if (OnChange != null)
{
foreach (CachedOnChange item in collection)
OnChange.Invoke(item.Operation, item.Key, item.Value, asServer);
}
collection.Clear();
}
/// <summary>
/// Writes an operation and data required by all operations.
/// </summary>
private void WriteOperationHeader(PooledWriter writer, SyncDictionaryOperation operation, int collectionCountAfterChange)
{
writer.WriteUInt8Unpacked((byte)operation);
writer.WriteInt32(collectionCountAfterChange);
}
/// <summary>
/// Reads an operation and data required by all operations.
/// </summary>
private void ReadOperationHeader(PooledReader reader, out SyncDictionaryOperation operation, out int collectionCountAfterChange)
{
operation = (SyncDictionaryOperation)reader.ReadUInt8Unpacked();
collectionCountAfterChange = reader.ReadInt32();
}
/// <summary>
/// Writes all changed values.
/// Internal use.
/// May be used for custom SyncObjects.
/// </summary>
/// <param name = "writer"></param>
/// <param name = "resetSyncTick">True to set the next time data may sync.</param>
[APIExclude]
protected internal override void WriteDelta(PooledWriter writer, bool resetSyncTick = true)
{
// If sending all then clear changed and write full.
if (_sendAll)
{
_sendAll = false;
_changed.Clear();
WriteFull(writer);
}
else
{
base.WriteDelta(writer, resetSyncTick);
// False for not full write.
writer.WriteBoolean(false);
writer.WriteInt32(_changed.Count);
for (int i = 0; i < _changed.Count; i++)
{
ChangeData change = _changed[i];
WriteOperationHeader(writer, change.Operation, change.CollectionCountAfterChange);
// Clear does not need to write anymore data so it is not included in checks.
if (change.Operation == SyncDictionaryOperation.Add || change.Operation == SyncDictionaryOperation.Set)
{
writer.Write(change.Key);
writer.Write(change.Value);
}
else if (change.Operation == SyncDictionaryOperation.Remove)
{
writer.Write(change.Key);
}
}
_changed.Clear();
}
}
/// <summary>
/// Writers all values if not initial values.
/// Internal use.
/// May be used for custom SyncObjects.
/// </summary>
/// <param name = "writer"></param>
[APIExclude]
protected internal override void WriteFull(PooledWriter writer)
{
if (!_valuesChanged)
return;
base.WriteHeader(writer, false);
// True for full write.
writer.WriteBoolean(true);
writer.WriteInt32(Collection.Count);
int iteration = 0;
foreach (KeyValuePair<TKey, TValue> item in Collection)
{
WriteOperationHeader(writer, SyncDictionaryOperation.Add, iteration + 1);
writer.Write(item.Key);
writer.Write(item.Value);
iteration++;
}
}
/// <summary>
/// Reads and sets the current values for server or client.
/// </summary>
[APIExclude]
protected internal override void Read(PooledReader reader, bool asServer)
{
SetReadArguments(reader, asServer, out bool newChangeId, out bool asClientHost, out bool canModifyValues);
// True to warn if this object was deinitialized on the server.
bool deinitialized = asClientHost && !OnStartServerCalled;
if (deinitialized)
NetworkManager.LogWarning($"SyncType {GetType().Name} received a Read but was deinitialized on the server. Client callback values may be incorrect. This is a ClientHost limitation.");
IDictionary<TKey, TValue> collection = Collection;
bool fullWrite = reader.ReadBoolean();
// Clear collection since it's a full write.
if (canModifyValues && fullWrite)
collection.Clear();
int changes = reader.ReadInt32();
for (int i = 0; i < changes; i++)
{
ReadOperationHeader(reader, out SyncDictionaryOperation operation, out int collectionCountAfterChange);
TKey key = default;
TValue value = default;
/* Add, Set.
* Use the Set code for add and set,
* especially so collection doesn't throw
* if entry has already been added. */
if (operation == SyncDictionaryOperation.Add || operation == SyncDictionaryOperation.Set)
{
/* If a set then the collection count should remain the same.
* Otherwise, the count should increase by 1. */
int sizeExpectedAfterChange = operation == SyncDictionaryOperation.Add ? collection.Count + 1 : collection.Count;
key = reader.Read<TKey>();
value = reader.Read<TValue>();
if (canModifyValues)
{
// Integrity validation.
if (sizeExpectedAfterChange == collectionCountAfterChange)
collection[key] = value;
}
}
// Clear.
else if (operation == SyncDictionaryOperation.Clear)
{
if (canModifyValues)
{
// No integrity validation needed.
collection.Clear();
}
}
//Remove.
else if (operation == SyncDictionaryOperation.Remove)
{
key = reader.Read<TKey>();
if (canModifyValues)
{
//Integrity validation.
if (collection.Count - 1 == collectionCountAfterChange)
collection.Remove(key);
}
}
if (newChangeId)
InvokeOnChange(operation, key, value, false);
}
//If changes were made invoke complete after all have been read.
if (newChangeId && changes > 0)
InvokeOnChange(SyncDictionaryOperation.Complete, default, default, false);
}
/// <summary>
/// Invokes OnChanged callback.
/// </summary>
private void InvokeOnChange(SyncDictionaryOperation operation, TKey key, TValue value, bool asServer)
{
if (asServer)
{
if (NetworkBehaviour.OnStartServerCalled)
OnChange?.Invoke(operation, key, value, asServer);
else
_serverOnChanges.Add(new(operation, key, value));
}
else
{
if (NetworkBehaviour.OnStartClientCalled)
OnChange?.Invoke(operation, key, value, asServer);
else
_clientOnChanges.Add(new(operation, key, value));
}
}
/// <summary>
/// Resets to initialized values.
/// </summary>
[APIExclude]
protected internal override void ResetState(bool asServer)
{
base.ResetState(asServer);
if (CanReset(asServer))
{
_sendAll = false;
_changed.Clear();
Collection.Clear();
_valuesChanged = false;
foreach (KeyValuePair<TKey, TValue> item in _initialValues)
Collection[item.Key] = item.Value;
}
}
/// <summary>
/// Adds item.
/// </summary>
/// <param name = "item">Item to add.</param>
public void Add(KeyValuePair<TKey, TValue> item)
{
Add(item.Key, item.Value);
}
/// <summary>
/// Adds key and value.
/// </summary>
/// <param name = "key">Key to add.</param>
/// <param name = "value">Value for key.</param>
public void Add(TKey key, TValue value)
{
Add(key, value, true);
}
private void Add(TKey key, TValue value, bool asServer)
{
if (!CanNetworkSetValues(true))
return;
Collection.Add(key, value);
/* We can perform add operation without checks, as Add would have failed above
* if entry already existed. */
if (asServer)
AddOperation(SyncDictionaryOperation.Add, key, value, Collection.Count);
}
/// <summary>
/// Clears all values.
/// </summary>
public void Clear()
{
Clear(true);
}
private void Clear(bool asServer)
{
if (!CanNetworkSetValues(true))
return;
Collection.Clear();
if (asServer)
AddOperation(SyncDictionaryOperation.Clear, default, default, Collection.Count);
}
/// <summary>
/// Returns if key exist.
/// </summary>
/// <param name = "key">Key to use.</param>
/// <returns>True if found.</returns>
public bool ContainsKey(TKey key)
{
return Collection.ContainsKey(key);
}
/// <summary>
/// Returns if item exist.
/// </summary>
/// <param name = "item">Item to use.</param>
/// <returns>True if found.</returns>
public bool Contains(KeyValuePair<TKey, TValue> item)
{
return TryGetValue(item.Key, out TValue value) && EqualityComparer<TValue>.Default.Equals(value, item.Value);
}
/// <summary>
/// Copies collection to an array.
/// </summary>
/// <param name = "array">Array to copy to.</param>
/// <param name = "offset">Offset of array data is copied to.</param>
public void CopyTo([NotNull] KeyValuePair<TKey, TValue>[] array, int offset)
{
if (offset <= -1 || offset >= array.Length)
{
NetworkManager.LogError($"Index is out of range.");
return;
}
int remaining = array.Length - offset;
if (remaining < Count)
{
NetworkManager.LogError($"Array is not large enough to copy data. Array is of length {array.Length}, index is {offset}, and number of values to be copied is {Count.ToString()}.");
return;
}
int i = offset;
foreach (KeyValuePair<TKey, TValue> item in Collection)
{
array[i] = item;
i++;
}
}
/// <summary>
/// Removes a key.
/// </summary>
/// <param name = "key">Key to remove.</param>
/// <returns>True if removed.</returns>
public bool Remove(TKey key)
{
if (!CanNetworkSetValues(true))
return false;
if (Collection.Remove(key))
{
AddOperation(SyncDictionaryOperation.Remove, key, default, Collection.Count);
return true;
}
return false;
}
/// <summary>
/// Removes an item.
/// </summary>
/// <param name = "item">Item to remove.</param>
/// <returns>True if removed.</returns>
public bool Remove(KeyValuePair<TKey, TValue> item)
{
return Remove(item.Key);
}
/// <summary>
/// Tries to get value from key.
/// </summary>
/// <param name = "key">Key to use.</param>
/// <param name = "value">Variable to output to.</param>
/// <returns>True if able to output value.</returns>
public bool TryGetValue(TKey key, out TValue value)
{
return Collection.TryGetValueIL2CPP(key, out value);
}
/// <summary>
/// Gets or sets value for a key.
/// </summary>
/// <param name = "key">Key to use.</param>
/// <returns>Value when using as Get.</returns>
public TValue this[TKey key]
{
get => Collection[key];
set
{
if (!CanNetworkSetValues(true))
return;
/* Change to Add if entry does not exist yet. */
SyncDictionaryOperation operation = Collection.ContainsKey(key) ? SyncDictionaryOperation.Set : SyncDictionaryOperation.Add;
Collection[key] = value;
AddOperation(operation, key, value, Collection.Count);
}
}
/// <summary>
/// Dirties the entire collection forcing a full send.
/// </summary>
public void DirtyAll()
{
if (!IsInitialized)
return;
if (!CanNetworkSetValues(log: true))
return;
if (base.Dirty())
_sendAll = true;
}
/// <summary>
/// Dirties an entry by key.
/// </summary>
/// <param name = "key">Key to dirty.</param>
public void Dirty(TKey key)
{
if (!IsInitialized)
return;
if (!CanNetworkSetValues(true))
return;
if (Collection.TryGetValueIL2CPP(key, out TValue value))
AddOperation(SyncDictionaryOperation.Set, key, value, Collection.Count);
}
/// <summary>
/// Dirties an entry by value.
/// This operation can be very expensive, will cause allocations, and may fail if your value cannot be compared.
/// </summary>
/// <param name = "value">Value to dirty.</param>
/// <returns>True if value was found and marked dirty.</returns>
public bool Dirty(TValue value, EqualityComparer<TValue> comparer = null)
{
if (!IsInitialized)
return false;
if (!CanNetworkSetValues(true))
return false;
if (comparer == null)
comparer = EqualityComparer<TValue>.Default;
foreach (KeyValuePair<TKey, TValue> item in Collection)
{
if (comparer.Equals(item.Value, value))
{
AddOperation(SyncDictionaryOperation.Set, item.Key, value, Collection.Count);
return true;
}
}
//Not found.
return false;
}
/// <summary>
/// Gets the IEnumerator for the collection.
/// </summary>
/// <returns></returns>
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => Collection.GetEnumerator();
/// <summary>
/// Gets the IEnumerator for the collection.
/// </summary>
/// <returns></returns>
IEnumerator IEnumerable.GetEnumerator() => Collection.GetEnumerator();
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 79196d6c6e862da499491d106f66ad72
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/SyncDictionary.cs
uploadId: 866910
@@ -0,0 +1,29 @@
using FishNet.Documenting;
namespace FishNet.Object.Synchronizing
{
[APIExclude]
public enum SyncDictionaryOperation : byte
{
/// <summary>
/// A key and value have been added to the collection.
/// </summary>
Add,
/// <summary>
/// Collection has been cleared.
/// </summary>
Clear,
/// <summary>
/// A key was removed from the collection.
/// </summary>
Remove,
/// <summary>
/// A value has been set for a key in the collection.
/// </summary>
Set,
/// <summary>
/// All operations for the tick have been processed.
/// </summary>
Complete
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: d5d6ed9db47a8224fa9ed4d2ff54586f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/SyncDictionaryOperation.cs
uploadId: 866910
@@ -0,0 +1,29 @@
using FishNet.Documenting;
namespace FishNet.Object.Synchronizing
{
[APIExclude]
public enum SyncHashSetOperation : byte
{
/// <summary>
/// An item is added to the collection.
/// </summary>
Add,
/// <summary>
/// An item is removed from the collection.
/// </summary>
Remove,
/// <summary>
/// Collection is cleared.
/// </summary>
Clear,
/// <summary>
/// An item has been updated within the collection. This is generally used when modifying data within a container.
/// </summary>
Set,
/// <summary>
/// All operations for the tick have been processed. This only occurs on clients as the server is unable to be aware of when the user is done modifying the list.
/// </summary>
Complete
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 914089f5707003340a68fd6cd718e4c4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/SyncHashSetOperation.cs
uploadId: 866910
@@ -0,0 +1,667 @@
using FishNet.Documenting;
using FishNet.Managing;
using FishNet.Object.Synchronizing.Internal;
using FishNet.Serializing;
using GameKit.Dependencies.Utilities;
using System.Collections;
using System.Collections.Generic;
namespace FishNet.Object.Synchronizing
{
[System.Serializable]
public class SyncHashSet<T> : SyncBase, ISet<T>
{
#region Types.
/// <summary>
/// Information needed to invoke a callback.
/// </summary>
private struct CachedOnChange
{
internal readonly SyncHashSetOperation Operation;
internal readonly T Item;
public CachedOnChange(SyncHashSetOperation operation, T item)
{
Operation = operation;
Item = item;
}
}
/// <summary>
/// Information about how the collection has changed.
/// </summary>
private struct ChangeData
{
internal readonly SyncHashSetOperation Operation;
internal readonly T Item;
internal readonly int CollectionCountAfterChange;
public ChangeData(SyncHashSetOperation operation, T item, int collectionCountAfterChange)
{
Operation = operation;
Item = item;
CollectionCountAfterChange = collectionCountAfterChange;
}
}
#endregion
#region Public.
/// <summary>
/// Implementation from List<T>. Not used.
/// </summary>
[APIExclude]
public bool IsReadOnly => false;
/// <summary>
/// Delegate signature for when SyncList changes.
/// </summary>
/// <param name = "op">Type of change.</param>
/// <param name = "item">Item which was modified.</param>
/// <param name = "asServer">True if callback is occuring on the server.</param>
[APIExclude]
public delegate void SyncHashSetChanged(SyncHashSetOperation op, T item, bool asServer);
/// <summary>
/// Called when the SyncList changes.
/// </summary>
public event SyncHashSetChanged OnChange;
/// <summary>
/// Collection of objects.
/// </summary>
public HashSet<T> Collection;
/// <summary>
/// Number of objects in the collection.
/// </summary>
public int Count => Collection.Count;
#endregion
#region Private.
/// <summary>
/// ListCache for comparing.
/// </summary>
private static List<T> _cache = new();
/// <summary>
/// Values upon initialization.
/// </summary>
private HashSet<T> _initialValues;
/// <summary>
/// Changed data which will be sent next tick.
/// </summary>
private List<ChangeData> _changed;
/// <summary>
/// Server OnChange events waiting for start callbacks.
/// </summary>
private List<CachedOnChange> _serverOnChanges;
/// <summary>
/// Client OnChange events waiting for start callbacks.
/// </summary>
private List<CachedOnChange> _clientOnChanges;
/// <summary>
/// Comparer to see if entries change when calling public methods.
/// // Not used right now.
/// </summary>
private readonly IEqualityComparer<T> _comparer;
/// <summary>
/// True if values have changed since initialization.
/// The only reasonable way to reset this during a Reset call is by duplicating the original list and setting all values to it on reset.
/// </summary>
private bool _valuesChanged;
/// <summary>
/// True to send all values in the next WriteDelta.
/// </summary>
private bool _sendAll;
#endregion
#region Constructors.
public SyncHashSet(SyncTypeSettings settings = new()) : this(CollectionCaches<T>.RetrieveHashSet(), EqualityComparer<T>.Default, settings) { }
public SyncHashSet(IEqualityComparer<T> comparer, SyncTypeSettings settings = new()) : this(CollectionCaches<T>.RetrieveHashSet(), comparer == null ? EqualityComparer<T>.Default : comparer, settings) { }
public SyncHashSet(HashSet<T> collection, IEqualityComparer<T> comparer = null, SyncTypeSettings settings = new()) : base(settings)
{
_comparer = comparer == null ? EqualityComparer<T>.Default : comparer;
Collection = collection == null ? CollectionCaches<T>.RetrieveHashSet() : collection;
_initialValues = CollectionCaches<T>.RetrieveHashSet();
_changed = CollectionCaches<ChangeData>.RetrieveList();
_serverOnChanges = CollectionCaches<CachedOnChange>.RetrieveList();
_clientOnChanges = CollectionCaches<CachedOnChange>.RetrieveList();
}
#endregion
#region Deconstructor.
~SyncHashSet()
{
CollectionCaches<T>.StoreAndDefault(ref Collection);
CollectionCaches<T>.StoreAndDefault(ref _initialValues);
CollectionCaches<ChangeData>.StoreAndDefault(ref _changed);
CollectionCaches<CachedOnChange>.StoreAndDefault(ref _serverOnChanges);
CollectionCaches<CachedOnChange>.StoreAndDefault(ref _clientOnChanges);
}
#endregion
/// <summary>
/// Called when the SyncType has been registered, but not yet initialized over the network.
/// </summary>
protected override void Initialized()
{
base.Initialized();
// Initialize collections if needed. OdinInspector can cause them to become deinitialized.
#if ODIN_INSPECTOR
if (_initialValues == null)
_initialValues = new();
if (_changed == null)
_changed = new();
if (_serverOnChanges == null)
_serverOnChanges = new();
if (_clientOnChanges == null)
_clientOnChanges = new();
#endif
foreach (T item in Collection)
_initialValues.Add(item);
}
/// <summary>
/// Gets the collection being used within this SyncList.
/// </summary>
/// <returns></returns>
public HashSet<T> GetCollection(bool asServer)
{
return Collection;
}
/// <summary>
/// Adds an operation and invokes locally.
/// </summary>
private void AddOperation(SyncHashSetOperation operation, T item, int collectionCountAfterChange)
{
if (!IsInitialized)
return;
bool asServerInvoke = !IsNetworkInitialized || NetworkBehaviour.IsServerStarted;
if (asServerInvoke)
{
_valuesChanged = true;
if (base.Dirty())
{
ChangeData change = new(operation, item, collectionCountAfterChange);
_changed.Add(change);
}
}
InvokeOnChange(operation, item, asServerInvoke);
}
/// <summary>
/// Called after OnStartXXXX has occurred.
/// </summary>
/// <param name = "asServer">True if OnStartServer was called, false if OnStartClient.</param>
protected internal override void OnStartCallback(bool asServer)
{
base.OnStartCallback(asServer);
List<CachedOnChange> collection = asServer ? _serverOnChanges : _clientOnChanges;
if (OnChange != null)
{
foreach (CachedOnChange item in collection)
OnChange.Invoke(item.Operation, item.Item, asServer);
}
collection.Clear();
}
/// <summary>
/// Writes an operation and data required by all operations.
/// </summary>
private void WriteOperationHeader(PooledWriter writer, SyncHashSetOperation operation, int collectionCountAfterChange)
{
writer.WriteUInt8Unpacked((byte)operation);
writer.WriteInt32(collectionCountAfterChange);
}
/// <summary>
/// Reads an operation and data required by all operations.
/// </summary>
private void ReadOperationHeader(PooledReader reader, out SyncHashSetOperation operation, out int collectionCountAfterChange)
{
operation = (SyncHashSetOperation)reader.ReadUInt8Unpacked();
collectionCountAfterChange = reader.ReadInt32();
}
/// <summary>
/// Writes all changed values.
/// </summary>
/// <param name = "writer"></param>
/// <param name = "resetSyncTick">True to set the next time data may sync.</param>
protected internal override void WriteDelta(PooledWriter writer, bool resetSyncTick = true)
{
// If sending all then clear changed and write full.
if (_sendAll)
{
_sendAll = false;
_changed.Clear();
WriteFull(writer);
}
else
{
base.WriteDelta(writer, resetSyncTick);
// False for not full write.
writer.WriteBoolean(false);
writer.WriteInt32(_changed.Count);
for (int i = 0; i < _changed.Count; i++)
{
ChangeData change = _changed[i];
WriteOperationHeader(writer, change.Operation, change.CollectionCountAfterChange);
// Clear does not need to write anymore data so it is not included in checks.
if (change.Operation == SyncHashSetOperation.Add || change.Operation == SyncHashSetOperation.Remove || change.Operation == SyncHashSetOperation.Set)
writer.Write(change.Item);
}
_changed.Clear();
}
}
/// <summary>
/// Writes all values if not initial values.
/// </summary>
/// <param name = "writer"></param>
protected internal override void WriteFull(PooledWriter writer)
{
if (!_valuesChanged)
return;
base.WriteHeader(writer, false);
// True for full write.
writer.WriteBoolean(true);
int count = Collection.Count;
writer.WriteInt32(count);
int iteration = 0;
foreach (T item in Collection)
{
WriteOperationHeader(writer, SyncHashSetOperation.Add, collectionCountAfterChange: iteration + 1);
writer.Write(item);
iteration++;
}
}
/// <summary>
/// Reads and sets the current values for server or client.
/// </summary>
[APIExclude]
protected internal override void Read(PooledReader reader, bool asServer)
{
SetReadArguments(reader, asServer, out bool newChangeId, out bool asClientHost, out bool canModifyValues);
// True to warn if this object was deinitialized on the server.
bool deinitialized = asClientHost && !OnStartServerCalled;
if (deinitialized)
NetworkManager.LogWarning($"SyncType {GetType().Name} received a Read but was deinitialized on the server. Client callback values may be incorrect. This is a ClientHost limitation.");
ISet<T> collection = Collection;
bool fullWrite = reader.ReadBoolean();
// Clear collection since it's a full write.
if (canModifyValues && fullWrite)
collection.Clear();
int changes = reader.ReadInt32();
for (int i = 0; i < changes; i++)
{
ReadOperationHeader(reader, out SyncHashSetOperation operation, out int collectionCountAfterChange);
T next = default;
// Add.
if (operation == SyncHashSetOperation.Add)
{
next = reader.Read<T>();
if (canModifyValues)
{
// Integrity validation.
if (collection.Count + 1 == collectionCountAfterChange)
collection.Add(next);
}
}
// Clear.
else if (operation == SyncHashSetOperation.Clear)
{
if (canModifyValues)
{
// No integrity validation needed.
collection.Clear();
}
}
// Remove.
else if (operation == SyncHashSetOperation.Remove)
{
next = reader.Read<T>();
if (canModifyValues)
{
// Integrity validation.
if (collection.Count - 1 == collectionCountAfterChange)
collection.Remove(next);
}
}
// Set.
else if (operation == SyncHashSetOperation.Set)
{
next = reader.Read<T>();
if (canModifyValues)
{
// Integrity validation.
if (collection.Count == collectionCountAfterChange)
{
collection.Remove(next);
collection.Add(next);
}
}
}
if (newChangeId)
InvokeOnChange(operation, next, false);
}
// If changes were made invoke complete after all have been read.
if (newChangeId && changes > 0)
InvokeOnChange(SyncHashSetOperation.Complete, default, false);
}
/// <summary>
/// Invokes OnChanged callback.
/// </summary>
private void InvokeOnChange(SyncHashSetOperation operation, T item, bool asServer)
{
if (asServer)
{
if (NetworkBehaviour.OnStartServerCalled)
OnChange?.Invoke(operation, item, asServer);
else
_serverOnChanges.Add(new(operation, item));
}
else
{
if (NetworkBehaviour.OnStartClientCalled)
OnChange?.Invoke(operation, item, asServer);
else
_clientOnChanges.Add(new(operation, item));
}
}
/// <summary>
/// Resets to initialized values.
/// </summary>
protected internal override void ResetState(bool asServer)
{
base.ResetState(asServer);
if (CanReset(asServer))
{
_sendAll = false;
_changed.Clear();
Collection.Clear();
foreach (T item in _initialValues)
Collection.Add(item);
}
}
/// <summary>
/// Adds value.
/// </summary>
/// <param name = "item"></param>
public bool Add(T item)
{
return Add(item, true);
}
private bool Add(T item, bool asServer)
{
if (!CanNetworkSetValues(true))
return false;
bool result = Collection.Add(item);
// Only process if add was successful.
if (result && asServer)
AddOperation(SyncHashSetOperation.Add, item, Collection.Count);
return result;
}
/// <summary>
/// Adds a range of values.
/// </summary>
/// <param name = "range"></param>
public void AddRange(IEnumerable<T> range)
{
foreach (T entry in range)
Add(entry, true);
}
/// <summary>
/// Clears all values.
/// </summary>
public void Clear()
{
Clear(true);
}
private void Clear(bool asServer)
{
if (!CanNetworkSetValues(true))
return;
Collection.Clear();
if (asServer)
AddOperation(SyncHashSetOperation.Clear, default, Collection.Count);
}
/// <summary>
/// Returns if value exist.
/// </summary>
/// <param name = "item"></param>
/// <returns></returns>
public bool Contains(T item)
{
return Collection.Contains(item);
}
/// <summary>
/// Removes a value.
/// </summary>
/// <param name = "item"></param>
/// <returns></returns>
public bool Remove(T item)
{
return Remove(item, true);
}
private bool Remove(T item, bool asServer)
{
if (!CanNetworkSetValues(true))
return false;
bool result = Collection.Remove(item);
// Only process if remove was successful.
if (result && asServer)
AddOperation(SyncHashSetOperation.Remove, item, Collection.Count);
return result;
}
/// <summary>
/// Dirties the entire collection forcing a full send.
/// </summary>
public void DirtyAll()
{
if (!IsInitialized)
return;
if (!CanNetworkSetValues(log: true))
return;
if (base.Dirty())
_sendAll = true;
}
/// <summary>
/// Looks up obj in Collection and if found marks it's index as dirty.
/// This operation can be very expensive, will cause allocations, and may fail if your value cannot be compared.
/// </summary>
/// <param name = "obj">Object to lookup.</param>
public void Dirty(T obj)
{
if (!IsInitialized)
return;
if (!CanNetworkSetValues(true))
return;
foreach (T item in Collection)
{
if (item.Equals(obj))
{
AddOperation(SyncHashSetOperation.Set, obj, Collection.Count);
return;
}
}
// Not found.
NetworkManager.LogError($"Could not find object within SyncHashSet, dirty will not be set.");
}
/// <summary>
/// Returns Enumerator for collection.
/// </summary>
/// <returns></returns>
public IEnumerator GetEnumerator() => Collection.GetEnumerator();
[APIExclude]
IEnumerator<T> IEnumerable<T>.GetEnumerator() => Collection.GetEnumerator();
[APIExclude]
IEnumerator IEnumerable.GetEnumerator() => Collection.GetEnumerator();
public void ExceptWith(IEnumerable<T> other)
{
// Again, removing from self is a clear.
if (other == Collection)
{
Clear();
}
else
{
foreach (T item in other)
Remove(item);
}
}
public void IntersectWith(IEnumerable<T> other)
{
ISet<T> set;
if (other is ISet<T> setA)
set = setA;
else
set = new HashSet<T>(other);
IntersectWith(set);
}
private void IntersectWith(ISet<T> other)
{
_cache.AddRange(Collection);
int count = _cache.Count;
for (int i = 0; i < count; i++)
{
T entry = _cache[i];
if (!other.Contains(entry))
Remove(entry);
}
_cache.Clear();
}
public bool IsProperSubsetOf(IEnumerable<T> other)
{
return Collection.IsProperSubsetOf(other);
}
public bool IsProperSupersetOf(IEnumerable<T> other)
{
return Collection.IsProperSupersetOf(other);
}
public bool IsSubsetOf(IEnumerable<T> other)
{
return Collection.IsSubsetOf(other);
}
public bool IsSupersetOf(IEnumerable<T> other)
{
return Collection.IsSupersetOf(other);
}
public bool Overlaps(IEnumerable<T> other)
{
bool result = Collection.Overlaps(other);
return result;
}
public bool SetEquals(IEnumerable<T> other)
{
return Collection.SetEquals(other);
}
public void SymmetricExceptWith(IEnumerable<T> other)
{
// If calling except on self then that is the same as a clear.
if (other == Collection)
{
Clear();
}
else
{
foreach (T item in other)
Remove(item);
}
}
public void UnionWith(IEnumerable<T> other)
{
if (other == Collection)
return;
foreach (T item in other)
Add(item);
}
/// <summary>
/// Adds an item.
/// </summary>
/// <param name = "item"></param>
void ICollection<T>.Add(T item)
{
Add(item, true);
}
/// <summary>
/// Copies values to an array.
/// </summary>
/// <param name = "array"></param>
/// <param name = "index"></param>
public void CopyTo(T[] array, int index)
{
Collection.CopyTo(array, index);
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: b8627bf3171f6274790bc7e60e471260
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/SyncHashset.cs
uploadId: 866910
@@ -0,0 +1,778 @@
using FishNet.Documenting;
using FishNet.Managing;
using FishNet.Object.Synchronizing.Internal;
using FishNet.Serializing;
using GameKit.Dependencies.Utilities;
using System;
using System.Collections;
using System.Collections.Generic;
namespace FishNet.Object.Synchronizing
{
[Serializable]
public class SyncList<T> : SyncBase, IList<T>, IReadOnlyList<T>
{
#region Types.
/// <summary>
/// Information needed to invoke a callback.
/// </summary>
private struct CachedOnChange
{
internal readonly SyncListOperation Operation;
internal readonly int Index;
internal readonly T Previous;
internal readonly T Next;
public CachedOnChange(SyncListOperation operation, int index, T previous, T next)
{
Operation = operation;
Index = index;
Previous = previous;
Next = next;
}
}
/// <summary>
/// Information about how the collection has changed.
/// </summary>
private struct ChangeData
{
internal readonly SyncListOperation Operation;
internal readonly int EntryIndex;
internal readonly T Item;
internal readonly int CollectionCountAfterChange;
public ChangeData(SyncListOperation operation, int entryIndex, T item, int collectionCountAfterChange)
{
Operation = operation;
EntryIndex = entryIndex;
Item = item;
CollectionCountAfterChange = collectionCountAfterChange;
}
}
#endregion
#region Public.
/// <summary>
/// Implementation from List<T>. Not used.
/// </summary>
[APIExclude]
public bool IsReadOnly => false;
/// <summary>
/// Delegate signature for when SyncList changes.
/// </summary>
/// <param name = "op"></param>
/// <param name = "index"></param>
/// <param name = "oldItem"></param>
/// <param name = "newItem"></param>
[APIExclude]
public delegate void SyncListChanged(SyncListOperation op, int index, T oldItem, T newItem, bool asServer);
/// <summary>
/// Called when the SyncList changes.
/// </summary>
public event SyncListChanged OnChange;
/// <summary>
/// Collection of objects.
/// </summary>
public List<T> Collection;
/// <summary>
/// Number of objects in the collection.
/// </summary>
public int Count => Collection.Count;
#endregion
#region Private.
/// <summary>
/// Values upon initialization.
/// </summary>
private List<T> _initialValues;
/// <summary>
/// Comparer to see if entries change when calling public methods.
/// </summary>
private readonly IEqualityComparer<T> _comparer;
/// <summary>
/// Changed data which will be sent next tick.
/// </summary>
private List<ChangeData> _changed;
/// <summary>
/// Server OnChange events waiting for start callbacks.
/// </summary>
private List<CachedOnChange> _serverOnChanges;
/// <summary>
/// Client OnChange events waiting for start callbacks.
/// </summary>
private List<CachedOnChange> _clientOnChanges;
/// <summary>
/// True if values have changed since initialization.
/// The only reasonable way to reset this during a Reset call is by duplicating the original list and setting all values to it on reset.
/// </summary>
private bool _valuesChanged;
/// <summary>
/// True to send all values in the next WriteDelta.
/// </summary>
private bool _sendAll;
#endregion
#region Constructors.
public SyncList(SyncTypeSettings settings = new()) : this(CollectionCaches<T>.RetrieveList(), EqualityComparer<T>.Default, settings) { }
public SyncList(IEqualityComparer<T> comparer, SyncTypeSettings settings = new()) : this(new(), comparer == null ? EqualityComparer<T>.Default : comparer, settings) { }
public SyncList(List<T> collection, IEqualityComparer<T> comparer = null, SyncTypeSettings settings = new()) : base(settings)
{
_comparer = comparer == null ? EqualityComparer<T>.Default : comparer;
Collection = collection == null ? CollectionCaches<T>.RetrieveList() : collection;
_initialValues = CollectionCaches<T>.RetrieveList();
_changed = CollectionCaches<ChangeData>.RetrieveList();
_serverOnChanges = CollectionCaches<CachedOnChange>.RetrieveList();
_clientOnChanges = CollectionCaches<CachedOnChange>.RetrieveList();
}
#endregion
#region Deconstructor.
~SyncList()
{
CollectionCaches<T>.StoreAndDefault(ref Collection);
CollectionCaches<T>.StoreAndDefault(ref _initialValues);
CollectionCaches<ChangeData>.StoreAndDefault(ref _changed);
CollectionCaches<CachedOnChange>.StoreAndDefault(ref _serverOnChanges);
CollectionCaches<CachedOnChange>.StoreAndDefault(ref _clientOnChanges);
}
#endregion
/// <summary>
/// Called when the SyncType has been registered, but not yet initialized over the network.
/// </summary>
protected override void Initialized()
{
base.Initialized();
// Initialize collections if needed. OdinInspector can cause them to become deinitialized.
#if ODIN_INSPECTOR
if (_initialValues == null)
_initialValues = new();
if (_changed == null)
_changed = new();
if (_serverOnChanges == null)
_serverOnChanges = new();
if (_clientOnChanges == null)
_clientOnChanges = new();
#endif
foreach (T item in Collection)
_initialValues.Add(item);
}
/// <summary>
/// Gets the collection being used within this SyncList.
/// </summary>
/// <param name = "asServer">True if returning the server value, false if client value. The values will only differ when running as host. While asServer is true the most current values on server will be returned, and while false the latest values received by client will be returned.</param>
/// <returns></returns>
public List<T> GetCollection(bool asServer)
{
return Collection;
}
/// <summary>
/// Adds an operation and invokes locally.
/// </summary>
/// <param name = "operation"></param>
/// <param name = "index"></param>
/// <param name = "prev"></param>
/// <param name = "next"></param>
private void AddOperation(SyncListOperation operation, int index, T prev, T next, int collectionCountAfterChange)
{
if (!IsInitialized)
return;
/* asServer might be true if the client is setting the value
* through user code. Typically synctypes can only be set
* by the server, that's why it is assumed asServer via user code.
* However, when excluding owner for the synctype the client should
* have permission to update the value locally for use with
* prediction. */
bool asServerInvoke = !IsNetworkInitialized || NetworkBehaviour.IsServerStarted;
/* Only the adds asServer may set
* this synctype as dirty and add
* to pending changes. However, the event may still
* invoke for clientside. */
if (asServerInvoke)
{
/* Set as changed even if cannot dirty.
* Dirty is only set when there are observers,
* but even if there are not observers
* values must be marked as changed so when
* there are observers, new values are sent. */
_valuesChanged = true;
/* If unable to dirty then do not add to changed.
* A dirty may fail if the server is not started
* or if there's no observers. Changed doesn't need
* to be populated in this situations because clients
* will get the full collection on spawn. If we
* were to also add to changed clients would get the full
* collection as well the changed, which would double results. */
if (base.Dirty())
{
ChangeData change = new(operation, index, next, collectionCountAfterChange);
_changed.Add(change);
}
}
InvokeOnChange(operation, index, prev, next, asServerInvoke);
}
/// <summary>
/// Called after OnStartXXXX has occurred.
/// </summary>
/// <param name = "asServer">True if OnStartServer was called, false if OnStartClient.</param>
protected internal override void OnStartCallback(bool asServer)
{
base.OnStartCallback(asServer);
List<CachedOnChange> collection = asServer ? _serverOnChanges : _clientOnChanges;
if (OnChange != null)
{
foreach (CachedOnChange item in collection)
OnChange.Invoke(item.Operation, item.Index, item.Previous, item.Next, asServer);
}
collection.Clear();
}
/// <summary>
/// Writes an operation and data required by all operations.
/// </summary>
private void WriteOperationHeader(PooledWriter writer, SyncListOperation operation, int entryIndex, int collectionCountAfterChange)
{
writer.WriteUInt8Unpacked((byte)operation);
writer.WriteInt32(entryIndex);
writer.WriteInt32(collectionCountAfterChange);
}
/// <summary>
/// Reads an operation and data required by all operations.
/// </summary>
private void ReadOperationHeader(PooledReader reader, out SyncListOperation operation, out int entryIndex, out int collectionCountAfterChange)
{
operation = (SyncListOperation)reader.ReadUInt8Unpacked();
entryIndex = reader.ReadInt32();
collectionCountAfterChange = reader.ReadInt32();
}
/// <summary>
/// Writes all changed values.
/// </summary>
/// <param name = "writer"></param>
/// <param name = "resetSyncTick">True to set the next time data may sync.</param>
protected internal override void WriteDelta(PooledWriter writer, bool resetSyncTick = true)
{
// If sending all then clear changed and write full.
if (_sendAll)
{
_sendAll = false;
_changed.Clear();
WriteFull(writer);
}
else
{
base.WriteDelta(writer, resetSyncTick);
// False for not full write.
writer.WriteBoolean(false);
// Number of entries expected.
writer.WriteInt32(_changed.Count);
for (int i = 0; i < _changed.Count; i++)
{
ChangeData change = _changed[i];
WriteOperationHeader(writer, change.Operation, change.EntryIndex, change.CollectionCountAfterChange);
// Clear does not need to write anymore data so it is not included in checks.
if (change.Operation == SyncListOperation.Add)
{
writer.Write(change.Item);
}
else if (change.Operation == SyncListOperation.RemoveAt)
{
// Entry index already written in header.
}
else if (change.Operation == SyncListOperation.Insert || change.Operation == SyncListOperation.Set)
{
writer.Write(change.Item);
}
}
_changed.Clear();
}
}
/// <summary>
/// Writes all values if not initial values.
/// </summary>
/// <param name = "writer"></param>
protected internal override void WriteFull(PooledWriter writer)
{
if (!_valuesChanged)
return;
base.WriteHeader(writer, false);
// True for full write.
writer.WriteBoolean(true);
int count = Collection.Count;
writer.WriteInt32(count);
for (int i = 0; i < count; i++)
{
WriteOperationHeader(writer, SyncListOperation.Add, entryIndex: i, collectionCountAfterChange: i + 1);
writer.Write(Collection[i]);
}
}
/// <summary>
/// Reads and sets the current values for server or client.
/// </summary>
[APIExclude]
protected internal override void Read(PooledReader reader, bool asServer)
{
SetReadArguments(reader, asServer, out bool newChangeId, out bool asClientHost, out bool canModifyValues);
// True to warn if this object was deinitialized on the server.
bool deinitialized = asClientHost && !OnStartServerCalled;
if (deinitialized)
NetworkManager.LogWarning($"SyncType {GetType().Name} received a Read but was deinitialized on the server. Client callback values may be incorrect. This is a ClientHost limitation.");
List<T> collection = Collection;
bool fullWrite = reader.ReadBoolean();
// Clear collection since it's a full write.
if (canModifyValues && fullWrite)
collection.Clear();
int changes = reader.ReadInt32();
for (int i = 0; i < changes; i++)
{
ReadOperationHeader(reader, out SyncListOperation operation, out int entryIndex, out int collectionCountAfterChange);
T prev = default;
T next = default;
// Add.
if (operation == SyncListOperation.Add || operation == SyncListOperation.Insert)
{
next = reader.Read<T>();
if (canModifyValues)
{
// Integrity validation.
if (collection.Count + 1 == collectionCountAfterChange && entryIndex <= collection.Count)
collection.Insert(entryIndex, next);
}
}
// Clear.
else if (operation == SyncListOperation.Clear)
{
if (canModifyValues)
{
//No integrity validation needed.
collection.Clear();
}
}
//RemoveAt.
else if (operation == SyncListOperation.RemoveAt)
{
if (canModifyValues)
{
//Integrity validation.
if (collection.Count - 1 == collectionCountAfterChange && entryIndex < collection.Count)
{
prev = collection[entryIndex];
collection.RemoveAt(entryIndex);
}
}
}
//Set
else if (operation == SyncListOperation.Set)
{
next = reader.Read<T>();
if (canModifyValues)
{
//Integrity validation.
if (collection.Count == collectionCountAfterChange && entryIndex < collection.Count)
{
prev = collection[entryIndex];
collection[entryIndex] = next;
}
}
}
if (newChangeId)
InvokeOnChange(operation, entryIndex, prev, next, false);
}
//If changes were made invoke complete after all have been read.
if (newChangeId && changes > 0)
InvokeOnChange(SyncListOperation.Complete, -1, default, default, false);
}
/// <summary>
/// Invokes OnChanged callback.
/// </summary>
private void InvokeOnChange(SyncListOperation operation, int index, T prev, T next, bool asServer)
{
if (asServer)
{
if (NetworkBehaviour.OnStartServerCalled)
OnChange?.Invoke(operation, index, prev, next, asServer);
else
_serverOnChanges.Add(new(operation, index, prev, next));
}
else
{
if (NetworkBehaviour.OnStartClientCalled)
OnChange?.Invoke(operation, index, prev, next, asServer);
else
_clientOnChanges.Add(new(operation, index, prev, next));
}
}
/// <summary>
/// Resets to initialized values.
/// </summary>
protected internal override void ResetState(bool asServer)
{
base.ResetState(asServer);
if (CanReset(asServer))
{
_sendAll = false;
_changed.Clear();
Collection.Clear();
foreach (T item in _initialValues)
Collection.Add(item);
}
}
/// <summary>
/// Adds value.
/// </summary>
/// <param name = "item"></param>
public void Add(T item)
{
Add(item, true);
}
private void Add(T item, bool asServer)
{
if (!CanNetworkSetValues(true))
return;
Collection.Add(item);
if (asServer)
{
int entryIndex = Collection.Count - 1;
AddOperation(SyncListOperation.Add, entryIndex, default, item, Collection.Count);
}
}
/// <summary>
/// Adds a range of values.
/// </summary>
/// <param name = "range"></param>
public void AddRange(IEnumerable<T> range)
{
foreach (T entry in range)
Add(entry, true);
}
/// <summary>
/// Clears all values.
/// </summary>
public void Clear()
{
Clear(true);
}
private void Clear(bool asServer)
{
if (!CanNetworkSetValues(true))
return;
Collection.Clear();
if (asServer)
AddOperation(SyncListOperation.Clear, -1, default, default, Collection.Count);
}
/// <summary>
/// Returns if value exist.
/// </summary>
/// <param name = "item"></param>
/// <returns></returns>
public bool Contains(T item)
{
return IndexOf(item) >= 0;
}
/// <summary>
/// Copies values to an array.
/// </summary>
/// <param name = "array"></param>
/// <param name = "index"></param>
public void CopyTo(T[] array, int index)
{
Collection.CopyTo(array, index);
}
/// <summary>
/// Gets the index of value.
/// </summary>
/// <param name = "item"></param>
/// <returns></returns>
public int IndexOf(T item)
{
for (int i = 0; i < Collection.Count; ++i)
{
if (_comparer.Equals(item, Collection[i]))
return i;
}
return -1;
}
/// <summary>
/// Finds index using match.
/// </summary>
/// <param name = "match"></param>
/// <returns></returns>
public int FindIndex(Predicate<T> match)
{
for (int i = 0; i < Collection.Count; ++i)
{
if (match(Collection[i]))
return i;
}
return -1;
}
/// <summary>
/// Finds value using match.
/// </summary>
/// <param name = "match"></param>
/// <returns></returns>
public T Find(Predicate<T> match)
{
int i = FindIndex(match);
return i != -1 ? Collection[i] : default;
}
/// <summary>
/// Finds all values using match.
/// </summary>
/// <param name = "match"></param>
/// <returns></returns>
public List<T> FindAll(Predicate<T> match)
{
List<T> results = new();
for (int i = 0; i < Collection.Count; ++i)
{
if (match(Collection[i]))
results.Add(Collection[i]);
}
return results;
}
/// <summary>
/// Inserts value at index.
/// </summary>
/// <param name = "index"></param>
/// <param name = "item"></param>
public void Insert(int index, T item)
{
Insert(index, item, true);
}
private void Insert(int index, T item, bool asServer)
{
if (!CanNetworkSetValues(true))
return;
Collection.Insert(index, item);
if (asServer)
AddOperation(SyncListOperation.Insert, index, default, item, Collection.Count);
}
/// <summary>
/// Inserts a range of values.
/// </summary>
/// <param name = "index"></param>
/// <param name = "range"></param>
public void InsertRange(int index, IEnumerable<T> range)
{
foreach (T entry in range)
{
Insert(index, entry);
index++;
}
}
/// <summary>
/// Removes a value.
/// </summary>
/// <param name = "item"></param>
/// <returns></returns>
public bool Remove(T item)
{
int index = IndexOf(item);
bool result = index >= 0;
if (result)
RemoveAt(index);
return result;
}
/// <summary>
/// Removes value at index.
/// </summary>
/// <param name = "index"></param>
/// <param name = "asServer"></param>
public void RemoveAt(int index)
{
RemoveAt(index, true);
}
private void RemoveAt(int index, bool asServer)
{
if (!CanNetworkSetValues(true))
return;
T oldItem = Collection[index];
Collection.RemoveAt(index);
if (asServer)
AddOperation(SyncListOperation.RemoveAt, index, oldItem, default, Collection.Count);
}
/// <summary>
/// Removes all values within the collection.
/// </summary>
/// <param name = "match"></param>
/// <returns></returns>
public int RemoveAll(Predicate<T> match)
{
List<T> toRemove = new();
for (int i = 0; i < Collection.Count; ++i)
{
if (match(Collection[i]))
toRemove.Add(Collection[i]);
}
foreach (T entry in toRemove)
Remove(entry);
return toRemove.Count;
}
/// <summary>
/// Gets or sets value at an index.
/// </summary>
/// <param name = "i"></param>
/// <returns></returns>
public T this[int i]
{
get => Collection[i];
set => Set(i, value, true, true);
}
/// <summary>
/// Dirties the entire collection forcing a full send.
/// This will not invoke the callback on server.
/// </summary>
public void DirtyAll()
{
if (!IsInitialized)
return;
if (!CanNetworkSetValues(true))
return;
if (base.Dirty())
_sendAll = true;
}
/// <summary>
/// Looks up obj in Collection and if found marks it's index as dirty.
/// While using this operation previous value will be the same as next.
/// This operation can be very expensive, and may fail if your value cannot be compared.
/// </summary>
/// <param name = "obj">Object to lookup.</param>
public void Dirty(T obj)
{
int index = Collection.IndexOf(obj);
if (index != -1)
Dirty(index);
else
NetworkManager.LogError($"Could not find object within SyncList, dirty will not be set.");
}
/// <summary>
/// Marks an index as dirty.
/// While using this operation previous value will be the same as next.
/// </summary>
/// <param name = "index"></param>
public void Dirty(int index)
{
if (!CanNetworkSetValues(true))
return;
T value = Collection[index];
AddOperation(SyncListOperation.Set, index, value, value, Collection.Count);
}
/// <summary>
/// Sets value at index.
/// </summary>
/// <param name = "index"></param>
/// <param name = "value"></param>
public void Set(int index, T value, bool force = true)
{
Set(index, value, true, force);
}
/// <summary>
/// Sets a value at an index.
/// </summary>
private void Set(int index, T value, bool asServer, bool force)
{
if (!CanNetworkSetValues(true))
return;
bool sameValue = !force && _comparer.Equals(Collection[index], value);
if (!sameValue)
{
T prev = Collection[index];
Collection[index] = value;
if (asServer)
AddOperation(SyncListOperation.Set, index, prev, value, Collection.Count);
}
}
/// <summary>
/// Returns Enumerator for collection.
/// </summary>
/// <returns></returns>
public IEnumerator<T> GetEnumerator() => Collection.GetEnumerator();
[APIExclude]
IEnumerator<T> IEnumerable<T>.GetEnumerator() => Collection.GetEnumerator();
[APIExclude]
IEnumerator IEnumerable.GetEnumerator() => Collection.GetEnumerator();
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 9561e48a6cd328040aa2a6de9193e677
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/SyncList.cs
uploadId: 866910
@@ -0,0 +1,33 @@
using FishNet.Documenting;
namespace FishNet.Object.Synchronizing
{
[APIExclude]
public enum SyncListOperation : byte
{
/// <summary>
/// An item is added to the collection.
/// </summary>
Add,
/// <summary>
/// An item is inserted into the collection.
/// </summary>
Insert,
/// <summary>
/// An item is set in the collection.
/// </summary>
Set,
/// <summary>
/// An item is removed from the collection.
/// </summary>
RemoveAt,
/// <summary>
/// Collection is cleared.
/// </summary>
Clear,
/// <summary>
/// All operations for the tick have been processed. This only occurs on clients as the server is unable to be aware of when the user is done modifying the list.
/// </summary>
Complete
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 4fa53fc807605df4997f0b63a6570bcf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/SyncListOperation.cs
uploadId: 866910
@@ -0,0 +1,388 @@
using FishNet.Documenting;
using FishNet.Object.Synchronizing.Internal;
using FishNet.Serializing;
using System.Collections.Generic;
namespace FishNet.Object.Synchronizing
{
/// <summary>
/// A SyncObject to efficiently synchronize Stopwatchs over the network.
/// </summary>
public class SyncStopwatch : SyncBase, ICustomSync
{
#region Type.
/// <summary>
/// Information about how the Stopwatch has changed.
/// </summary>
private struct ChangeData
{
public readonly SyncStopwatchOperation Operation;
public readonly float Previous;
public ChangeData(SyncStopwatchOperation operation, float previous)
{
Operation = operation;
Previous = previous;
}
}
#endregion
#region Public.
/// <summary>
/// Delegate signature for when the Stopwatch operation occurs.
/// </summary>
/// <param name = "op">Operation which was performed.</param>
/// <param name = "prev">Previous value of the Stopwatch. This will be -1f is the value is not available.</param>
/// <param name = "asServer">True if occurring on server.</param>
public delegate void SyncTypeChanged(SyncStopwatchOperation op, float prev, bool asServer);
/// <summary>
/// Called when a Stopwatch operation occurs.
/// </summary>
public event SyncTypeChanged OnChange;
/// <summary>
/// How much time has passed since the Stopwatch started.
/// </summary>
public float Elapsed { get; private set; } = -1f;
/// <summary>
/// True if the SyncStopwatch is currently paused. Calls to Update(float) will be ignored when paused.
/// </summary>
public bool Paused { get; private set; }
#endregion
#region Private.
/// <summary>
/// Changed data which will be sent next tick.
/// </summary>
private List<ChangeData> _changed = new();
/// <summary>
/// Server OnChange events waiting for start callbacks.
/// </summary>
private List<ChangeData> _serverOnChanges = new();
/// <summary>
/// Client OnChange events waiting for start callbacks.
/// </summary>
private List<ChangeData> _clientOnChanges = new();
#endregion
#region Constructors
public SyncStopwatch(SyncTypeSettings settings = new()) : base(settings) { }
#endregion
/// <summary>
/// Called when the SyncType has been registered, but not yet initialized over the network.
/// </summary>
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
}
/// <summary>
/// Starts a Stopwatch. If called when a Stopwatch is already active then StopStopwatch will automatically be sent.
/// </summary>
/// <param name = "remaining">Time in which the Stopwatch should start with.</param>
/// <param name = "sendElapsedOnStop">True to include remaining time when automatically sending StopStopwatch.</param>
public void StartStopwatch(bool sendElapsedOnStop = true)
{
if (!CanNetworkSetValues(true))
return;
if (Elapsed > 0f)
StopStopwatch(sendElapsedOnStop);
Elapsed = 0f;
AddOperation(SyncStopwatchOperation.Start, 0f);
}
/// <summary>
/// Pauses the Stopwatch. Calling while already paused will be result in no action.
/// </summary>
/// <param name = "sendElapsed">True to send Remaining with this operation.</param>
public void PauseStopwatch(bool sendElapsed = false)
{
if (Elapsed < 0f)
return;
if (Paused)
return;
if (!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);
}
/// <summary>
/// Unpauses the Stopwatch. Calling while already unpaused will be result in no action.
/// </summary>
public void UnpauseStopwatch()
{
if (Elapsed < 0f)
return;
if (!Paused)
return;
if (!CanNetworkSetValues(true))
return;
Paused = false;
AddOperation(SyncStopwatchOperation.Unpause, -1f);
}
/// <summary>
/// Stops and resets the Stopwatch.
/// </summary>
public void StopStopwatch(bool sendElapsed = false)
{
if (Elapsed < 0f)
return;
if (!CanNetworkSetValues(true))
return;
float prev = sendElapsed ? -1f : Elapsed;
StopStopwatch_Internal(true);
SyncStopwatchOperation op = sendElapsed ? SyncStopwatchOperation.StopUpdated : SyncStopwatchOperation.Stop;
AddOperation(op, prev);
}
/// <summary>
/// Adds an operation to synchronize.
/// </summary>
private void AddOperation(SyncStopwatchOperation operation, float prev)
{
if (!IsInitialized)
return;
bool asServerInvoke = !IsNetworkInitialized || NetworkBehaviour.IsServerStarted;
if (asServerInvoke)
{
if (Dirty())
{
ChangeData change = new(operation, prev);
_changed.Add(change);
}
}
OnChange?.Invoke(operation, prev, asServerInvoke);
}
/// <summary>
/// Writes all changed values.
/// </summary>
/// <param name = "resetSyncTick">True to set the next time data may sync.</param>
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();
}
/// <summary>
/// Writes all values.
/// </summary>
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);
}
/// <summary>
/// Writers a start with elapsed time.
/// </summary>
/// <param name = "elapsed"></param>
private void WriteStartStopwatch(Writer w, float elapsed, bool includeOperationByte)
{
if (includeOperationByte)
w.WriteUInt8Unpacked((byte)SyncStopwatchOperation.Start);
w.WriteSingle(elapsed);
}
/// <summary>
/// Reads and sets the current values for server or client.
/// </summary>
[APIExclude]
protected internal override void Read(PooledReader reader, bool asServer)
{
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 (canModifyValues)
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);
}
/// <summary>
/// Stops the Stopwatch and resets.
/// </summary>
private void StopStopwatch_Internal(bool asServer)
{
Paused = false;
Elapsed = -1f;
}
/// <summary>
/// Invokes OnChanged callback.
/// </summary>
private void InvokeOnChange(SyncStopwatchOperation operation, float prev, bool asServer)
{
if (asServer)
{
if (NetworkBehaviour.OnStartServerCalled)
OnChange?.Invoke(operation, prev, asServer);
else
_serverOnChanges.Add(new(operation, prev));
}
else
{
if (NetworkBehaviour.OnStartClientCalled)
OnChange?.Invoke(operation, prev, asServer);
else
_clientOnChanges.Add(new(operation, prev));
}
}
/// <summary>
/// Called after OnStartXXXX has occurred.
/// </summary>
/// <param name = "asServer">True if OnStartServer was called, false if OnStartClient.</param>
protected internal override void OnStartCallback(bool asServer)
{
base.OnStartCallback(asServer);
List<ChangeData> collection = asServer ? _serverOnChanges : _clientOnChanges;
if (OnChange != null)
{
foreach (ChangeData item in collection)
OnChange.Invoke(item.Operation, item.Previous, asServer);
}
collection.Clear();
}
/// <summary>
/// Adds delta from Remaining for server and client.
/// </summary>
/// <param name = "delta">Value to remove from Remaining.</param>
public void Update(float delta)
{
//Not enabled.
if (Elapsed == -1f)
return;
if (Paused)
return;
Elapsed += delta;
}
/// <summary>
/// Return the serialized type.
/// </summary>
/// <returns></returns>
public object GetSerializedType() => null;
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 1aa50198b4dd1bc45a6b7ef0461c5809
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/SyncStopwatch.cs
uploadId: 866910
@@ -0,0 +1,34 @@
namespace FishNet.Object.Synchronizing
{
public enum SyncStopwatchOperation : byte
{
/// <summary>
/// Stopwatch is started. Value is included with start.
/// </summary>
Start = 1,
/// <summary>
/// Stopwatch was paused.
/// </summary>
Pause = 2,
/// <summary>
/// Stopwatch was paused. Value at time of pause is sent.
/// </summary>
PauseUpdated = 3,
/// <summary>
/// Stopwatch was unpaused.
/// </summary>
Unpause = 4,
/// <summary>
/// Stopwatch was stopped.
/// </summary>
Stop = 6,
/// <summary>
/// Stopwatch was stopped. Value prior to stopping is sent.
/// </summary>
StopUpdated = 7,
/// <summary>
/// All operations for the tick have been processed. This only occurs on clients as the server is unable to be aware of when the user is done modifying the list.
/// </summary>
Complete = 9
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 3ef101e3b1527224ca96ef61c238adbb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/SyncStopwatchOperation.cs
uploadId: 866910
@@ -0,0 +1,479 @@
using FishNet.Documenting;
using FishNet.Object.Synchronizing.Internal;
using FishNet.Serializing;
using System.Collections.Generic;
using UnityEngine;
namespace FishNet.Object.Synchronizing
{
/// <summary>
/// A SyncObject to efficiently synchronize timers over the network.
/// </summary>
public class SyncTimer : SyncBase, ICustomSync
{
#region Type.
/// <summary>
/// Information about how the timer has changed.
/// </summary>
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.
/// <summary>
/// Delegate signature for when the timer operation occurs.
/// </summary>
/// <param name = "op">Operation which was performed.</param>
/// <param name = "prev">Previous value of the timer. This will be -1f is the value is not available.</param>
/// <param name = "next">Value of the timer. This will be -1f is the value is not available.</param>
/// <param name = "asServer">True if occurring on server.</param>
public delegate void SyncTypeChanged(SyncTimerOperation op, float prev, float next, bool asServer);
/// <summary>
/// Called when a timer operation occurs.
/// </summary>
public event SyncTypeChanged OnChange;
/// <summary>
/// Time remaining on the timer. When the timer is expired this value will be 0f.
/// </summary>
public float Remaining { get; private set; }
/// <summary>
/// How much time has passed since the timer started.
/// </summary>
public float Elapsed => Duration - Remaining;
/// <summary>
/// Starting duration of the timer.
/// </summary>
public float Duration { get; private set; }
/// <summary>
/// True if the SyncTimer is currently paused. Calls to Update(float) will be ignored when paused.
/// </summary>
public bool Paused { get; private set; }
#endregion
#region Private.
/// <summary>
/// Changed data which will be sent next tick.
/// </summary>
private List<ChangeData> _changed = new();
/// <summary>
/// Server OnChange events waiting for start callbacks.
/// </summary>
private List<ChangeData> _serverOnChanges = new();
/// <summary>
/// Client OnChange events waiting for start callbacks.
/// </summary>
private List<ChangeData> _clientOnChanges = new();
/// <summary>
/// Last Time.unscaledTime the timer delta was updated.
/// </summary>
private float _updateTime;
#endregion
#region Constructors
public SyncTimer(SyncTypeSettings settings = new()) : base(settings) { }
#endregion
/// <summary>
/// Called when the SyncType has been registered, but not yet initialized over the network.
/// </summary>
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
}
/// <summary>
/// Starts a timer. If called when a timer is already active then StopTimer will automatically be sent.
/// </summary>
/// <param name = "remaining">Time in which the timer should start with.</param>
/// <param name = "sendRemainingOnStop">True to include remaining time when automatically sending StopTimer.</param>
public void StartTimer(float remaining, bool sendRemainingOnStop = true)
{
if (!CanNetworkSetValues(true))
return;
if (Remaining > 0f)
StopTimer(sendRemainingOnStop);
Paused = false;
Remaining = remaining;
Duration = remaining;
SetUpdateTime();
AddOperation(SyncTimerOperation.Start, -1f, remaining);
}
/// <summary>
/// Pauses the timer. Calling while already paused will be result in no action.
/// </summary>
/// <param name = "sendRemaining">True to send Remaining with this operation.</param>
public void PauseTimer(bool sendRemaining = false)
{
if (Remaining <= 0f)
return;
if (Paused)
return;
if (!CanNetworkSetValues(true))
return;
Paused = true;
SyncTimerOperation op = sendRemaining ? SyncTimerOperation.PauseUpdated : SyncTimerOperation.Pause;
AddOperation(op, Remaining, Remaining);
}
/// <summary>
/// Unpauses the timer. Calling while already unpaused will be result in no action.
/// </summary>
public void UnpauseTimer()
{
if (Remaining <= 0f)
return;
if (!Paused)
return;
if (!CanNetworkSetValues(true))
return;
Paused = false;
SetUpdateTime();
AddOperation(SyncTimerOperation.Unpause, Remaining, Remaining);
}
/// <summary>
/// Stops and resets the timer.
/// </summary>
public void StopTimer(bool sendRemaining = false)
{
if (Remaining <= 0f)
return;
if (!CanNetworkSetValues(true))
return;
bool asServer = true;
float prev = Remaining;
StopTimer_Internal(asServer);
SyncTimerOperation op = sendRemaining ? SyncTimerOperation.StopUpdated : SyncTimerOperation.Stop;
AddOperation(op, prev, 0f);
}
/// <summary>
/// Adds an operation to synchronize.
/// </summary>
private void AddOperation(SyncTimerOperation operation, float prev, float next)
{
if (!IsInitialized)
return;
bool asServerInvoke = !IsNetworkInitialized || NetworkBehaviour.IsServerStarted;
if (asServerInvoke)
{
if (Dirty())
{
ChangeData change = new(operation, prev, next);
_changed.Add(change);
}
}
OnChange?.Invoke(operation, prev, next, asServerInvoke);
}
/// <summary>
/// Writes all changed values.
/// </summary>
/// <param name = "resetSyncTick">True to set the next time data may sync.</param>
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();
}
/// <summary>
/// Writes all values.
/// </summary>
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);
}
/// <summary>
/// Writes a StartTimer operation.
/// </summary>
/// <param name = "w"></param>
/// <param name = "includeOperationByte"></param>
private void WriteStartTimer(Writer w, bool includeOperationByte)
{
if (includeOperationByte)
w.WriteUInt8Unpacked((byte)SyncTimerOperation.Start);
w.WriteSingle(Remaining);
w.WriteSingle(Duration);
}
/// <summary>
/// Reads and sets the current values for server or client.
/// </summary>
[APIExclude]
protected internal override void Read(PooledReader reader, bool asServer)
{
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);
}
/// <summary>
/// Stops the timer and resets.
/// </summary>
private void StopTimer_Internal(bool asServer)
{
Paused = false;
Remaining = 0f;
}
/// <summary>
/// Invokes OnChanged callback.
/// </summary>
private void InvokeOnChange(SyncTimerOperation operation, float prev, float next, bool asServer)
{
if (asServer)
{
if (NetworkBehaviour.OnStartServerCalled)
OnChange?.Invoke(operation, prev, next, asServer);
else
_serverOnChanges.Add(new(operation, prev, next));
}
else
{
if (NetworkBehaviour.OnStartClientCalled)
OnChange?.Invoke(operation, prev, next, asServer);
else
_clientOnChanges.Add(new(operation, prev, next));
}
}
/// <summary>
/// Called after OnStartXXXX has occurred.
/// </summary>
/// <param name = "asServer">True if OnStartServer was called, false if OnStartClient.</param>
protected internal override void OnStartCallback(bool asServer)
{
base.OnStartCallback(asServer);
List<ChangeData> collection = asServer ? _serverOnChanges : _clientOnChanges;
if (OnChange != null)
{
foreach (ChangeData item in collection)
OnChange.Invoke(item.Operation, item.Previous, item.Next, asServer);
}
collection.Clear();
}
/// <summary>
/// Sets updateTime to current values.
/// </summary>
private void SetUpdateTime()
{
_updateTime = Time.unscaledTime;
}
/// <summary>
/// Removes time passed from Remaining since the last unscaled time using this method.
/// </summary>
public void Update()
{
float delta = Time.unscaledTime - _updateTime;
Update(delta);
}
/// <summary>
/// Removes delta from Remaining for server and client.
/// This also resets unscaledTime delta for Update().
/// </summary>
/// <param name = "delta">Value to remove from Remaining.</param>
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);
}
/// <summary>
/// Invokes SyncTimer finished a previous value.
/// </summary>
/// <param name = "prev"></param>
private void InvokeFinished(float prev)
{
if (NetworkManager.IsServerStarted)
OnChange?.Invoke(SyncTimerOperation.Finished, prev, 0f, true);
if (NetworkManager.IsClientStarted)
OnChange?.Invoke(SyncTimerOperation.Finished, prev, 0f, false);
}
/// <summary>
/// Return the serialized type.
/// </summary>
/// <returns></returns>
public object GetSerializedType() => null;
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 46c58fcbaf8624a49834273c8e6eb871
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/SyncTimer.cs
uploadId: 866910
@@ -0,0 +1,38 @@
namespace FishNet.Object.Synchronizing
{
public enum SyncTimerOperation : byte
{
/// <summary>
/// Timer is started. Value is included with start.
/// </summary>
Start = 1,
/// <summary>
/// Timer was paused.
/// </summary>
Pause = 2,
/// <summary>
/// Timer was paused. Value at time of pause is sent.
/// </summary>
PauseUpdated = 3,
/// <summary>
/// Timer was unpaused.
/// </summary>
Unpause = 4,
/// <summary>
/// Timer was stopped.
/// </summary>
Stop = 6,
/// <summary>
/// Timer was stopped. Value prior to stopping is sent.
/// </summary>
StopUpdated = 7,
/// <summary>
/// The timer has ended finished it's duration.
/// </summary>
Finished = 8,
/// <summary>
/// All operations for the tick have been processed. This only occurs on clients as the server is unable to be aware of when the user is done modifying the list.
/// </summary>
Complete = 9
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: c7732c822303d3c4387a4af44467e791
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/SyncTimerOperation.cs
uploadId: 866910
@@ -0,0 +1,78 @@
using FishNet.Transporting;
namespace FishNet.Object.Synchronizing
{
/// <summary>
/// Settings which can be passed into SyncTypes.
/// </summary>
[System.Serializable]
public struct SyncTypeSettings
{
public WritePermission WritePermission;
public ReadPermission ReadPermission;
public float SendRate;
public Channel Channel;
internal bool IsDefault()
{
return WritePermission == WritePermission.ServerOnly && ReadPermission == ReadPermission.Observers && SendRate == 0f && Channel == (Channel)0;
}
// Work around for C# parameterless struct limitation.
public SyncTypeSettings(float sendRate = 0.1f)
{
WritePermission = WritePermission.ServerOnly;
ReadPermission = ReadPermission.Observers;
SendRate = sendRate;
Channel = Channel.Reliable;
}
public SyncTypeSettings(float sendRate, Channel channel)
{
WritePermission = WritePermission.ServerOnly;
ReadPermission = ReadPermission.Observers;
SendRate = sendRate;
Channel = channel;
}
public SyncTypeSettings(Channel channel)
{
WritePermission = WritePermission.ServerOnly;
ReadPermission = ReadPermission.Observers;
SendRate = 0.1f;
Channel = channel;
}
public SyncTypeSettings(WritePermission writePermissions)
{
WritePermission = writePermissions;
ReadPermission = ReadPermission.Observers;
SendRate = 0.1f;
Channel = Channel.Reliable;
}
public SyncTypeSettings(ReadPermission readPermissions)
{
WritePermission = WritePermission.ServerOnly;
ReadPermission = readPermissions;
SendRate = 0.1f;
Channel = Channel.Reliable;
}
public SyncTypeSettings(WritePermission writePermissions, ReadPermission readPermissions)
{
WritePermission = writePermissions;
ReadPermission = readPermissions;
SendRate = 0.1f;
Channel = Channel.Reliable;
}
public SyncTypeSettings(WritePermission writePermissions, ReadPermission readPermissions, float sendRate, Channel channel)
{
WritePermission = writePermissions;
ReadPermission = readPermissions;
SendRate = sendRate;
Channel = channel;
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 9ae08e01a0057a84b84ed5f228724839
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/SyncTypeSetting.cs
uploadId: 866910
@@ -0,0 +1,15 @@
namespace FishNet.Object.Synchronizing
{
[System.Flags]
internal enum SyncTypeWriteFlag
{
Unset = 0,
IgnoreInterval = 1,
ForceReliable = 2
}
internal static class SyncTypeWriteFlagExtensions
{
public static bool FastContains(this SyncTypeWriteFlag whole, SyncTypeWriteFlag part) => (whole & part) == part;
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 3ee97e78eaa779d46a91541c9babe061
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/SyncTypeWriteFlag.cs
uploadId: 866910
@@ -0,0 +1,473 @@
using FishNet.CodeGenerating;
using FishNet.Documenting;
using FishNet.Managing;
using FishNet.Object.Helping;
using FishNet.Object.Synchronizing.Internal;
using FishNet.Serializing;
using FishNet.Serializing.Helping;
using System.Runtime.InteropServices;
using UnityEngine;
namespace FishNet.Object.Synchronizing
{
internal interface ISyncVar { }
[APIExclude]
[System.Serializable]
[StructLayout(LayoutKind.Auto, CharSet = CharSet.Auto)]
public class SyncVar<T> : SyncBase, ISyncVar
{
#region Types.
public struct InterpolationContainer
{
/// <summary>
/// Value prior to setting new.
/// </summary>
public T LastValue;
/// <summary>
/// Tick when LastValue was set.
/// </summary>
public float UpdateTime;
public void Update(T prevValue)
{
LastValue = prevValue;
UpdateTime = Time.unscaledTime;
}
}
/// <summary>
/// Information needed to invoke a callback.
/// </summary>
private struct CachedOnChange
{
internal readonly T Previous;
internal readonly T Next;
public CachedOnChange(T previous, T next)
{
Previous = previous;
Next = next;
}
}
#endregion
#region Public.
/// <summary>
/// Value interpolated between last received and current.
/// </summary>
/// <param name = "useCurrentValue">
/// True if to ignore interpolated calculations and use the current value.
/// This can be useful if you are able to write this SyncVars values in update.
/// </param>
public T InterpolatedValue(bool useCurrentValue = false)
{
if (useCurrentValue)
return _value;
float diff = Time.unscaledTime - _interpolator.UpdateTime;
float percent = Mathf.InverseLerp(0f, Settings.SendRate, diff);
return Interpolate(_interpolator.LastValue, _value, percent);
}
/// <summary>
/// Gets and sets the current value for this SyncVar.
/// </summary>
public T Value
{
get => _value;
set => SetValue(value, true);
}
///// <summary>
///// Sets the current value for this SyncVar while sending it immediately.
///// </summary>
// public T ValueRpc
// {
// set => SetValue(value, true, true);
// }
///// <summary>
///// Gets the current value for this SyncVar while marking it dirty. This could be useful to change properties or fields on a reference type SyncVar and have the SyncVar be dirtied after.
///// </summary>
// public T ValueDirty
// {
// get
// {
// base.Dirty();
// return _value;
// }
// }
///// <summary>
///// Gets the current value for this SyncVar while sending it imediately. This could be useful to change properties or fields on a reference type SyncVar and have the SyncVar send after.
///// </summary>
// public T ValueDirtyRpc
// {
// get
// {
// base.Dirty(true);
// return _value;
// }
// }
/// <summary>
/// Called when the SyncDictionary changes.
/// </summary>
public event OnChanged OnChange;
public delegate void OnChanged(T prev, T next, bool asServer);
#endregion
#region Private.
/// <summary>
/// Server OnChange event waiting for start callbacks.
/// </summary>
private CachedOnChange? _serverOnChange;
/// <summary>
/// Client OnChange event waiting for start callbacks.
/// </summary>
private CachedOnChange? _clientOnChange;
/// <summary>
/// Value before the network is initialized on the containing object.
/// </summary>
private T _initialValue;
/// <summary>
/// Current value on the server, or client.
/// </summary>
[SerializeField]
private T _value;
/// <summary>
/// Holds information about interpolating between values.
/// </summary>
private InterpolationContainer _interpolator = new();
/// <summary>
/// True if value was ever set after the SyncType initialized.
/// This is true even if SetInitialValues was called at runtime.
/// </summary>
private bool _valueSetAfterInitialized;
#endregion
#region Constructors.
public SyncVar(SyncTypeSettings settings = new()) : this(default, settings) { }
public SyncVar(T initialValue, SyncTypeSettings settings = new()) : base(settings) => SetInitialValues(initialValue);
#endregion
/// <summary>
/// Called when the SyncType has been registered, but not yet initialized over the network.
/// </summary>
protected override void Initialized()
{
base.Initialized();
_initialValue = _value;
}
/// <summary>
/// Sets initial values.
/// Initial values are not automatically synchronized, as it is assumed clients and server already have them set to the specified value.
/// When a SyncVar is reset, such as when the object despawns, current values are set to initial values.
/// </summary>
public void SetInitialValues(T value)
{
_initialValue = value;
/* Only update current if a value has not been set already.
* A value normally would not be set unless a SyncVar came through
* as the object was enabling, such as if it started in a disabled state
* and was later enabled. */
if (!_valueSetAfterInitialized)
UpdateValues(value);
if (IsInitialized)
_valueSetAfterInitialized = true;
}
/// <summary>
/// Sets current and previous values.
/// </summary>
/// <param name = "next"></param>
private void UpdateValues(T next)
{
// If network initialized then update interpolator.
if (IsNetworkInitialized)
_interpolator.Update(_value);
_value = next;
}
/// <summary>
/// Sets current value and marks the SyncVar dirty when able to. Returns if able to set value.
/// </summary>
/// <param name = "calledByUser">True if SetValue was called in response to user code. False if from automated code.</param>
internal void SetValue(T nextValue, bool calledByUser, bool sendRpc = false)
{
/* IsInitialized is only set after the script containing this SyncVar
* has executed our codegen in the beginning of awake, and after awake
* user logic. When not set update the initial values */
if (!IsInitialized)
{
SetInitialValues(nextValue);
return;
}
_valueSetAfterInitialized = true;
/* If not client or server then set skipChecks
* as true. When neither is true it's likely user is changing
* value before object is initialized. This is allowed
* but checks cannot be processed because they would otherwise
* stop setting the value. */
bool isNetworkInitialized = IsNetworkInitialized;
// Object is deinitializing.
if (isNetworkInitialized && CodegenHelper.NetworkObject_Deinitializing(NetworkBehaviour))
return;
// If being set by user code.
if (calledByUser)
{
if (!CanNetworkSetValues(log: true))
return;
/* We will only be this far if the network is not active yet,
* server is active, or client has setting permissions.
* We only need to set asServerInvoke to false if the network
* is initialized and the server is not active. */
bool asServerInvoke = CanInvokeCallbackAsServer();
/* If the network has not been network initialized then
* Value is expected to be set on server and client since
* it's being set before the object is initialized. */
if (!isNetworkInitialized)
{
T prev = _value;
UpdateValues(nextValue);
// Still call invoke because change will be cached for when the network initializes.
InvokeOnChange(prev, _value, asServer: true);
}
else
{
if (Comparers.EqualityCompare(_value, nextValue))
return;
T prev = _value;
UpdateValues(nextValue);
InvokeOnChange(prev, _value, asServerInvoke);
}
TryDirty(asServerInvoke);
}
// Not called by user.
else
{
/* Only perform the equality checks when not host.
*
* In the previous SyncVar version it was okay to call
* this on host because a separate clientHost value was kept for
* the client side, and that was compared against.
*
* In newer SyncVar(this one) a client side copy is
* not kept so when compariing against the current vlaue
* as clientHost, it will always return as matched.
*
* But it's impossible for clientHost to send a value
* and it not have changed, so this check is not needed. */
// /* Previously clients were not allowed to set values
// * but this has been changed because clients may want
// * to update values locally while occasionally
// * letting the syncvar adjust their side. */
// T prev = _value;
// if (Comparers.EqualityCompare(prev, nextValue))
// return;
T prev = _value;
/* If also server do not update value.
* Server side has say of the current value. */
/* Only update value if not server. We do not want
* clientHost overwriting servers current with what
* they just received.*/
if (!NetworkManager.IsServerStarted)
UpdateValues(nextValue);
InvokeOnChange(prev, nextValue, asServer: false);
}
/* Tries to dirty so update
* is sent over network. This needs to be called
* anytime the data changes because there is no way
* to know if the user set the value on both server
* and client or just one side. */
void TryDirty(bool asServer)
{
//Cannot dirty when network is not initialized.
if (!isNetworkInitialized)
return;
if (asServer)
Dirty();
//base.Dirty(sendRpc);
}
}
/// <summary>
/// Returns interpolated values between previous and current using a percentage.
/// </summary>
protected virtual T Interpolate(T previous, T current, float percent)
{
NetworkManager.LogError($"Type {typeof(T).FullName} does not support interpolation. Implement a supported type class or create your own. See class FloatSyncVar for an example.");
return default;
}
/// <summary>
/// True if callback can be invoked with asServer true.
/// </summary>
/// <returns></returns>
private bool AsServerInvoke() => !IsNetworkInitialized || NetworkBehaviour.IsServerStarted;
/// <summary>
/// Dirties the the syncVar for a full send.
/// </summary>
public void DirtyAll()
{
if (!IsInitialized)
return;
if (!CanNetworkSetValues(log: true))
return;
//Also set that values have changed since the user is forcing a sync.
_valueSetAfterInitialized = true;
Dirty();
/* Invoke even if was unable to dirty. Dirtying only
* becomes true if server is running, but also if there are
* observers. Even if there are not observers we still want
* to invoke for the server side. */
//todo: this behaviour needs to be done for all synctypes with dirt/dirtyall.
bool asServerInvoke = CanInvokeCallbackAsServer();
InvokeOnChange(_value, _value, asServerInvoke);
}
/// <summary>
/// Invokes OnChanged callback.
/// </summary>
private void InvokeOnChange(T prev, T next, bool asServer)
{
if (asServer)
{
if (NetworkBehaviour.OnStartServerCalled)
OnChange?.Invoke(prev, next, asServer);
else
_serverOnChange = new CachedOnChange(prev, next);
}
else
{
if (NetworkBehaviour.OnStartClientCalled)
OnChange?.Invoke(prev, next, asServer);
else
_clientOnChange = new CachedOnChange(prev, next);
}
}
/// <summary>
/// Called after OnStartXXXX has occurred.
/// </summary>
/// <param name = "asServer">True if OnStartServer was called, false if OnStartClient.</param>
[MakePublic]
protected internal override void OnStartCallback(bool asServer)
{
base.OnStartCallback(asServer);
if (OnChange != null)
{
CachedOnChange? change = asServer ? _serverOnChange : _clientOnChange;
if (change != null)
InvokeOnChange(change.Value.Previous, change.Value.Next, asServer);
}
if (asServer)
_serverOnChange = null;
else
_clientOnChange = null;
}
/// <summary>
/// Writes current value.
/// </summary>
/// <param name = "resetSyncTick">True to set the next time data may sync.</param>
[MakePublic]
protected internal override void WriteDelta(PooledWriter writer, bool resetSyncTick = true)
{
base.WriteDelta(writer, resetSyncTick);
writer.Write(_value);
}
/// <summary>
/// Writes current value if not initialized value.
/// </summary>
/// m>
[MakePublic]
protected internal override void WriteFull(PooledWriter obj0)
{
// /* If a class then skip comparer check.
// * InitialValue and Value will be the same reference.
// *
// * If a value then compare field changes, since the references
// * will not be the same. */
// //Compare if a value type.
// if (_isValueType)
// {
// if (Comparers.EqualityCompare(_initialValue, _value))
// return;
// }
// else
// {
// if (!_valueSetAfterInitialized)
// return;
// }
if (!_valueSetAfterInitialized)
return;
/* SyncVars only hold latest value, so just
* write current delta. */
WriteDelta(obj0, false);
}
/// <summary>
/// Reads a SyncVar value.
/// </summary>
protected internal override void Read(PooledReader reader, bool asServer)
{
T value = reader.Read<T>();
if (!ReadChangeId(reader))
return;
SetValue(value, false);
//TODO this needs to separate invokes from setting values so that syncvar can be written like remainder of synctypes.
}
//SyncVars do not use changeId.
[APIExclude]
protected override bool ReadChangeId(Reader reader) => true;
//SyncVars do not use changeId.
[APIExclude]
protected override void WriteChangeId(PooledWriter writer) { }
/// <summary>
/// Resets to initialized values.
/// </summary>
[MakePublic]
protected internal override void ResetState(bool asServer)
{
base.ResetState(asServer);
/* Only full reset under the following conditions:
* asServer is true.
* Is not network initialized.
* asServer is false, and server is not started. */
if (CanReset(asServer))
{
_value = _initialValue;
_valueSetAfterInitialized = false;
}
}
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: d8d8d88365cea6445bd5268ac9ed2a86
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/SyncVar.cs
uploadId: 866910
@@ -0,0 +1,17 @@
namespace FishNet.Object.Synchronizing
{
/// <summary>
/// Which clients or server may write updates.
/// </summary>
public enum WritePermission : byte
{
/// <summary>
/// Only the server can change the value of the SyncType.
/// </summary>
ServerOnly = 0,
/// <summary>
/// Server and clients can change the value of the SyncType. When changed by client the value is not sent to the server.
/// </summary>
ClientUnsynchronized = 1
}
}
@@ -0,0 +1,18 @@
fileFormatVersion: 2
guid: 2696d0da2ff02e8499a8351a3021008f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 207815
packageName: 'FishNet: Networking Evolved'
packageVersion: 4.6.22R
assetPath: Assets/FishNet/Runtime/Object/Synchronizing/WritePermissions.cs
uploadId: 866910