fishnet installed
This commit is contained in:
19
Assets/FishNet/Runtime/Object/Synchronizing/ICustomSync.cs
Normal file
19
Assets/FishNet/Runtime/Object/Synchronizing/ICustomSync.cs
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
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,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2024b0be0cd1cc744a442f3e2e6ba483
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
48
Assets/FishNet/Runtime/Object/Synchronizing/ISyncObject.cs
Normal file
48
Assets/FishNet/Runtime/Object/Synchronizing/ISyncObject.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using FishNet.Managing;
|
||||
using FishNet.Serializing;
|
||||
using System;
|
||||
|
||||
namespace FishNet.Object.Synchronizing.Internal
|
||||
{
|
||||
/// <summary>
|
||||
/// A sync object is an object that can synchronize it's state
|
||||
/// between server and client, such as a SyncList
|
||||
/// </summary>
|
||||
public interface ISyncType
|
||||
{
|
||||
/// <summary>
|
||||
/// true if there are changes since the last flush
|
||||
/// </summary>
|
||||
bool IsDirty { get; }
|
||||
/// <summary>
|
||||
/// Sets index for the SyncType.
|
||||
/// </summary>
|
||||
void SetRegistered();
|
||||
/// <summary>
|
||||
/// PreInitializes this for use with the network.
|
||||
/// </summary>
|
||||
void PreInitialize(NetworkManager networkManager);
|
||||
/// <summary>
|
||||
/// Writes all changed values.
|
||||
/// </summary>
|
||||
/// <param name="writer"></param>
|
||||
///<param name="resetSyncTick">True to set the next time data may sync.</param>
|
||||
void WriteDelta(PooledWriter writer, bool resetSyncTick = true);
|
||||
/// <summary>
|
||||
/// Writers all values if not initial values.
|
||||
/// </summary>
|
||||
/// <param name="writer"></param>
|
||||
void WriteFull(PooledWriter writer);
|
||||
/// <summary>
|
||||
/// Sets current values.
|
||||
/// </summary>
|
||||
/// <param name="reader"></param>
|
||||
void Read(PooledReader reader);
|
||||
/// <summary>
|
||||
/// Resets the SyncObject so that it can be re-used
|
||||
/// </summary>
|
||||
void Reset();
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d0e81c03149ecd4eba926bba2d0edbe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,11 @@
|
||||
namespace FishNet.Object
|
||||
{
|
||||
|
||||
internal enum MissingObjectPacketLength : int
|
||||
{
|
||||
Reliable = -1,
|
||||
PurgeRemaiming = -2,
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3d177496f9519e246b8e3ef199d83437
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,21 @@
|
||||
namespace FishNet.Object.Synchronizing
|
||||
{
|
||||
/// <summary>
|
||||
/// Which clients may receive synchronization updates.
|
||||
/// </summary>
|
||||
public enum ReadPermission : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// All observers will receive updates.
|
||||
/// </summary>
|
||||
Observers,
|
||||
/// <summary>
|
||||
/// Only owner will receive updates.
|
||||
/// </summary>
|
||||
OwnerOnly,
|
||||
/// <summary>
|
||||
/// Send to all observers except owner.
|
||||
/// </summary>
|
||||
ExcludeOwner
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8050ef114e01f74409d8e29b821b6fc0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8c38d8297956cf34c9826ebe5fbadff6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,21 @@
|
||||
//using FishNet.Documenting;
|
||||
|
||||
//Remove on 2023/06/01
|
||||
//namespace FishNet.Object.Synchronizing.SecretMenu
|
||||
//{
|
||||
// /// <summary>
|
||||
// /// Internal SyncVar extensions.
|
||||
// /// </summary>
|
||||
// [APIExclude]
|
||||
// public static class SyncVarExtensions
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// Dirties SyncVars.
|
||||
// /// </summary>
|
||||
// /// <param name="obj"></param>
|
||||
// [APIExclude]
|
||||
// public static void Dirty(this object obj) { }
|
||||
// }
|
||||
|
||||
|
||||
//}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1f982cd721eb344c88bab105d779127
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
53
Assets/FishNet/Runtime/Object/Synchronizing/Settings.cs
Normal file
53
Assets/FishNet/Runtime/Object/Synchronizing/Settings.cs
Normal file
@ -0,0 +1,53 @@
|
||||
using FishNet.Transporting;
|
||||
|
||||
namespace FishNet.Object.Synchronizing.Internal
|
||||
{
|
||||
public class Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the write permissions for this var
|
||||
/// </summary>
|
||||
public WritePermission WritePermission = WritePermission.ServerOnly;
|
||||
/// <summary>
|
||||
/// Clients which may receive updated values.
|
||||
/// </summary>
|
||||
public ReadPermission ReadPermission = ReadPermission.Observers;
|
||||
/// <summary>
|
||||
/// How often this variable may synchronize.
|
||||
/// </summary>
|
||||
public float SendRate = 0f;
|
||||
/// <summary>
|
||||
/// Channel to send values on.
|
||||
/// </summary>
|
||||
public Channel Channel = Channel.Reliable;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new NetworkedVarSettings instance
|
||||
/// </summary>
|
||||
public Settings()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public Settings(WritePermission writePermission, ReadPermission readPermission, float sendRate, Channel channel)
|
||||
{
|
||||
WritePermission = writePermission;
|
||||
ReadPermission = readPermission;
|
||||
SendRate = sendRate;
|
||||
Channel = channel;
|
||||
}
|
||||
|
||||
public Settings(float sendTickrate)
|
||||
{
|
||||
SendRate = sendTickrate;
|
||||
}
|
||||
|
||||
public Settings(ReadPermission readPermission, float sendRate, Channel channel)
|
||||
{
|
||||
ReadPermission = readPermission;
|
||||
SendRate = sendRate;
|
||||
Channel = channel;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
11
Assets/FishNet/Runtime/Object/Synchronizing/Settings.cs.meta
Normal file
11
Assets/FishNet/Runtime/Object/Synchronizing/Settings.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 24688925098da4d42be8dbfa66b88b82
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
265
Assets/FishNet/Runtime/Object/Synchronizing/SyncBase.cs
Normal file
265
Assets/FishNet/Runtime/Object/Synchronizing/SyncBase.cs
Normal file
@ -0,0 +1,265 @@
|
||||
using FishNet.Managing;
|
||||
using FishNet.Managing.Timing;
|
||||
using FishNet.Serializing;
|
||||
using FishNet.Transporting;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FishNet.Object.Synchronizing.Internal
|
||||
{
|
||||
public class SyncBase : ISyncType
|
||||
{
|
||||
|
||||
#region Public.
|
||||
/// <summary>
|
||||
/// True if this SyncBase has been registered within it's containing class.
|
||||
/// </summary>
|
||||
public bool IsRegistered { get; private set; }
|
||||
/// <summary>
|
||||
/// True if the object for which this SyncType is for has been initialized for the network.
|
||||
/// </summary>
|
||||
public bool IsNetworkInitialized => (IsRegistered && (NetworkBehaviour.IsServer || NetworkBehaviour.IsClient));
|
||||
/// <summary>
|
||||
/// True if a SyncObject, false if a SyncVar.
|
||||
/// </summary>
|
||||
public bool IsSyncObject { get; private set; }
|
||||
/// <summary>
|
||||
/// The settings for this SyncVar.
|
||||
/// </summary>
|
||||
public Settings Settings = new Settings();
|
||||
/// <summary>
|
||||
/// How often updates may send.
|
||||
/// </summary>
|
||||
public 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>
|
||||
/// Next time a SyncVar may send data/
|
||||
/// </summary>
|
||||
public 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;
|
||||
#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;
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this SyncBase.
|
||||
/// </summary>
|
||||
public void InitializeInstance(NetworkBehaviour nb, uint syncIndex, WritePermission writePermissions, ReadPermission readPermissions, float tickRate, Channel channel, bool isSyncObject)
|
||||
{
|
||||
NetworkBehaviour = nb;
|
||||
SyncIndex = syncIndex;
|
||||
_currentChannel = channel;
|
||||
IsSyncObject = isSyncObject;
|
||||
Settings = new Settings()
|
||||
{
|
||||
WritePermission = writePermissions,
|
||||
ReadPermission = readPermissions,
|
||||
SendRate = tickRate,
|
||||
Channel = channel
|
||||
};
|
||||
|
||||
NetworkBehaviour.RegisterSyncType(this, SyncIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the SyncIndex.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetRegistered()
|
||||
{
|
||||
Registered();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the SyncType has been registered, but not yet initialized over the network.
|
||||
/// </summary>
|
||||
protected virtual void Registered()
|
||||
{
|
||||
IsRegistered = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PreInitializes this for use with the network.
|
||||
/// </summary>
|
||||
public void PreInitialize(NetworkManager networkManager)
|
||||
{
|
||||
NetworkManager = networkManager;
|
||||
if (Settings.SendRate < 0f)
|
||||
Settings.SendRate = networkManager.ServerManager.GetSynctypeRate();
|
||||
|
||||
_timeToTicks = NetworkManager.TimeManager.TimeToTicks(Settings.SendRate, TickRounding.RoundUp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after OnStartXXXX has occurred.
|
||||
/// </summary>
|
||||
/// <param name="asServer">True if OnStartServer was called, false if OnStartClient.</param>
|
||||
public virtual void OnStartCallback(bool asServer) { }
|
||||
|
||||
protected bool CanNetworkSetValues(bool warn = 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 (!IsRegistered)
|
||||
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.IsServer)
|
||||
return true;
|
||||
//Predicted spawning is enabled.
|
||||
if (NetworkManager != null && NetworkManager.PredictionManager.GetAllowPredictedSpawning() && NetworkBehaviour.NetworkObject.AllowPredictedSpawning)
|
||||
return true;
|
||||
/* If here then server is not active and additional
|
||||
* checks must be performed. */
|
||||
bool result = (Settings.ReadPermission == ReadPermission.ExcludeOwner && NetworkBehaviour.IsOwner);
|
||||
if (!result && warn)
|
||||
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.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dirties this Sync and the NetworkBehaviour.
|
||||
/// </summary>
|
||||
public bool Dirty()
|
||||
{
|
||||
/* 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(IsSyncObject);
|
||||
IsDirty |= canDirty;
|
||||
|
||||
return canDirty;
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="tick"></param>
|
||||
/// <returns></returns>
|
||||
internal bool WriteTimeMet(uint tick)
|
||||
{
|
||||
return (IsDirty && tick >= NextSyncTick);
|
||||
}
|
||||
/// <summary>
|
||||
/// Writes current value.
|
||||
/// </summary>
|
||||
/// <param name="writer"></param>
|
||||
/// <param name="resetSyncTick">True to set the next time data may sync.</param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public virtual void WriteDelta(PooledWriter writer, bool resetSyncTick = true)
|
||||
{
|
||||
WriteHeader(writer, resetSyncTick);
|
||||
}
|
||||
/// <summary>
|
||||
/// Writers the header for this SyncType.
|
||||
/// </summary>
|
||||
/// <param name="writer"></param>
|
||||
/// <param name="resetSyncTick"></param>
|
||||
protected virtual void WriteHeader(PooledWriter writer, bool resetSyncTick = true)
|
||||
{
|
||||
if (resetSyncTick)
|
||||
NextSyncTick = NetworkManager.TimeManager.Tick + _timeToTicks;
|
||||
|
||||
writer.WriteByte((byte)SyncIndex);
|
||||
}
|
||||
/// <summary>
|
||||
/// Writes current value if not initialized value.
|
||||
/// </summary>
|
||||
/// <param name="writer"></param>
|
||||
public virtual void WriteFull(PooledWriter writer) { }
|
||||
/// <summary>
|
||||
/// Sets current value as client.
|
||||
/// </summary>
|
||||
/// <param name="reader"></param>
|
||||
[Obsolete("Use Read(PooledReader, bool).")]
|
||||
public virtual void Read(PooledReader reader) { }
|
||||
/// <summary>
|
||||
/// Sets current value as server or client.
|
||||
/// </summary>
|
||||
/// <param name="reader"></param>
|
||||
/// <param name="asServer"></param>
|
||||
public virtual void Read(PooledReader reader, bool asServer) { }
|
||||
/// <summary>
|
||||
/// Resets to initialized values.
|
||||
/// </summary>
|
||||
public virtual void Reset()
|
||||
{
|
||||
NextSyncTick = 0;
|
||||
ResetDirty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
11
Assets/FishNet/Runtime/Object/Synchronizing/SyncBase.cs.meta
Normal file
11
Assets/FishNet/Runtime/Object/Synchronizing/SyncBase.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1a6f26e3f8016cc499b3fa99e7368fbc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
628
Assets/FishNet/Runtime/Object/Synchronizing/SyncDictionary.cs
Normal file
628
Assets/FishNet/Runtime/Object/Synchronizing/SyncDictionary.cs
Normal file
@ -0,0 +1,628 @@
|
||||
using FishNet.Documenting;
|
||||
using FishNet.Managing.Logging;
|
||||
using FishNet.Object.Synchronizing.Internal;
|
||||
using FishNet.Serializing;
|
||||
using FishNet.Utility.Extension;
|
||||
using JetBrains.Annotations;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FishNet.Object.Synchronizing
|
||||
{
|
||||
|
||||
public class SyncIDictionary<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;
|
||||
|
||||
public ChangeData(SyncDictionaryOperation operation, TKey key, TValue value)
|
||||
{
|
||||
this.Operation = operation;
|
||||
this.Key = key;
|
||||
this.Value = value;
|
||||
}
|
||||
}
|
||||
#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 readonly IDictionary<TKey, TValue> Collection;
|
||||
/// <summary>
|
||||
/// Copy of objects on client portion when acting as a host.
|
||||
/// </summary>
|
||||
public readonly IDictionary<TKey, TValue> ClientHostCollection = new Dictionary<TKey, TValue>();
|
||||
/// <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 IDictionary<TKey, TValue> _initialValues = new Dictionary<TKey, TValue>();
|
||||
/// <summary>
|
||||
/// Changed data which will be sent next tick.
|
||||
/// </summary>
|
||||
private readonly List<ChangeData> _changed = new List<ChangeData>();
|
||||
/// <summary>
|
||||
/// Server OnChange events waiting for start callbacks.
|
||||
/// </summary>
|
||||
private readonly List<CachedOnChange> _serverOnChanges = new List<CachedOnChange>();
|
||||
/// <summary>
|
||||
/// Client OnChange events waiting for start callbacks.
|
||||
/// </summary>
|
||||
private readonly List<CachedOnChange> _clientOnChanges = new List<CachedOnChange>();
|
||||
/// <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
|
||||
|
||||
[APIExclude]
|
||||
public SyncIDictionary(IDictionary<TKey, TValue> objects)
|
||||
{
|
||||
this.Collection = objects;
|
||||
//Add to clienthostcollection.
|
||||
foreach (KeyValuePair<TKey, TValue> item in objects)
|
||||
this.ClientHostCollection[item.Key] = item.Value;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
bool asClientAndHost = (!asServer && base.NetworkManager.IsServer);
|
||||
IDictionary<TKey, TValue> collection = (asClientAndHost) ? ClientHostCollection : Collection;
|
||||
return (collection as Dictionary<TKey, TValue>);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the SyncType has been registered, but not yet initialized over the network.
|
||||
/// </summary>
|
||||
protected override void Registered()
|
||||
{
|
||||
base.Registered();
|
||||
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]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void AddOperation(SyncDictionaryOperation operation, TKey key, TValue value)
|
||||
{
|
||||
if (!base.IsRegistered)
|
||||
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 = (!base.IsNetworkInitialized || base.NetworkBehaviour.IsServer);
|
||||
|
||||
if (asServerInvoke)
|
||||
{
|
||||
_valuesChanged = true;
|
||||
if (base.Dirty())
|
||||
{
|
||||
ChangeData change = new ChangeData(operation, key, value);
|
||||
_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>
|
||||
public 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 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]
|
||||
public override void WriteDelta(PooledWriter writer, bool resetSyncTick = true)
|
||||
{
|
||||
base.WriteDelta(writer, resetSyncTick);
|
||||
|
||||
//If sending all then clear changed and write full.
|
||||
if (_sendAll)
|
||||
{
|
||||
_sendAll = false;
|
||||
_changed.Clear();
|
||||
WriteFull(writer);
|
||||
}
|
||||
else
|
||||
{
|
||||
//False for not full write.
|
||||
writer.WriteBoolean(false);
|
||||
writer.WriteInt32(_changed.Count);
|
||||
|
||||
for (int i = 0; i < _changed.Count; i++)
|
||||
{
|
||||
ChangeData change = _changed[i];
|
||||
writer.WriteByte((byte)change.Operation);
|
||||
|
||||
//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]
|
||||
public override void WriteFull(PooledWriter writer)
|
||||
{
|
||||
if (!_valuesChanged)
|
||||
return;
|
||||
|
||||
base.WriteHeader(writer, false);
|
||||
//True for full write.
|
||||
writer.WriteBoolean(true);
|
||||
writer.WriteInt32(Collection.Count);
|
||||
foreach (KeyValuePair<TKey, TValue> item in Collection)
|
||||
{
|
||||
writer.WriteByte((byte)SyncDictionaryOperation.Add);
|
||||
writer.Write(item.Key);
|
||||
writer.Write(item.Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Reads and sets the current values for server or client.
|
||||
/// </summary>
|
||||
[APIExclude]
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override void Read(PooledReader reader, bool asServer)
|
||||
{
|
||||
/* When !asServer don't make changes if server is running.
|
||||
* This is because changes would have already been made on
|
||||
* the server side and doing so again would result in duplicates
|
||||
* and potentially overwrite data not yet sent. */
|
||||
bool asClientAndHost = (!asServer && base.NetworkBehaviour.IsServer);
|
||||
IDictionary<TKey, TValue> collection = (asClientAndHost) ? ClientHostCollection : Collection;
|
||||
|
||||
//Clear collection since it's a full write.
|
||||
bool fullWrite = reader.ReadBoolean();
|
||||
if (fullWrite)
|
||||
collection.Clear();
|
||||
|
||||
int changes = reader.ReadInt32();
|
||||
for (int i = 0; i < changes; i++)
|
||||
{
|
||||
SyncDictionaryOperation operation = (SyncDictionaryOperation)reader.ReadByte();
|
||||
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)
|
||||
{
|
||||
key = reader.Read<TKey>();
|
||||
value = reader.Read<TValue>();
|
||||
collection[key] = value;
|
||||
}
|
||||
//Clear.
|
||||
else if (operation == SyncDictionaryOperation.Clear)
|
||||
{
|
||||
collection.Clear();
|
||||
}
|
||||
//Remove.
|
||||
else if (operation == SyncDictionaryOperation.Remove)
|
||||
{
|
||||
key = reader.Read<TKey>();
|
||||
collection.Remove(key);
|
||||
}
|
||||
|
||||
InvokeOnChange(operation, key, value, false);
|
||||
}
|
||||
|
||||
//If changes were made invoke complete after all have been read.
|
||||
if (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 (base.NetworkBehaviour.OnStartServerCalled)
|
||||
OnChange?.Invoke(operation, key, value, asServer);
|
||||
else
|
||||
_serverOnChanges.Add(new CachedOnChange(operation, key, value));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (base.NetworkBehaviour.OnStartClientCalled)
|
||||
OnChange?.Invoke(operation, key, value, asServer);
|
||||
else
|
||||
_clientOnChanges.Add(new CachedOnChange(operation, key, value));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Resets to initialized values.
|
||||
/// </summary>
|
||||
[APIExclude]
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
_sendAll = false;
|
||||
_changed.Clear();
|
||||
Collection.Clear();
|
||||
ClientHostCollection.Clear();
|
||||
_valuesChanged = false;
|
||||
|
||||
foreach (KeyValuePair<TKey, TValue> item in _initialValues)
|
||||
{
|
||||
Collection[item.Key] = item.Value;
|
||||
ClientHostCollection[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 (!base.CanNetworkSetValues(true))
|
||||
return;
|
||||
|
||||
Collection.Add(key, value);
|
||||
if (asServer)
|
||||
AddOperation(SyncDictionaryOperation.Add, key, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all values.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
Clear(true);
|
||||
}
|
||||
private void Clear(bool asServer)
|
||||
{
|
||||
if (!base.CanNetworkSetValues(true))
|
||||
return;
|
||||
|
||||
Collection.Clear();
|
||||
if (asServer)
|
||||
AddOperation(SyncDictionaryOperation.Clear, default, default);
|
||||
}
|
||||
|
||||
|
||||
/// <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>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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)
|
||||
{
|
||||
base.NetworkManager.LogError($"Index is out of range.");
|
||||
return;
|
||||
}
|
||||
|
||||
int remaining = array.Length - offset;
|
||||
if (remaining < Count)
|
||||
{
|
||||
base.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 (!base.CanNetworkSetValues(true))
|
||||
return false;
|
||||
|
||||
if (Collection.Remove(key))
|
||||
{
|
||||
AddOperation(SyncDictionaryOperation.Remove, key, default);
|
||||
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>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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 (!base.CanNetworkSetValues(true))
|
||||
return;
|
||||
|
||||
Collection[key] = value;
|
||||
AddOperation(SyncDictionaryOperation.Set, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dirties the entire collection forcing a full send.
|
||||
/// </summary>
|
||||
public void DirtyAll()
|
||||
{
|
||||
if (!base.IsRegistered)
|
||||
return;
|
||||
if (!base.CanNetworkSetValues(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 (!base.IsRegistered)
|
||||
return;
|
||||
if (!base.CanNetworkSetValues(true))
|
||||
return;
|
||||
|
||||
if (Collection.TryGetValueIL2CPP(key, out TValue value))
|
||||
AddOperation(SyncDictionaryOperation.Set, key, value);
|
||||
}
|
||||
|
||||
/// <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 (!base.IsRegistered)
|
||||
return false;
|
||||
if (!base.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);
|
||||
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();
|
||||
|
||||
}
|
||||
|
||||
[APIExclude]
|
||||
public class SyncDictionary<TKey, TValue> : SyncIDictionary<TKey, TValue>
|
||||
{
|
||||
[APIExclude]
|
||||
public SyncDictionary() : base(new Dictionary<TKey, TValue>()) { }
|
||||
[APIExclude]
|
||||
public SyncDictionary(IEqualityComparer<TKey> eq) : base(new Dictionary<TKey, TValue>(eq)) { }
|
||||
[APIExclude]
|
||||
public new Dictionary<TKey, TValue>.ValueCollection Values => ((Dictionary<TKey, TValue>)Collection).Values;
|
||||
[APIExclude]
|
||||
public new Dictionary<TKey, TValue>.KeyCollection Keys => ((Dictionary<TKey, TValue>)Collection).Keys;
|
||||
[APIExclude]
|
||||
public new Dictionary<TKey, TValue>.Enumerator GetEnumerator() => ((Dictionary<TKey, TValue>)Collection).GetEnumerator();
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54751f912587a854cb61ff80a82087bf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,31 @@
|
||||
|
||||
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,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5d6ed9db47a8224fa9ed4d2ff54586f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,31 @@
|
||||
|
||||
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>
|
||||
/// 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,
|
||||
/// <summary>
|
||||
/// An item has been updated within the collection. This is generally used when modifying data within a container.
|
||||
/// </summary>
|
||||
Update,
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 914089f5707003340a68fd6cd718e4c4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
616
Assets/FishNet/Runtime/Object/Synchronizing/SyncHashset.cs
Normal file
616
Assets/FishNet/Runtime/Object/Synchronizing/SyncHashset.cs
Normal file
@ -0,0 +1,616 @@
|
||||
using FishNet.Documenting;
|
||||
using FishNet.Managing.Logging;
|
||||
using FishNet.Object.Synchronizing.Internal;
|
||||
using FishNet.Serializing;
|
||||
using FishNet.Serializing.Helping;
|
||||
using FishNet.Utility.Performance;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FishNet.Object.Synchronizing
|
||||
{
|
||||
|
||||
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;
|
||||
|
||||
public ChangeData(SyncHashSetOperation operation, T item)
|
||||
{
|
||||
Operation = operation;
|
||||
|
||||
Item = item;
|
||||
}
|
||||
}
|
||||
#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 readonly ISet<T> Collection;
|
||||
/// <summary>
|
||||
/// Copy of objects on client portion when acting as a host.
|
||||
/// </summary>
|
||||
public readonly ISet<T> ClientHostCollection = new HashSet<T>();
|
||||
/// <summary>
|
||||
/// Number of objects in the collection.
|
||||
/// </summary>
|
||||
public int Count => Collection.Count;
|
||||
#endregion
|
||||
|
||||
#region Private.
|
||||
/// <summary>
|
||||
/// ListCache for comparing.
|
||||
/// </summary>
|
||||
private ListCache<T> _listCache;
|
||||
/// <summary>
|
||||
/// Values upon initialization.
|
||||
/// </summary>
|
||||
private ISet<T> _initialValues = new HashSet<T>();
|
||||
/// <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 readonly List<ChangeData> _changed = new List<ChangeData>();
|
||||
/// <summary>
|
||||
/// Server OnChange events waiting for start callbacks.
|
||||
/// </summary>
|
||||
private readonly List<CachedOnChange> _serverOnChanges = new List<CachedOnChange>();
|
||||
/// <summary>
|
||||
/// Client OnChange events waiting for start callbacks.
|
||||
/// </summary>
|
||||
private readonly List<CachedOnChange> _clientOnChanges = new List<CachedOnChange>();
|
||||
/// <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
|
||||
|
||||
[APIExclude]
|
||||
public SyncHashSet() : this(new HashSet<T>(), EqualityComparer<T>.Default) { }
|
||||
[APIExclude]
|
||||
public SyncHashSet(IEqualityComparer<T> comparer) : this(new HashSet<T>(), (comparer == null) ? EqualityComparer<T>.Default : comparer) { }
|
||||
[APIExclude]
|
||||
public SyncHashSet(ISet<T> collection, IEqualityComparer<T> comparer = null)
|
||||
{
|
||||
this._comparer = (comparer == null) ? EqualityComparer<T>.Default : comparer;
|
||||
this.Collection = collection;
|
||||
//Add each in collection to clienthostcollection.
|
||||
foreach (T item in collection)
|
||||
ClientHostCollection.Add(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the SyncType has been registered, but not yet initialized over the network.
|
||||
/// </summary>
|
||||
protected override void Registered()
|
||||
{
|
||||
base.Registered();
|
||||
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)
|
||||
{
|
||||
bool asClientAndHost = (!asServer && base.NetworkManager.IsServer);
|
||||
ISet<T> collection = (asClientAndHost) ? ClientHostCollection : Collection;
|
||||
return (collection as HashSet<T>);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an operation and invokes locally.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void AddOperation(SyncHashSetOperation operation, T item)
|
||||
{
|
||||
if (!base.IsRegistered)
|
||||
return;
|
||||
|
||||
bool asServerInvoke = (!base.IsNetworkInitialized || base.NetworkBehaviour.IsServer);
|
||||
|
||||
if (asServerInvoke)
|
||||
{
|
||||
_valuesChanged = true;
|
||||
if (base.Dirty())
|
||||
{
|
||||
ChangeData change = new ChangeData(operation, item);
|
||||
_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>
|
||||
public 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 all changed values.
|
||||
/// </summary>
|
||||
/// <param name="writer"></param>
|
||||
///<param name="resetSyncTick">True to set the next time data may sync.</param>
|
||||
public 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];
|
||||
writer.WriteByte((byte)change.Operation);
|
||||
|
||||
//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.Update)
|
||||
{
|
||||
writer.Write(change.Item);
|
||||
}
|
||||
}
|
||||
|
||||
_changed.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes all values if not initial values.
|
||||
/// </summary>
|
||||
/// <param name="writer"></param>
|
||||
public 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);
|
||||
foreach (T item in Collection)
|
||||
{
|
||||
writer.WriteByte((byte)SyncHashSetOperation.Add);
|
||||
writer.Write(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads and sets the current values for server or client.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[APIExclude]
|
||||
public override void Read(PooledReader reader, bool asServer)
|
||||
{
|
||||
/* When !asServer don't make changes if server is running.
|
||||
* This is because changes would have already been made on
|
||||
* the server side and doing so again would result in duplicates
|
||||
* and potentially overwrite data not yet sent. */
|
||||
bool asClientAndHost = (!asServer && base.NetworkManager.IsServer);
|
||||
ISet<T> collection = (asClientAndHost) ? ClientHostCollection : Collection;
|
||||
|
||||
//Clear collection since it's a full write.
|
||||
bool fullWrite = reader.ReadBoolean();
|
||||
if (fullWrite)
|
||||
collection.Clear();
|
||||
|
||||
int changes = reader.ReadInt32();
|
||||
for (int i = 0; i < changes; i++)
|
||||
{
|
||||
SyncHashSetOperation operation = (SyncHashSetOperation)reader.ReadByte();
|
||||
T next = default;
|
||||
|
||||
//Add.
|
||||
if (operation == SyncHashSetOperation.Add)
|
||||
{
|
||||
next = reader.Read<T>();
|
||||
collection.Add(next);
|
||||
}
|
||||
//Clear.
|
||||
else if (operation == SyncHashSetOperation.Clear)
|
||||
{
|
||||
collection.Clear();
|
||||
}
|
||||
//Remove.
|
||||
else if (operation == SyncHashSetOperation.Remove)
|
||||
{
|
||||
next = reader.Read<T>();
|
||||
collection.Remove(next);
|
||||
}
|
||||
//Updated.
|
||||
else if (operation == SyncHashSetOperation.Update)
|
||||
{
|
||||
next = reader.Read<T>();
|
||||
collection.Remove(next);
|
||||
collection.Add(next);
|
||||
}
|
||||
|
||||
InvokeOnChange(operation, next, false);
|
||||
}
|
||||
|
||||
//If changes were made invoke complete after all have been read.
|
||||
if (changes > 0)
|
||||
InvokeOnChange(SyncHashSetOperation.Complete, default, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes OnChanged callback.
|
||||
/// </summary>
|
||||
private void InvokeOnChange(SyncHashSetOperation operation, T item, bool asServer)
|
||||
{
|
||||
if (asServer)
|
||||
{
|
||||
if (base.NetworkBehaviour.OnStartServerCalled)
|
||||
OnChange?.Invoke(operation, item, asServer);
|
||||
else
|
||||
_serverOnChanges.Add(new CachedOnChange(operation, item));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (base.NetworkBehaviour.OnStartClientCalled)
|
||||
OnChange?.Invoke(operation, item, asServer);
|
||||
else
|
||||
_clientOnChanges.Add(new CachedOnChange(operation, item));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets to initialized values.
|
||||
/// </summary>
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
_sendAll = false;
|
||||
_changed.Clear();
|
||||
Collection.Clear();
|
||||
ClientHostCollection.Clear();
|
||||
|
||||
foreach (T item in _initialValues)
|
||||
{
|
||||
Collection.Add(item);
|
||||
ClientHostCollection.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 (!base.CanNetworkSetValues(true))
|
||||
return false;
|
||||
|
||||
bool result = Collection.Add(item);
|
||||
//Only process if remove was successful.
|
||||
if (result && asServer)
|
||||
{
|
||||
if (base.NetworkManager == null)
|
||||
ClientHostCollection.Add(item);
|
||||
AddOperation(SyncHashSetOperation.Add, item);
|
||||
}
|
||||
|
||||
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 (!base.CanNetworkSetValues(true))
|
||||
return;
|
||||
|
||||
Collection.Clear();
|
||||
if (asServer)
|
||||
{
|
||||
if (base.NetworkManager == null)
|
||||
ClientHostCollection.Clear();
|
||||
AddOperation(SyncHashSetOperation.Clear, default);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 (!base.CanNetworkSetValues(true))
|
||||
return false;
|
||||
|
||||
bool result = Collection.Remove(item);
|
||||
//Only process if remove was successful.
|
||||
if (result && asServer)
|
||||
{
|
||||
if (base.NetworkManager == null)
|
||||
ClientHostCollection.Remove(item);
|
||||
AddOperation(SyncHashSetOperation.Remove, item);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dirties the entire collection forcing a full send.
|
||||
/// </summary>
|
||||
public void DirtyAll()
|
||||
{
|
||||
if (!base.IsRegistered)
|
||||
return;
|
||||
if (!base.CanNetworkSetValues(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 (!base.IsRegistered)
|
||||
return;
|
||||
if (!base.CanNetworkSetValues(true))
|
||||
return;
|
||||
|
||||
foreach (T item in Collection)
|
||||
{
|
||||
if (item.Equals(obj))
|
||||
{
|
||||
AddOperation(SyncHashSetOperation.Update, obj);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//Not found.
|
||||
base.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)
|
||||
{
|
||||
Intersect(Collection);
|
||||
if (base.NetworkManager == null)
|
||||
Intersect(ClientHostCollection);
|
||||
|
||||
void Intersect(ISet<T> collection)
|
||||
{
|
||||
if (_listCache == null)
|
||||
_listCache = new ListCache<T>();
|
||||
else
|
||||
_listCache.Reset();
|
||||
|
||||
_listCache.AddValues(collection);
|
||||
|
||||
int count = _listCache.Written;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
T entry = _listCache.Collection[i];
|
||||
if (!other.Contains(entry))
|
||||
Remove(entry);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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);
|
||||
if (base.NetworkManager == null)
|
||||
ClientHostCollection.CopyTo(array, index);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 624322b9d999d4b43a560134460955c6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
748
Assets/FishNet/Runtime/Object/Synchronizing/SyncList.cs
Normal file
748
Assets/FishNet/Runtime/Object/Synchronizing/SyncList.cs
Normal file
@ -0,0 +1,748 @@
|
||||
using FishNet.Documenting;
|
||||
using FishNet.Managing.Logging;
|
||||
using FishNet.Object.Synchronizing.Internal;
|
||||
using FishNet.Serializing;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FishNet.Object.Synchronizing
|
||||
{
|
||||
|
||||
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 Index;
|
||||
internal readonly T Item;
|
||||
|
||||
public ChangeData(SyncListOperation operation, int index, T item)
|
||||
{
|
||||
Operation = operation;
|
||||
Index = index;
|
||||
Item = item;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom enumerator to prevent garbage collection.
|
||||
/// </summary>
|
||||
[APIExclude]
|
||||
public struct Enumerator : IEnumerator<T>
|
||||
{
|
||||
public T Current { get; private set; }
|
||||
private readonly SyncList<T> _list;
|
||||
private int _index;
|
||||
|
||||
public Enumerator(SyncList<T> list)
|
||||
{
|
||||
this._list = list;
|
||||
_index = -1;
|
||||
Current = default;
|
||||
}
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
_index++;
|
||||
if (_index >= _list.Count)
|
||||
return false;
|
||||
Current = _list[_index];
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Reset() => _index = -1;
|
||||
object IEnumerator.Current => Current;
|
||||
public void Dispose() { }
|
||||
}
|
||||
#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 readonly IList<T> Collection;
|
||||
/// <summary>
|
||||
/// Copy of objects on client portion when acting as a host.
|
||||
/// </summary>
|
||||
public readonly IList<T> ClientHostCollection = new List<T>();
|
||||
/// <summary>
|
||||
/// Number of objects in the collection.
|
||||
/// </summary>
|
||||
public int Count => Collection.Count;
|
||||
#endregion
|
||||
|
||||
#region Private.
|
||||
/// <summary>
|
||||
/// Values upon initialization.
|
||||
/// </summary>
|
||||
private IList<T> _initialValues = new List<T>();
|
||||
/// <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 readonly List<ChangeData> _changed = new List<ChangeData>();
|
||||
/// <summary>
|
||||
/// Server OnChange events waiting for start callbacks.
|
||||
/// </summary>
|
||||
private readonly List<CachedOnChange> _serverOnChanges = new List<CachedOnChange>();
|
||||
/// <summary>
|
||||
/// Client OnChange events waiting for start callbacks.
|
||||
/// </summary>
|
||||
private readonly List<CachedOnChange> _clientOnChanges = new List<CachedOnChange>();
|
||||
/// <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
|
||||
|
||||
[APIExclude]
|
||||
public SyncList() : this(new List<T>(), EqualityComparer<T>.Default) { }
|
||||
[APIExclude]
|
||||
public SyncList(IEqualityComparer<T> comparer) : this(new List<T>(), (comparer == null) ? EqualityComparer<T>.Default : comparer) { }
|
||||
[APIExclude]
|
||||
public SyncList(IList<T> collection, IEqualityComparer<T> comparer = null)
|
||||
{
|
||||
this._comparer = (comparer == null) ? EqualityComparer<T>.Default : comparer;
|
||||
this.Collection = collection;
|
||||
//Add each in collection to clienthostcollection.
|
||||
foreach (T item in collection)
|
||||
this.ClientHostCollection.Add(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the SyncType has been registered, but not yet initialized over the network.
|
||||
/// </summary>
|
||||
protected override void Registered()
|
||||
{
|
||||
base.Registered();
|
||||
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)
|
||||
{
|
||||
bool asClientAndHost = (!asServer && base.NetworkManager.IsServer);
|
||||
IList<T> collection = (asClientAndHost) ? ClientHostCollection : Collection;
|
||||
return (collection as List<T>);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an operation and invokes locally.
|
||||
/// </summary>
|
||||
/// <param name="operation"></param>
|
||||
/// <param name="index"></param>
|
||||
/// <param name="prev"></param>
|
||||
/// <param name="next"></param>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void AddOperation(SyncListOperation operation, int index, T prev, T next)
|
||||
{
|
||||
if (!base.IsRegistered)
|
||||
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 = (!base.IsNetworkInitialized || base.NetworkBehaviour.IsServer);
|
||||
|
||||
/* 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 ChangeData(operation, index, next);
|
||||
_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>
|
||||
public 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 all changed values.
|
||||
/// </summary>
|
||||
/// <param name="writer"></param>
|
||||
///<param name="resetSyncTick">True to set the next time data may sync.</param>
|
||||
public 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];
|
||||
writer.WriteByte((byte)change.Operation);
|
||||
|
||||
//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)
|
||||
{
|
||||
writer.WriteInt32(change.Index);
|
||||
}
|
||||
else if (change.Operation == SyncListOperation.Insert || change.Operation == SyncListOperation.Set)
|
||||
{
|
||||
writer.WriteInt32(change.Index);
|
||||
writer.Write(change.Item);
|
||||
}
|
||||
}
|
||||
|
||||
_changed.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes all values if not initial values.
|
||||
/// </summary>
|
||||
/// <param name="writer"></param>
|
||||
public override void WriteFull(PooledWriter writer)
|
||||
{
|
||||
if (!_valuesChanged)
|
||||
return;
|
||||
|
||||
base.WriteHeader(writer, false);
|
||||
//True for full write.
|
||||
writer.WriteBoolean(true);
|
||||
writer.WriteInt32(Collection.Count);
|
||||
for (int i = 0; i < Collection.Count; i++)
|
||||
{
|
||||
writer.WriteByte((byte)SyncListOperation.Add);
|
||||
writer.Write(Collection[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads and sets the current values for server or client.
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
[APIExclude]
|
||||
public override void Read(PooledReader reader, bool asServer)
|
||||
{
|
||||
/* When !asServer don't make changes if server is running.
|
||||
* This is because changes would have already been made on
|
||||
* the server side and doing so again would result in duplicates
|
||||
* and potentially overwrite data not yet sent. */
|
||||
bool asClientAndHost = (!asServer && base.NetworkManager.IsServer);
|
||||
IList<T> collection = (asClientAndHost) ? ClientHostCollection : Collection;
|
||||
|
||||
//Clear collection since it's a full write.
|
||||
bool fullWrite = reader.ReadBoolean();
|
||||
if (fullWrite)
|
||||
collection.Clear();
|
||||
|
||||
int changes = reader.ReadInt32();
|
||||
|
||||
for (int i = 0; i < changes; i++)
|
||||
{
|
||||
SyncListOperation operation = (SyncListOperation)reader.ReadByte();
|
||||
int index = -1;
|
||||
T prev = default;
|
||||
T next = default;
|
||||
|
||||
//Add.
|
||||
if (operation == SyncListOperation.Add)
|
||||
{
|
||||
next = reader.Read<T>();
|
||||
index = collection.Count;
|
||||
collection.Add(next);
|
||||
}
|
||||
//Clear.
|
||||
else if (operation == SyncListOperation.Clear)
|
||||
{
|
||||
collection.Clear();
|
||||
}
|
||||
//Insert.
|
||||
else if (operation == SyncListOperation.Insert)
|
||||
{
|
||||
index = reader.ReadInt32();
|
||||
next = reader.Read<T>();
|
||||
collection.Insert(index, next);
|
||||
}
|
||||
//RemoveAt.
|
||||
else if (operation == SyncListOperation.RemoveAt)
|
||||
{
|
||||
index = reader.ReadInt32();
|
||||
prev = collection[index];
|
||||
collection.RemoveAt(index);
|
||||
}
|
||||
//Set
|
||||
else if (operation == SyncListOperation.Set)
|
||||
{
|
||||
index = reader.ReadInt32();
|
||||
next = reader.Read<T>();
|
||||
prev = collection[index];
|
||||
collection[index] = next;
|
||||
}
|
||||
|
||||
InvokeOnChange(operation, index, prev, next, false);
|
||||
}
|
||||
|
||||
//If changes were made invoke complete after all have been read.
|
||||
if (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 (base.NetworkBehaviour.OnStartServerCalled)
|
||||
OnChange?.Invoke(operation, index, prev, next, asServer);
|
||||
else
|
||||
_serverOnChanges.Add(new CachedOnChange(operation, index, prev, next));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (base.NetworkBehaviour.OnStartClientCalled)
|
||||
OnChange?.Invoke(operation, index, prev, next, asServer);
|
||||
else
|
||||
_clientOnChanges.Add(new CachedOnChange(operation, index, prev, next));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets to initialized values.
|
||||
/// </summary>
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
_sendAll = false;
|
||||
_changed.Clear();
|
||||
ClientHostCollection.Clear();
|
||||
Collection.Clear();
|
||||
|
||||
foreach (T item in _initialValues)
|
||||
{
|
||||
Collection.Add(item);
|
||||
ClientHostCollection.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 (!base.CanNetworkSetValues(true))
|
||||
return;
|
||||
|
||||
Collection.Add(item);
|
||||
if (asServer)
|
||||
{
|
||||
if (base.NetworkManager == null)
|
||||
ClientHostCollection.Add(item);
|
||||
AddOperation(SyncListOperation.Add, Collection.Count - 1, default, item);
|
||||
}
|
||||
}
|
||||
/// <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 (!base.CanNetworkSetValues(true))
|
||||
return;
|
||||
|
||||
Collection.Clear();
|
||||
if (asServer)
|
||||
{
|
||||
if (base.NetworkManager == null)
|
||||
ClientHostCollection.Clear();
|
||||
AddOperation(SyncListOperation.Clear, -1, default, default);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 List<T>();
|
||||
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 (!base.CanNetworkSetValues(true))
|
||||
return;
|
||||
|
||||
Collection.Insert(index, item);
|
||||
if (asServer)
|
||||
{
|
||||
if (base.NetworkManager == null)
|
||||
ClientHostCollection.Insert(index, item);
|
||||
AddOperation(SyncListOperation.Insert, index, default, item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 (!base.CanNetworkSetValues(true))
|
||||
return;
|
||||
|
||||
T oldItem = Collection[index];
|
||||
Collection.RemoveAt(index);
|
||||
if (asServer)
|
||||
{
|
||||
if (base.NetworkManager == null)
|
||||
ClientHostCollection.RemoveAt(index);
|
||||
AddOperation(SyncListOperation.RemoveAt, index, oldItem, default);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all values within the collection.
|
||||
/// </summary>
|
||||
/// <param name="match"></param>
|
||||
/// <returns></returns>
|
||||
public int RemoveAll(Predicate<T> match)
|
||||
{
|
||||
List<T> toRemove = new List<T>();
|
||||
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 (!base.IsRegistered)
|
||||
return;
|
||||
|
||||
if (base.NetworkManager != null && !base.NetworkBehaviour.IsServer)
|
||||
{
|
||||
base.NetworkManager.LogWarning($"Cannot complete operation as server when server is not active.");
|
||||
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
|
||||
base.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 (!base.CanNetworkSetValues(true))
|
||||
return;
|
||||
|
||||
bool asServer = true;
|
||||
T value = Collection[index];
|
||||
if (asServer)
|
||||
AddOperation(SyncListOperation.Set, index, value, value);
|
||||
}
|
||||
/// <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);
|
||||
}
|
||||
private void Set(int index, T value, bool asServer, bool force)
|
||||
{
|
||||
if (!base.CanNetworkSetValues(true))
|
||||
return;
|
||||
|
||||
bool sameValue = (!force && !_comparer.Equals(Collection[index], value));
|
||||
if (!sameValue)
|
||||
{
|
||||
T prev = Collection[index];
|
||||
Collection[index] = value;
|
||||
if (asServer)
|
||||
{
|
||||
if (base.NetworkManager == null)
|
||||
ClientHostCollection[index] = value;
|
||||
AddOperation(SyncListOperation.Set, index, prev, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns Enumerator for collection.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Enumerator GetEnumerator() => new Enumerator(this);
|
||||
[APIExclude]
|
||||
IEnumerator<T> IEnumerable<T>.GetEnumerator() => new Enumerator(this);
|
||||
[APIExclude]
|
||||
IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this);
|
||||
|
||||
}
|
||||
}
|
11
Assets/FishNet/Runtime/Object/Synchronizing/SyncList.cs.meta
Normal file
11
Assets/FishNet/Runtime/Object/Synchronizing/SyncList.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f3a4c0d0a34e5142be66143d732c079
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,35 @@
|
||||
|
||||
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,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4fa53fc807605df4997f0b63a6570bcf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
290
Assets/FishNet/Runtime/Object/Synchronizing/SyncVar.cs
Normal file
290
Assets/FishNet/Runtime/Object/Synchronizing/SyncVar.cs
Normal file
@ -0,0 +1,290 @@
|
||||
using FishNet.Documenting;
|
||||
using FishNet.Object.Helping;
|
||||
using FishNet.Object.Synchronizing;
|
||||
using FishNet.Object.Synchronizing.Internal;
|
||||
using FishNet.Serializing;
|
||||
using FishNet.Serializing.Helping;
|
||||
using FishNet.Transporting;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace FishNet.Object.Synchronizing
|
||||
{
|
||||
[APIExclude]
|
||||
[StructLayout(LayoutKind.Auto, CharSet = CharSet.Auto)]
|
||||
public class SyncVar<T> : SyncBase
|
||||
{
|
||||
#region Types.
|
||||
/// <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>
|
||||
/// Called when the SyncDictionary changes.
|
||||
/// </summary>
|
||||
public event Action<T, T, bool> OnChange;
|
||||
#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>
|
||||
/// Previous value on the client.
|
||||
/// </summary>
|
||||
private T _previousClientValue;
|
||||
/// <summary>
|
||||
/// Current value on the server, or client.
|
||||
/// </summary>
|
||||
private T _value;
|
||||
#endregion
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public SyncVar(NetworkBehaviour nb, uint syncIndex, WritePermission writePermission, ReadPermission readPermission, float sendRate, Channel channel, T value)
|
||||
{
|
||||
SetInitialValues(value);
|
||||
base.InitializeInstance(nb, syncIndex, writePermission, readPermission, sendRate, channel, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the SyncType has been registered, but not yet initialized over the network.
|
||||
/// </summary>
|
||||
protected override void Registered()
|
||||
{
|
||||
base.Registered();
|
||||
_initialValue = _value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets initial values to next.
|
||||
/// </summary>
|
||||
/// <param name="next"></param>
|
||||
private void SetInitialValues(T next)
|
||||
{
|
||||
_initialValue = next;
|
||||
UpdateValues(next);
|
||||
}
|
||||
/// <summary>
|
||||
/// Sets current and previous values.
|
||||
/// </summary>
|
||||
/// <param name="next"></param>
|
||||
private void UpdateValues(T next)
|
||||
{
|
||||
_previousClientValue = next;
|
||||
_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>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void SetValue(T nextValue, bool calledByUser)
|
||||
{
|
||||
/* If not registered then that means Awake
|
||||
* has not completed on the owning class. This would be true
|
||||
* when setting values within awake on the owning class. Registered
|
||||
* is called at the end of awake, so it would be unset until awake completed.
|
||||
*
|
||||
* Registered however will be true when setting from another script,
|
||||
* even if the owning class of this was just spawned. This is because
|
||||
* the unity cycle will fire awake on the object soon as it's spawned,
|
||||
* completing awake, and the user would set the value after. */
|
||||
if (!base.IsRegistered)
|
||||
return;
|
||||
|
||||
/* 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 = base.IsNetworkInitialized;
|
||||
|
||||
//Object is deinitializing.
|
||||
if (isNetworkInitialized && CodegenHelper.NetworkObject_Deinitializing(this.NetworkBehaviour))
|
||||
return;
|
||||
|
||||
//If being set by user code.
|
||||
if (calledByUser)
|
||||
{
|
||||
if (!base.CanNetworkSetValues(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 = (!isNetworkInitialized || base.NetworkBehaviour.IsServer);
|
||||
|
||||
/* 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, calledByUser);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Comparers.EqualityCompare<T>(_value, nextValue))
|
||||
return;
|
||||
|
||||
T prev = _value;
|
||||
_value = nextValue;
|
||||
InvokeOnChange(prev, _value, asServerInvoke);
|
||||
}
|
||||
|
||||
TryDirty(asServerInvoke);
|
||||
}
|
||||
//Not called by user.
|
||||
else
|
||||
{
|
||||
/* 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 = _previousClientValue;
|
||||
if (Comparers.EqualityCompare<T>(prev, nextValue))
|
||||
return;
|
||||
|
||||
/* If also server do not update value.
|
||||
* Server side has say of the current value. */
|
||||
if (!base.NetworkManager.IsServer)
|
||||
UpdateValues(nextValue);
|
||||
else
|
||||
_previousClientValue = nextValue;
|
||||
|
||||
InvokeOnChange(prev, nextValue, calledByUser);
|
||||
}
|
||||
|
||||
|
||||
/* 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)
|
||||
base.Dirty();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes OnChanged callback.
|
||||
/// </summary>
|
||||
private void InvokeOnChange(T prev, T next, bool asServer)
|
||||
{
|
||||
if (asServer)
|
||||
{
|
||||
if (base.NetworkBehaviour.OnStartServerCalled)
|
||||
OnChange?.Invoke(prev, next, asServer);
|
||||
else
|
||||
_serverOnChange = new CachedOnChange(prev, next);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (base.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>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public 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>
|
||||
public override void WriteDelta(PooledWriter writer, bool resetSyncTick = true)
|
||||
{
|
||||
base.WriteDelta(writer, resetSyncTick);
|
||||
writer.Write<T>(_value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes current value if not initialized value.
|
||||
/// </summary>m>
|
||||
public override void WriteFull(PooledWriter obj0)
|
||||
{
|
||||
if (Comparers.EqualityCompare<T>(_initialValue, _value))
|
||||
return;
|
||||
/* SyncVars only hold latest value, so just
|
||||
* write current delta. */
|
||||
WriteDelta(obj0, false);
|
||||
}
|
||||
|
||||
//Read isn't used by SyncVar<T>, it's done within the NB.
|
||||
//public override void Read(PooledReader reader) { }
|
||||
|
||||
/// <summary>
|
||||
/// Gets current value.
|
||||
/// </summary>
|
||||
/// <param name="calledByUser"></param>
|
||||
/// <returns></returns>
|
||||
public T GetValue(bool calledByUser) => (calledByUser) ? _value : _previousClientValue;
|
||||
|
||||
/// <summary>
|
||||
/// Resets to initialized values.
|
||||
/// </summary>
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
_value = _initialValue;
|
||||
_previousClientValue = _initialValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
11
Assets/FishNet/Runtime/Object/Synchronizing/SyncVar.cs.meta
Normal file
11
Assets/FishNet/Runtime/Object/Synchronizing/SyncVar.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f319403eec508734a93d723617ab1136
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,10 @@
|
||||
namespace FishNet.Object.Synchronizing
|
||||
{
|
||||
/// <summary>
|
||||
/// Which clients or server may write updates.
|
||||
/// </summary>
|
||||
public enum WritePermission
|
||||
{
|
||||
ServerOnly
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2696d0da2ff02e8499a8351a3021008f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
Reference in New Issue
Block a user