Commit f6ed3c6f authored by cann-alberto's avatar cann-alberto
Browse files

first project commit

parent 4487b14f
fileFormatVersion: 2
guid: d2cdbcbd1e377d6408a91acbec31ba16
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
fileFormatVersion: 2
guid: 4468e736f87964eaebb9d55fc3e132f7
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
// Snapshot interface so we can reuse it for all kinds of systems.
// for example, NetworkTransform, NetworkRigidbody, CharacterController etc.
// NOTE: we use '<T>' and 'where T : Snapshot' to avoid boxing.
// List<Snapshot> would cause allocations through boxing.
namespace Mirror
{
public interface Snapshot
{
// the remote timestamp (when it was sent by the remote)
double remoteTime { get; set; }
// the local timestamp (when it was received on our end)
// technically not needed for basic snapshot interpolation.
// only for dynamic buffer time adjustment.
double localTime { get; set; }
}
}
fileFormatVersion: 2
guid: 12afea28fdb94154868a0a3b7a9df55b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
// snapshot interpolation V2 by mischa
//
// Unity independent to be engine agnostic & easy to test.
// boxing: in C#, uses <T> does not box! passing the interface would box!
//
// credits:
// glenn fiedler: https://gafferongames.com/post/snapshot_interpolation/
// fholm: netcode streams
// fakebyte: standard deviation for dynamic adjustment
// ninjakicka: math & debugging
using System.Collections.Generic;
using System.Runtime.CompilerServices;
namespace Mirror
{
public static class SortedListExtensions
{
// removes the first 'amount' elements from the sorted list
public static void RemoveRange<T, U>(this SortedList<T, U> list, int amount)
{
// remove the first element 'amount' times.
// handles -1 and > count safely.
for (int i = 0; i < amount && i < list.Count; ++i)
list.RemoveAt(0);
}
}
public static class SnapshotInterpolation
{
// calculate timescale for catch-up / slow-down
// note that negative threshold should be <0.
// caller should verify (i.e. Unity OnValidate).
// improves branch prediction.
public static double Timescale(
double drift, // how far we are off from bufferTime
double catchupSpeed, // in % [0,1]
double slowdownSpeed, // in % [0,1]
double absoluteCatchupNegativeThreshold, // in seconds (careful, we may run out of snapshots)
double absoluteCatchupPositiveThreshold) // in seconds
{
// if the drift time is too large, it means we are behind more time.
// so we need to speed up the timescale.
// note the threshold should be sendInterval * catchupThreshold.
if (drift > absoluteCatchupPositiveThreshold)
{
// localTimeline += 0.001; // too simple, this would ping pong
return 1 + catchupSpeed; // n% faster
}
// if the drift time is too small, it means we are ahead of time.
// so we need to slow down the timescale.
// note the threshold should be sendInterval * catchupThreshold.
if (drift < absoluteCatchupNegativeThreshold)
{
// localTimeline -= 0.001; // too simple, this would ping pong
return 1 - slowdownSpeed; // n% slower
}
// keep constant timescale while within threshold.
// this way we have perfectly smooth speed most of the time.
return 1;
}
// calculate dynamic buffer time adjustment
public static double DynamicAdjustment(
double sendInterval,
double jitterStandardDeviation,
double dynamicAdjustmentTolerance)
{
// jitter is equal to delivery time standard variation.
// delivery time is made up of 'sendInterval+jitter'.
// .Average would be dampened by the constant sendInterval
// .StandardDeviation is the changes in 'jitter' that we want
// so add it to send interval again.
double intervalWithJitter = sendInterval + jitterStandardDeviation;
// how many multiples of sendInterval is that?
// we want to convert to bufferTimeMultiplier later.
double multiples = intervalWithJitter / sendInterval;
// add the tolerance
double safezone = multiples + dynamicAdjustmentTolerance;
// UnityEngine.Debug.Log($"sendInterval={sendInterval:F3} jitter std={jitterStandardDeviation:F3} => that is ~{multiples:F1} x sendInterval + {dynamicAdjustmentTolerance} => dynamic bufferTimeMultiplier={safezone}");
return safezone;
}
// helper function to insert a snapshot if it doesn't exist yet.
// extra function so we can use it for both cases:
// NetworkClient global timeline insertions & adjustments via Insert<T>.
// NetworkBehaviour local insertion without any time adjustments.
public static bool InsertIfNotExists<T>(
SortedList<double, T> buffer, // snapshot buffer
int bufferLimit, // don't grow infinitely
T snapshot) // the newly received snapshot
where T : Snapshot
{
// slow clients may not be able to process incoming snapshots fast enough.
// infinitely growing snapshots would make it even worse.
// for example, run NetworkRigidbodyBenchmark while deep profiling client.
// the client just grows and reallocates the buffer forever.
if (buffer.Count >= bufferLimit) return false;
// SortedList does not allow duplicates.
// we don't need to check ContainsKey (which is expensive).
// simply add and compare count before/after for the return value.
//if (buffer.ContainsKey(snapshot.remoteTime)) return false; // too expensive
// buffer.Add(snapshot.remoteTime, snapshot); // throws if key exists
int before = buffer.Count;
buffer[snapshot.remoteTime] = snapshot; // overwrites if key exists
return buffer.Count > before;
}
// clamp timeline for cases where it gets too far behind.
// for example, a client app may go into the background and get updated
// with 1hz for a while. by the time it's back it's at least 30 frames
// behind, possibly more if the transport also queues up. In this
// scenario, at 1% catch up it took around 20+ seconds to finally catch
// up. For these kinds of scenarios it will be better to snap / clamp.
//
// to reproduce, try snapshot interpolation demo and press the button to
// simulate the client timeline at multiple seconds behind. it'll take
// a long time to catch up if the timeline is a long time behind.
public static double TimelineClamp(
double localTimeline,
double bufferTime,
double latestRemoteTime)
{
// we want local timeline to always be 'bufferTime' behind remote.
double targetTime = latestRemoteTime - bufferTime;
// we define a boundary of 'bufferTime' around the target time.
// this is where catchup / slowdown will happen.
// outside of the area, we clamp.
double lowerBound = targetTime - bufferTime; // how far behind we can get
double upperBound = targetTime + bufferTime; // how far ahead we can get
return Mathd.Clamp(localTimeline, lowerBound, upperBound);
}
// call this for every received snapshot.
// adds / inserts it to the list & initializes local time if needed.
public static void InsertAndAdjust<T>(
SortedList<double, T> buffer, // snapshot buffer
int bufferLimit, // don't grow infinitely
T snapshot, // the newly received snapshot
ref double localTimeline, // local interpolation time based on server time
ref double localTimescale, // timeline multiplier to apply catchup / slowdown over time
float sendInterval, // for debugging
double bufferTime, // offset for buffering
double catchupSpeed, // in % [0,1]
double slowdownSpeed, // in % [0,1]
ref ExponentialMovingAverage driftEma, // for catchup / slowdown
float catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots)
float catchupPositiveThreshold, // in % of sendInterval
ref ExponentialMovingAverage deliveryTimeEma) // for dynamic buffer time adjustment
where T : Snapshot
{
// first snapshot?
// initialize local timeline.
// we want it to be behind by 'offset'.
//
// note that the first snapshot may be a lagging packet.
// so we would always be behind by that lag.
// this requires catchup later.
if (buffer.Count == 0)
localTimeline = snapshot.remoteTime - bufferTime;
// insert into the buffer.
//
// note that we might insert it between our current interpolation
// which is fine, it adds another data point for accuracy.
//
// note that insert may be called twice for the same key.
// by default, this would throw.
// need to handle it silently.
if (InsertIfNotExists(buffer, bufferLimit, snapshot))
{
// dynamic buffer adjustment needs delivery interval jitter
if (buffer.Count >= 2)
{
// note that this is not entirely accurate for scrambled inserts.
//
// we always use the last two, not what we just inserted
// even if we were to use the diff for what we just inserted,
// a scrambled insert would still not be 100% accurate:
// => assume a buffer of AC, with delivery time C-A
// => we then insert B, with delivery time B-A
// => but then technically the first C-A wasn't correct,
// as it would have to be C-B
//
// in practice, scramble is rare and won't make much difference
double previousLocalTime = buffer.Values[buffer.Count - 2].localTime;
double lastestLocalTime = buffer.Values[buffer.Count - 1].localTime;
// this is the delivery time since last snapshot
double localDeliveryTime = lastestLocalTime - previousLocalTime;
// feed the local delivery time to the EMA.
// this is what the original stream did too.
// our final dynamic buffer adjustment is different though.
// we use standard deviation instead of average.
deliveryTimeEma.Add(localDeliveryTime);
}
// adjust timescale to catch up / slow down after each insertion
// because that is when we add new values to our EMA.
// we want localTimeline to be about 'bufferTime' behind.
// for that, we need the delivery time EMA.
// snapshots may arrive out of order, we can not use last-timeline.
// we need to use the inserted snapshot's time - timeline.
double latestRemoteTime = snapshot.remoteTime;
// ensure timeline stays within a reasonable bound behind/ahead.
localTimeline = TimelineClamp(localTimeline, bufferTime, latestRemoteTime);
// calculate timediff after localTimeline override changes
double timeDiff = latestRemoteTime - localTimeline;
// next, calculate average of a few seconds worth of timediffs.
// this gives smoother results.
//
// to calculate the average, we could simply loop through the
// last 'n' seconds worth of timediffs, but:
// - our buffer may only store a few snapshots (bufferTime)
// - looping through seconds worth of snapshots every time is
// expensive
//
// to solve this, we use an exponential moving average.
// https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
// which is basically fancy math to do the same but faster.
// additionally, it allows us to look at more timeDiff values
// than we sould have access to in our buffer :)
driftEma.Add(timeDiff);
// timescale depends on driftEma.
// driftEma only changes when inserting.
// therefore timescale only needs to be calculated when inserting.
// saves CPU cycles in Update.
// next up, calculate how far we are currently away from bufferTime
double drift = driftEma.Value - bufferTime;
// convert relative thresholds to absolute values based on sendInterval
double absoluteNegativeThreshold = sendInterval * catchupNegativeThreshold;
double absolutePositiveThreshold = sendInterval * catchupPositiveThreshold;
// next, set localTimescale to catchup consistently in Update().
// we quantize between default/catchup/slowdown,
// this way we have 'default' speed most of the time(!).
// and only catch up / slow down for a little bit occasionally.
// a consistent multiplier would never be exactly 1.0.
localTimescale = Timescale(drift, catchupSpeed, slowdownSpeed, absoluteNegativeThreshold, absolutePositiveThreshold);
// debug logging
// UnityEngine.Debug.Log($"sendInterval={sendInterval:F3} bufferTime={bufferTime:F3} drift={drift:F3} driftEma={driftEma.Value:F3} timescale={localTimescale:F3} deliveryIntervalEma={deliveryTimeEma.Value:F3}");
}
}
// sample snapshot buffer to find the pair around the given time.
// returns indices so we can use it with RemoveRange to clear old snaps.
// make sure to use use buffer.Values[from/to], not buffer[from/to].
// make sure to only call this is we have > 0 snapshots.
public static void Sample<T>(
SortedList<double, T> buffer, // snapshot buffer
double localTimeline, // local interpolation time based on server time
out int from, // the snapshot <= time
out int to, // the snapshot >= time
out double t) // interpolation factor
where T : Snapshot
{
from = -1;
to = -1;
t = 0;
// sample from [0,count-1] so we always have two at 'i' and 'i+1'.
for (int i = 0; i < buffer.Count - 1; ++i)
{
// is local time between these two?
T first = buffer.Values[i];
T second = buffer.Values[i + 1];
if (localTimeline >= first.remoteTime &&
localTimeline <= second.remoteTime)
{
// use these two snapshots
from = i;
to = i + 1;
t = Mathd.InverseLerp(first.remoteTime, second.remoteTime, localTimeline);
return;
}
}
// didn't find two snapshots around local time.
// so pick either the first or last, depending on which is closer.
// oldest snapshot ahead of local time?
if (buffer.Values[0].remoteTime > localTimeline)
{
from = to = 0;
t = 0;
}
// otherwise initialize both to the last one
else
{
from = to = buffer.Count - 1;
t = 0;
}
}
// progress local timeline every update.
//
// ONLY CALL IF SNAPSHOTS.COUNT > 0!
//
// decoupled from Step<T> for easier testing and so we can progress
// time only once in NetworkClient, while stepping for each component.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void StepTime(
double deltaTime, // engine delta time (unscaled)
ref double localTimeline, // local interpolation time based on server time
double localTimescale) // catchup / slowdown is applied to time every update)
{
// move local forward in time, scaled with catchup / slowdown applied
localTimeline += deltaTime * localTimescale;
}
// sample, clear old.
// call this every update.
//
// ONLY CALL IF SNAPSHOTS.COUNT > 0!
//
// returns true if there is anything to apply (requires at least 1 snap)
// from/to/t are out parameters instead of an interpolated 'computed'.
// this allows us to store from/to/t globally (i.e. in NetworkClient)
// and have each component apply the interpolation manually.
// besides, passing "Func Interpolate" would allocate anyway.
public static void StepInterpolation<T>(
SortedList<double, T> buffer, // snapshot buffer
double localTimeline, // local interpolation time based on server time
out T fromSnapshot, // we interpolate 'from' this snapshot
out T toSnapshot, // 'to' this snapshot
out double t) // at ratio 't' [0,1]
where T : Snapshot
{
// check this in caller:
// nothing to do if there are no snapshots at all yet
// if (buffer.Count == 0) return false;
// sample snapshot buffer at local interpolation time
Sample(buffer, localTimeline, out int from, out int to, out t);
// save from/to
fromSnapshot = buffer.Values[from];
toSnapshot = buffer.Values[to];
// remove older snapshots that we definitely don't need anymore.
// after(!) using the indices.
//
// if we have 3 snapshots and we are between 2nd and 3rd:
// from = 1, to = 2
// then we need to remove the first one, which is exactly 'from'.
// because 'from-1' = 0 would remove none.
buffer.RemoveRange(from);
}
// update time, sample, clear old.
// call this every update.
//
// ONLY CALL IF SNAPSHOTS.COUNT > 0!
//
// returns true if there is anything to apply (requires at least 1 snap)
// from/to/t are out parameters instead of an interpolated 'computed'.
// this allows us to store from/to/t globally (i.e. in NetworkClient)
// and have each component apply the interpolation manually.
// besides, passing "Func Interpolate" would allocate anyway.
public static void Step<T>(
SortedList<double, T> buffer, // snapshot buffer
double deltaTime, // engine delta time (unscaled)
ref double localTimeline, // local interpolation time based on server time
double localTimescale, // catchup / slowdown is applied to time every update
out T fromSnapshot, // we interpolate 'from' this snapshot
out T toSnapshot, // 'to' this snapshot
out double t) // at ratio 't' [0,1]
where T : Snapshot
{
StepTime(deltaTime, ref localTimeline, localTimescale);
StepInterpolation(buffer, localTimeline, out fromSnapshot, out toSnapshot, out t);
}
}
}
fileFormatVersion: 2
guid: 72c16070d85334011853813488ab1431
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
// snapshot interpolation settings struct.
// can easily be exposed in Unity inspectors.
using System;
using UnityEngine;
namespace Mirror
{
// class so we can define defaults easily
[Serializable]
public class SnapshotInterpolationSettings
{
// decrease bufferTime at runtime to see the catchup effect.
// increase to see slowdown.
// 'double' so we can have very precise dynamic adjustment without rounding
[Header("Buffering")]
[Tooltip("Local simulation is behind by sendInterval * multiplier seconds.\n\nThis guarantees that we always have enough snapshots in the buffer to mitigate lags & jitter.\n\nIncrease this if the simulation isn't smooth. By default, it should be around 2.")]
public double bufferTimeMultiplier = 2;
[Tooltip("If a client can't process snapshots fast enough, don't store too many.")]
public int bufferLimit = 32;
// catchup /////////////////////////////////////////////////////////////
// catchup thresholds in 'frames'.
// half a frame might be too aggressive.
[Header("Catchup / Slowdown")]
[Tooltip("Slowdown begins when the local timeline is moving too fast towards remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be negative.\n\nDon't modify unless you know what you are doing.")]
public float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots
[Tooltip("Catchup begins when the local timeline is moving too slow and getting too far away from remote time. Threshold is in frames worth of snapshots.\n\nThis needs to be positive.\n\nDon't modify unless you know what you are doing.")]
public float catchupPositiveThreshold = 1;
[Tooltip("Local timeline acceleration in % while catching up.")]
[Range(0, 1)]
public double catchupSpeed = 0.02f; // see snap interp demo. 1% is too slow.
[Tooltip("Local timeline slowdown in % while slowing down.")]
[Range(0, 1)]
public double slowdownSpeed = 0.04f; // slow down a little faster so we don't encounter empty buffer (= jitter)
[Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")]
public int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway
// dynamic buffer time adjustment //////////////////////////////////////
// dynamically adjusts bufferTimeMultiplier for smooth results.
// to understand how this works, try this manually:
//
// - disable dynamic adjustment
// - set jitter = 0.2 (20% is a lot!)
// - notice some stuttering
// - disable interpolation to see just how much jitter this really is(!)
// - enable interpolation again
// - manually increase bufferTimeMultiplier to 3-4
// ... the cube slows down (blue) until it's smooth
// - with dynamic adjustment enabled, it will set 4 automatically
// ... the cube slows down (blue) until it's smooth as well
//
// note that 20% jitter is extreme.
// for this to be perfectly smooth, set the safety tolerance to '2'.
// but realistically this is not necessary, and '1' is enough.
[Header("Dynamic Adjustment")]
[Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")]
public bool dynamicAdjustment = true;
[Tooltip("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")]
public float dynamicAdjustmentTolerance = 1; // 1 is realistically just fine, 2 is very very safe even for 20% jitter. can be half a frame too. (see above comments)
[Tooltip("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")]
public int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time
}
}
fileFormatVersion: 2
guid: f955b76b7956417088c03992b3622dc9
timeCreated: 1678507210
\ No newline at end of file
namespace Mirror
{
// empty snapshot that is only used to progress client's local timeline.
public struct TimeSnapshot : Snapshot
{
public double remoteTime { get; set; }
public double localTime { get; set; }
public TimeSnapshot(double remoteTime, double localTime)
{
this.remoteTime = remoteTime;
this.localTime = localTime;
}
}
}
fileFormatVersion: 2
guid: afe2b5ed49634971a2aec720ad74e5cd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
using System;
using System.Collections;
using System.Collections.Generic;
namespace Mirror
{
public class SyncIDictionary<TKey, TValue> : SyncObject, IDictionary<TKey, TValue>, IReadOnlyDictionary<TKey, TValue>
{
/// <summary>This is called after the item is added with TKey</summary>
public Action<TKey> OnAdd;
/// <summary>This is called after the item is changed with TKey. TValue is the OLD item</summary>
public Action<TKey, TValue> OnSet;
/// <summary>This is called after the item is removed with TKey. TValue is the OLD item</summary>
public Action<TKey, TValue> OnRemove;
/// <summary>
/// This is called for all changes to the Dictionary.
/// <para>For OP_ADD, TValue is the NEW value of the entry.</para>
/// <para>For OP_SET and OP_REMOVE, TValue is the OLD value of the entry.</para>
/// <para>For OP_CLEAR, both TKey and TValue are default.</para>
/// </summary>
public Action<Operation, TKey, TValue> OnChange;
/// <summary>This is called before the data is cleared</summary>
public Action OnClear;
// Deprecated 2024-03-22
[Obsolete("Use individual Actions, which pass OLD values where appropriate, instead.")]
public Action<Operation, TKey, TValue> Callback;
protected readonly IDictionary<TKey, TValue> objects;
public SyncIDictionary(IDictionary<TKey, TValue> objects)
{
this.objects = objects;
}
public int Count => objects.Count;
public bool IsReadOnly => !IsWritable();
public enum Operation : byte
{
OP_ADD,
OP_CLEAR,
OP_REMOVE,
OP_SET
}
struct Change
{
internal Operation operation;
internal TKey key;
internal TValue item;
}
// list of changes.
// -> insert/delete/clear is only ONE change
// -> changing the same slot 10x causes 10 changes.
// -> note that this grows until next sync(!)
// TODO Dictionary<key, change> to avoid ever growing changes / redundant changes!
readonly List<Change> changes = new List<Change>();
// how many changes we need to ignore
// this is needed because when we initialize the list,
// we might later receive changes that have already been applied
// so we need to skip them
int changesAhead;
public ICollection<TKey> Keys => objects.Keys;
public ICollection<TValue> Values => objects.Values;
IEnumerable<TKey> IReadOnlyDictionary<TKey, TValue>.Keys => objects.Keys;
IEnumerable<TValue> IReadOnlyDictionary<TKey, TValue>.Values => objects.Values;
public override void OnSerializeAll(NetworkWriter writer)
{
// if init, write the full list content
writer.WriteUInt((uint)objects.Count);
foreach (KeyValuePair<TKey, TValue> syncItem in objects)
{
writer.Write(syncItem.Key);
writer.Write(syncItem.Value);
}
// all changes have been applied already
// thus the client will need to skip all the pending changes
// or they would be applied again.
// So we write how many changes are pending
writer.WriteUInt((uint)changes.Count);
}
public override void OnSerializeDelta(NetworkWriter writer)
{
// write all the queued up changes
writer.WriteUInt((uint)changes.Count);
for (int i = 0; i < changes.Count; i++)
{
Change change = changes[i];
writer.WriteByte((byte)change.operation);
switch (change.operation)
{
case Operation.OP_ADD:
case Operation.OP_SET:
writer.Write(change.key);
writer.Write(change.item);
break;
case Operation.OP_REMOVE:
writer.Write(change.key);
break;
case Operation.OP_CLEAR:
break;
}
}
}
public override void OnDeserializeAll(NetworkReader reader)
{
// if init, write the full list content
int count = (int)reader.ReadUInt();
objects.Clear();
changes.Clear();
for (int i = 0; i < count; i++)
{
TKey key = reader.Read<TKey>();
TValue obj = reader.Read<TValue>();
objects.Add(key, obj);
}
// We will need to skip all these changes
// the next time the list is synchronized
// because they have already been applied
changesAhead = (int)reader.ReadUInt();
}
public override void OnDeserializeDelta(NetworkReader reader)
{
int changesCount = (int)reader.ReadUInt();
for (int i = 0; i < changesCount; i++)
{
Operation operation = (Operation)reader.ReadByte();
// apply the operation only if it is a new change
// that we have not applied yet
bool apply = changesAhead == 0;
TKey key = default;
TValue item = default;
switch (operation)
{
case Operation.OP_ADD:
case Operation.OP_SET:
key = reader.Read<TKey>();
item = reader.Read<TValue>();
if (apply)
{
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
if (objects.TryGetValue(key, out TValue oldItem))
{
objects[key] = item; // assign after TryGetValue
AddOperation(Operation.OP_SET, key, item, oldItem, false);
}
else
{
objects[key] = item; // assign after TryGetValue
AddOperation(Operation.OP_ADD, key, item, default, false);
}
}
break;
case Operation.OP_CLEAR:
if (apply)
{
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
AddOperation(Operation.OP_CLEAR, default, default, default, false);
// clear after invoking the callback so users can iterate the dictionary
// and take appropriate action on the items before they are wiped.
objects.Clear();
}
break;
case Operation.OP_REMOVE:
key = reader.Read<TKey>();
if (apply)
{
if (objects.TryGetValue(key, out TValue oldItem))
{
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
objects.Remove(key);
AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, false);
}
}
break;
}
if (!apply)
{
// we just skipped this change
changesAhead--;
}
}
}
// throw away all the changes
// this should be called after a successful sync
public override void ClearChanges() => changes.Clear();
public override void Reset()
{
changes.Clear();
changesAhead = 0;
objects.Clear();
}
public TValue this[TKey i]
{
get => objects[i];
set
{
if (ContainsKey(i))
{
TValue oldItem = objects[i];
objects[i] = value;
AddOperation(Operation.OP_SET, i, value, oldItem, true);
}
else
{
objects[i] = value;
AddOperation(Operation.OP_ADD, i, value, default, true);
}
}
}
public bool TryGetValue(TKey key, out TValue value) => objects.TryGetValue(key, out value);
public bool ContainsKey(TKey key) => objects.ContainsKey(key);
public bool Contains(KeyValuePair<TKey, TValue> item) => TryGetValue(item.Key, out TValue val) && EqualityComparer<TValue>.Default.Equals(val, item.Value);
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
{
if (arrayIndex < 0 || arrayIndex > array.Length)
throw new System.ArgumentOutOfRangeException(nameof(arrayIndex), "Array Index Out of Range");
if (array.Length - arrayIndex < Count)
throw new System.ArgumentException("The number of items in the SyncDictionary is greater than the available space from arrayIndex to the end of the destination array");
int i = arrayIndex;
foreach (KeyValuePair<TKey, TValue> item in objects)
{
array[i] = item;
i++;
}
}
public void Add(KeyValuePair<TKey, TValue> item) => Add(item.Key, item.Value);
public void Add(TKey key, TValue value)
{
objects.Add(key, value);
AddOperation(Operation.OP_ADD, key, value, default, true);
}
public bool Remove(TKey key)
{
if (objects.TryGetValue(key, out TValue oldItem) && objects.Remove(key))
{
AddOperation(Operation.OP_REMOVE, key, oldItem, oldItem, true);
return true;
}
return false;
}
public bool Remove(KeyValuePair<TKey, TValue> item)
{
bool result = objects.Remove(item.Key);
if (result)
AddOperation(Operation.OP_REMOVE, item.Key, item.Value, item.Value, true);
return result;
}
public void Clear()
{
AddOperation(Operation.OP_CLEAR, default, default, default, true);
// clear after invoking the callback so users can iterate the dictionary
// and take appropriate action on the items before they are wiped.
objects.Clear();
}
void AddOperation(Operation op, TKey key, TValue item, TValue oldItem, bool checkAccess)
{
if (checkAccess && IsReadOnly)
throw new InvalidOperationException("SyncDictionaries can only be modified by the owner.");
Change change = new Change
{
operation = op,
key = key,
item = item
};
if (IsRecording())
{
changes.Add(change);
OnDirty?.Invoke();
}
switch (op)
{
case Operation.OP_ADD:
OnAdd?.Invoke(key);
OnChange?.Invoke(op, key, item);
break;
case Operation.OP_SET:
OnSet?.Invoke(key, oldItem);
OnChange?.Invoke(op, key, oldItem);
break;
case Operation.OP_REMOVE:
OnRemove?.Invoke(key, oldItem);
OnChange?.Invoke(op, key, oldItem);
break;
case Operation.OP_CLEAR:
OnClear?.Invoke();
OnChange?.Invoke(op, default, default);
break;
}
#pragma warning disable CS0618 // Type or member is obsolete
Callback?.Invoke(op, key, item);
#pragma warning restore CS0618 // Type or member is obsolete
}
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => objects.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => objects.GetEnumerator();
}
public class SyncDictionary<TKey, TValue> : SyncIDictionary<TKey, TValue>
{
public SyncDictionary() : base(new Dictionary<TKey, TValue>()) { }
public SyncDictionary(IEqualityComparer<TKey> eq) : base(new Dictionary<TKey, TValue>(eq)) { }
public SyncDictionary(IDictionary<TKey, TValue> d) : base(new Dictionary<TKey, TValue>(d)) { }
public new Dictionary<TKey, TValue>.ValueCollection Values => ((Dictionary<TKey, TValue>)objects).Values;
public new Dictionary<TKey, TValue>.KeyCollection Keys => ((Dictionary<TKey, TValue>)objects).Keys;
public new Dictionary<TKey, TValue>.Enumerator GetEnumerator() => ((Dictionary<TKey, TValue>)objects).GetEnumerator();
}
}
fileFormatVersion: 2
guid: 4b346c49cfdb668488a364c3023590e2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
using System;
using System.Collections;
using System.Collections.Generic;
namespace Mirror
{
public class SyncList<T> : SyncObject, IList<T>, IReadOnlyList<T>
{
public enum Operation : byte
{
OP_ADD,
OP_SET,
OP_INSERT,
OP_REMOVEAT,
OP_CLEAR
}
/// <summary>This is called after the item is added with index</summary>
public Action<int> OnAdd;
/// <summary>This is called after the item is inserted with inedx</summary>
public Action<int> OnInsert;
/// <summary>This is called after the item is set with index and OLD Value</summary>
public Action<int, T> OnSet;
/// <summary>This is called after the item is removed with index and OLD Value</summary>
public Action<int, T> OnRemove;
/// <summary>
/// This is called for all changes to the List.
/// <para>For OP_ADD and OP_INSERT, T is the NEW value of the entry.</para>
/// <para>For OP_SET and OP_REMOVE, T is the OLD value of the entry.</para>
/// <para>For OP_CLEAR, T is default.</para>
/// </summary>
public Action<Operation, int, T> OnChange;
/// <summary>This is called before the list is cleared so the list can be iterated</summary>
public Action OnClear;
// Deprecated 2024-03-23
[Obsolete("Use individual Actions, which pass OLD values where appropriate, instead.")]
public Action<Operation, int, T, T> Callback;
readonly IList<T> objects;
readonly IEqualityComparer<T> comparer;
public int Count => objects.Count;
public bool IsReadOnly => !IsWritable();
struct Change
{
internal Operation operation;
internal int index;
internal T item;
}
// list of changes.
// -> insert/delete/clear is only ONE change
// -> changing the same slot 10x caues 10 changes.
// -> note that this grows until next sync(!)
readonly List<Change> changes = new List<Change>();
// how many changes we need to ignore
// this is needed because when we initialize the list,
// we might later receive changes that have already been applied
// so we need to skip them
int changesAhead;
public SyncList() : this(EqualityComparer<T>.Default) { }
public SyncList(IEqualityComparer<T> comparer)
{
this.comparer = comparer ?? EqualityComparer<T>.Default;
objects = new List<T>();
}
public SyncList(IList<T> objects, IEqualityComparer<T> comparer = null)
{
this.comparer = comparer ?? EqualityComparer<T>.Default;
this.objects = objects;
}
// throw away all the changes
// this should be called after a successful sync
public override void ClearChanges() => changes.Clear();
public override void Reset()
{
changes.Clear();
changesAhead = 0;
objects.Clear();
}
void AddOperation(Operation op, int itemIndex, T oldItem, T newItem, bool checkAccess)
{
if (checkAccess && IsReadOnly)
throw new InvalidOperationException("Synclists can only be modified by the owner.");
Change change = new Change
{
operation = op,
index = itemIndex,
item = newItem
};
if (IsRecording())
{
changes.Add(change);
OnDirty?.Invoke();
}
switch (op)
{
case Operation.OP_ADD:
OnAdd?.Invoke(itemIndex);
OnChange?.Invoke(op, itemIndex, newItem);
break;
case Operation.OP_INSERT:
OnInsert?.Invoke(itemIndex);
OnChange?.Invoke(op, itemIndex, newItem);
break;
case Operation.OP_SET:
OnSet?.Invoke(itemIndex, oldItem);
OnChange?.Invoke(op, itemIndex, oldItem);
break;
case Operation.OP_REMOVEAT:
OnRemove?.Invoke(itemIndex, oldItem);
OnChange?.Invoke(op, itemIndex, oldItem);
break;
case Operation.OP_CLEAR:
OnClear?.Invoke();
OnChange?.Invoke(op, itemIndex, default);
break;
}
#pragma warning disable CS0618 // Type or member is obsolete
Callback?.Invoke(op, itemIndex, oldItem, newItem);
#pragma warning restore CS0618 // Type or member is obsolete
}
public override void OnSerializeAll(NetworkWriter writer)
{
// if init, write the full list content
writer.WriteUInt((uint)objects.Count);
for (int i = 0; i < objects.Count; i++)
{
T obj = objects[i];
writer.Write(obj);
}
// all changes have been applied already
// thus the client will need to skip all the pending changes
// or they would be applied again.
// So we write how many changes are pending
writer.WriteUInt((uint)changes.Count);
}
public override void OnSerializeDelta(NetworkWriter writer)
{
// write all the queued up changes
writer.WriteUInt((uint)changes.Count);
for (int i = 0; i < changes.Count; i++)
{
Change change = changes[i];
writer.WriteByte((byte)change.operation);
switch (change.operation)
{
case Operation.OP_ADD:
writer.Write(change.item);
break;
case Operation.OP_CLEAR:
break;
case Operation.OP_REMOVEAT:
writer.WriteUInt((uint)change.index);
break;
case Operation.OP_INSERT:
case Operation.OP_SET:
writer.WriteUInt((uint)change.index);
writer.Write(change.item);
break;
}
}
}
public override void OnDeserializeAll(NetworkReader reader)
{
// if init, write the full list content
int count = (int)reader.ReadUInt();
objects.Clear();
changes.Clear();
for (int i = 0; i < count; i++)
{
T obj = reader.Read<T>();
objects.Add(obj);
}
// We will need to skip all these changes
// the next time the list is synchronized
// because they have already been applied
changesAhead = (int)reader.ReadUInt();
}
public override void OnDeserializeDelta(NetworkReader reader)
{
int changesCount = (int)reader.ReadUInt();
for (int i = 0; i < changesCount; i++)
{
Operation operation = (Operation)reader.ReadByte();
// apply the operation only if it is a new change
// that we have not applied yet
bool apply = changesAhead == 0;
int index = 0;
T oldItem = default;
T newItem = default;
switch (operation)
{
case Operation.OP_ADD:
newItem = reader.Read<T>();
if (apply)
{
index = objects.Count;
objects.Add(newItem);
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
AddOperation(Operation.OP_ADD, objects.Count - 1, default, newItem, false);
}
break;
case Operation.OP_CLEAR:
if (apply)
{
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
AddOperation(Operation.OP_CLEAR, 0, default, default, false);
// clear after invoking the callback so users can iterate the list
// and take appropriate action on the items before they are wiped.
objects.Clear();
}
break;
case Operation.OP_INSERT:
index = (int)reader.ReadUInt();
newItem = reader.Read<T>();
if (apply)
{
objects.Insert(index, newItem);
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
AddOperation(Operation.OP_INSERT, index, default, newItem, false);
}
break;
case Operation.OP_REMOVEAT:
index = (int)reader.ReadUInt();
if (apply)
{
oldItem = objects[index];
objects.RemoveAt(index);
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
AddOperation(Operation.OP_REMOVEAT, index, oldItem, default, false);
}
break;
case Operation.OP_SET:
index = (int)reader.ReadUInt();
newItem = reader.Read<T>();
if (apply)
{
oldItem = objects[index];
objects[index] = newItem;
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
AddOperation(Operation.OP_SET, index, oldItem, newItem, false);
}
break;
}
if (!apply)
{
// we just skipped this change
changesAhead--;
}
}
}
public void Add(T item)
{
objects.Add(item);
AddOperation(Operation.OP_ADD, objects.Count - 1, default, item, true);
}
public void AddRange(IEnumerable<T> range)
{
foreach (T entry in range)
Add(entry);
}
public void Clear()
{
AddOperation(Operation.OP_CLEAR, 0, default, default, true);
// clear after invoking the callback so users can iterate the list
// and take appropriate action on the items before they are wiped.
objects.Clear();
}
public bool Contains(T item) => IndexOf(item) >= 0;
public void CopyTo(T[] array, int index) => objects.CopyTo(array, index);
public int IndexOf(T item)
{
for (int i = 0; i < objects.Count; ++i)
if (comparer.Equals(item, objects[i]))
return i;
return -1;
}
public int FindIndex(Predicate<T> match)
{
for (int i = 0; i < objects.Count; ++i)
if (match(objects[i]))
return i;
return -1;
}
public T Find(Predicate<T> match)
{
int i = FindIndex(match);
return (i != -1) ? objects[i] : default;
}
public List<T> FindAll(Predicate<T> match)
{
List<T> results = new List<T>();
for (int i = 0; i < objects.Count; ++i)
if (match(objects[i]))
results.Add(objects[i]);
return results;
}
public void Insert(int index, T item)
{
objects.Insert(index, item);
AddOperation(Operation.OP_INSERT, index, default, item, true);
}
public void InsertRange(int index, IEnumerable<T> range)
{
foreach (T entry in range)
{
Insert(index, entry);
index++;
}
}
public bool Remove(T item)
{
int index = IndexOf(item);
bool result = index >= 0;
if (result)
RemoveAt(index);
return result;
}
public void RemoveAt(int index)
{
T oldItem = objects[index];
objects.RemoveAt(index);
AddOperation(Operation.OP_REMOVEAT, index, oldItem, default, true);
}
public int RemoveAll(Predicate<T> match)
{
List<T> toRemove = new List<T>();
for (int i = 0; i < objects.Count; ++i)
if (match(objects[i]))
toRemove.Add(objects[i]);
foreach (T entry in toRemove)
Remove(entry);
return toRemove.Count;
}
public T this[int i]
{
get => objects[i];
set
{
if (!comparer.Equals(objects[i], value))
{
T oldItem = objects[i];
objects[i] = value;
AddOperation(Operation.OP_SET, i, oldItem, value, true);
}
}
}
public Enumerator GetEnumerator() => new Enumerator(this);
IEnumerator<T> IEnumerable<T>.GetEnumerator() => new Enumerator(this);
IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this);
// default Enumerator allocates. we need a custom struct Enumerator to
// not allocate on the heap.
// (System.Collections.Generic.List<T> source code does the same)
//
// benchmark:
// uMMORPG with 800 monsters, Skills.GetHealthBonus() which runs a
// foreach on skills SyncList:
// before: 81.2KB GC per frame
// after: 0KB GC per frame
// => this is extremely important for MMO scale networking
public struct Enumerator : IEnumerator<T>
{
readonly SyncList<T> list;
int index;
public T Current { get; private set; }
public Enumerator(SyncList<T> list)
{
this.list = list;
index = -1;
Current = default;
}
public bool MoveNext()
{
if (++index >= list.Count)
return false;
Current = list[index];
return true;
}
public void Reset() => index = -1;
object IEnumerator.Current => Current;
public void Dispose() { }
}
}
}
fileFormatVersion: 2
guid: 744fc71f748fe40d5940e04bf42b29f3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
using System;
namespace Mirror
{
/// <summary>SyncObjects sync state between server and client. E.g. SyncLists.</summary>
// SyncObject should be a class (instead of an interface) for a few reasons:
// * NetworkBehaviour stores SyncObjects in a list. structs would be a copy
// and OnSerialize would use the copy instead of the original struct.
// * Obsolete functions like Flush() don't need to be defined by each type
// * OnDirty/IsRecording etc. default functions can be defined once here
// for example, handling 'OnDirty wasn't initialized' with a default
// function that throws an exception will be useful for SyncVar<T>
public abstract class SyncObject
{
/// <summary>Used internally to set owner NetworkBehaviour's dirty mask bit when changed.</summary>
public Action OnDirty;
/// <summary>Used internally to check if we are currently tracking changes.</summary>
// prevents ever growing .changes lists:
// if a monster has no observers but we keep modifying a SyncObject,
// then the changes would never be flushed and keep growing,
// because OnSerialize isn't called without observers.
// => Func so we can set it to () => observers.Count > 0
// without depending on NetworkComponent/NetworkIdentity here.
// => virtual so it simply always records by default
public Func<bool> IsRecording = () => true;
// SyncList/Set/etc. shouldn't be modifiable if not owned.
// otherwise they would silently get out of sync.
// need a lambda because InitSyncObject is called in ctor, when
// 'isClient' etc. aren't initialized yet.
public Func<bool> IsWritable = () => true;
/// <summary>Discard all the queued changes</summary>
// Consider the object fully synchronized with clients
public abstract void ClearChanges();
/// <summary>Write a full copy of the object</summary>
public abstract void OnSerializeAll(NetworkWriter writer);
/// <summary>Write the changes made to the object since last sync</summary>
public abstract void OnSerializeDelta(NetworkWriter writer);
/// <summary>Reads a full copy of the object</summary>
public abstract void OnDeserializeAll(NetworkReader reader);
/// <summary>Reads the changes made to the object since last sync</summary>
public abstract void OnDeserializeDelta(NetworkReader reader);
/// <summary>Resets the SyncObject so that it can be re-used</summary>
public abstract void Reset();
}
}
fileFormatVersion: 2
guid: ae226d17a0c844041aa24cc2c023dd49
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
using System;
using System.Collections;
using System.Collections.Generic;
namespace Mirror
{
public class SyncSet<T> : SyncObject, ISet<T>
{
/// <summary>This is called after the item is added. T is the new item.</summary>
public Action<T> OnAdd;
/// <summary>This is called after the item is removed. T is the OLD item</summary>
public Action<T> OnRemove;
/// <summary>
/// This is called for all changes to the Set.
/// <para>For OP_ADD, T is the NEW value of the entry.</para>
/// <para>For OP_REMOVE, T is the OLD value of the entry.</para>
/// <para>For OP_CLEAR, T is default.</para>
/// </summary>
public Action<Operation, T> OnChange;
/// <summary>This is called BEFORE the data is cleared</summary>
public Action OnClear;
// Deprecated 2024-03-22
[Obsolete("Use individual Actions, which pass OLD value where appropriate, instead.")]
public Action<Operation, T> Callback;
protected readonly ISet<T> objects;
public int Count => objects.Count;
public bool IsReadOnly => !IsWritable();
public enum Operation : byte
{
OP_ADD,
OP_REMOVE,
OP_CLEAR
}
struct Change
{
internal Operation operation;
internal T item;
}
// list of changes.
// -> insert/delete/clear is only ONE change
// -> changing the same slot 10x caues 10 changes.
// -> note that this grows until next sync(!)
// TODO Dictionary<key, change> to avoid ever growing changes / redundant changes!
readonly List<Change> changes = new List<Change>();
// how many changes we need to ignore
// this is needed because when we initialize the list,
// we might later receive changes that have already been applied
// so we need to skip them
int changesAhead;
public SyncSet(ISet<T> objects)
{
this.objects = objects;
}
public override void Reset()
{
changes.Clear();
changesAhead = 0;
objects.Clear();
}
// throw away all the changes
// this should be called after a successful sync
public override void ClearChanges() => changes.Clear();
void AddOperation(Operation op, T oldItem, T newItem, bool checkAccess)
{
if (checkAccess && IsReadOnly)
throw new InvalidOperationException("SyncSets can only be modified by the owner.");
Change change = default;
switch (op)
{
case Operation.OP_ADD:
change = new Change
{
operation = op,
item = newItem
};
break;
case Operation.OP_REMOVE:
change = new Change
{
operation = op,
item = oldItem
};
break;
case Operation.OP_CLEAR:
change = new Change
{
operation = op,
item = default
};
break;
}
if (IsRecording())
{
changes.Add(change);
OnDirty?.Invoke();
}
switch (op)
{
case Operation.OP_ADD:
OnAdd?.Invoke(newItem);
OnChange?.Invoke(op, newItem);
#pragma warning disable CS0618 // Type or member is obsolete
Callback?.Invoke(op, newItem);
#pragma warning restore CS0618 // Type or member is obsolete
break;
case Operation.OP_REMOVE:
OnRemove?.Invoke(oldItem);
OnChange?.Invoke(op, oldItem);
#pragma warning disable CS0618 // Type or member is obsolete
Callback?.Invoke(op, oldItem);
#pragma warning restore CS0618 // Type or member is obsolete
break;
case Operation.OP_CLEAR:
OnClear?.Invoke();
OnChange?.Invoke(op, default);
#pragma warning disable CS0618 // Type or member is obsolete
Callback?.Invoke(op, default);
#pragma warning restore CS0618 // Type or member is obsolete
break;
}
}
void AddOperation(Operation op, bool checkAccess) => AddOperation(op, default, default, checkAccess);
public override void OnSerializeAll(NetworkWriter writer)
{
// if init, write the full list content
writer.WriteUInt((uint)objects.Count);
foreach (T obj in objects)
writer.Write(obj);
// all changes have been applied already
// thus the client will need to skip all the pending changes
// or they would be applied again.
// So we write how many changes are pending
writer.WriteUInt((uint)changes.Count);
}
public override void OnSerializeDelta(NetworkWriter writer)
{
// write all the queued up changes
writer.WriteUInt((uint)changes.Count);
for (int i = 0; i < changes.Count; i++)
{
Change change = changes[i];
writer.WriteByte((byte)change.operation);
switch (change.operation)
{
case Operation.OP_ADD:
writer.Write(change.item);
break;
case Operation.OP_REMOVE:
writer.Write(change.item);
break;
case Operation.OP_CLEAR:
break;
}
}
}
public override void OnDeserializeAll(NetworkReader reader)
{
// if init, write the full list content
int count = (int)reader.ReadUInt();
objects.Clear();
changes.Clear();
for (int i = 0; i < count; i++)
{
T obj = reader.Read<T>();
objects.Add(obj);
}
// We will need to skip all these changes
// the next time the list is synchronized
// because they have already been applied
changesAhead = (int)reader.ReadUInt();
}
public override void OnDeserializeDelta(NetworkReader reader)
{
int changesCount = (int)reader.ReadUInt();
for (int i = 0; i < changesCount; i++)
{
Operation operation = (Operation)reader.ReadByte();
// apply the operation only if it is a new change
// that we have not applied yet
bool apply = changesAhead == 0;
T oldItem = default;
T newItem = default;
switch (operation)
{
case Operation.OP_ADD:
newItem = reader.Read<T>();
if (apply)
{
objects.Add(newItem);
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
AddOperation(Operation.OP_ADD, default, newItem, false);
}
break;
case Operation.OP_REMOVE:
oldItem = reader.Read<T>();
if (apply)
{
objects.Remove(oldItem);
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
AddOperation(Operation.OP_REMOVE, oldItem, default, false);
}
break;
case Operation.OP_CLEAR:
if (apply)
{
// add dirty + changes.
// ClientToServer needs to set dirty in server OnDeserialize.
// no access check: server OnDeserialize can always
// write, even for ClientToServer (for broadcasting).
AddOperation(Operation.OP_CLEAR, false);
// clear after invoking the callback so users can iterate the set
// and take appropriate action on the items before they are wiped.
objects.Clear();
}
break;
}
if (!apply)
{
// we just skipped this change
changesAhead--;
}
}
}
public bool Add(T item)
{
if (objects.Add(item))
{
AddOperation(Operation.OP_ADD, default, item, true);
return true;
}
return false;
}
void ICollection<T>.Add(T item)
{
if (objects.Add(item))
AddOperation(Operation.OP_ADD, default, item, true);
}
public void Clear()
{
AddOperation(Operation.OP_CLEAR, true);
// clear after invoking the callback so users can iterate the set
// and take appropriate action on the items before they are wiped.
objects.Clear();
}
public bool Contains(T item) => objects.Contains(item);
public void CopyTo(T[] array, int index) => objects.CopyTo(array, index);
public bool Remove(T item)
{
if (objects.Remove(item))
{
AddOperation(Operation.OP_REMOVE, item, default, true);
return true;
}
return false;
}
public IEnumerator<T> GetEnumerator() => objects.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void ExceptWith(IEnumerable<T> other)
{
if (other == this)
{
Clear();
return;
}
// remove every element in other from this
foreach (T element in other)
Remove(element);
}
public void IntersectWith(IEnumerable<T> other)
{
if (other is ISet<T> otherSet)
IntersectWithSet(otherSet);
else
{
HashSet<T> otherAsSet = new HashSet<T>(other);
IntersectWithSet(otherAsSet);
}
}
void IntersectWithSet(ISet<T> otherSet)
{
List<T> elements = new List<T>(objects);
foreach (T element in elements)
if (!otherSet.Contains(element))
Remove(element);
}
public bool IsProperSubsetOf(IEnumerable<T> other) => objects.IsProperSubsetOf(other);
public bool IsProperSupersetOf(IEnumerable<T> other) => objects.IsProperSupersetOf(other);
public bool IsSubsetOf(IEnumerable<T> other) => objects.IsSubsetOf(other);
public bool IsSupersetOf(IEnumerable<T> other) => objects.IsSupersetOf(other);
public bool Overlaps(IEnumerable<T> other) => objects.Overlaps(other);
public bool SetEquals(IEnumerable<T> other) => objects.SetEquals(other);
// custom implementation so we can do our own Clear/Add/Remove for delta
public void SymmetricExceptWith(IEnumerable<T> other)
{
if (other == this)
Clear();
else
foreach (T element in other)
if (!Remove(element))
Add(element);
}
// custom implementation so we can do our own Clear/Add/Remove for delta
public void UnionWith(IEnumerable<T> other)
{
if (other != this)
foreach (T element in other)
Add(element);
}
}
public class SyncHashSet<T> : SyncSet<T>
{
public SyncHashSet() : this(EqualityComparer<T>.Default) { }
public SyncHashSet(IEqualityComparer<T> comparer) : base(new HashSet<T>(comparer ?? EqualityComparer<T>.Default)) { }
// allocation free enumerator
public new HashSet<T>.Enumerator GetEnumerator() => ((HashSet<T>)objects).GetEnumerator();
}
public class SyncSortedSet<T> : SyncSet<T>
{
public SyncSortedSet() : this(Comparer<T>.Default) { }
public SyncSortedSet(IComparer<T> comparer) : base(new SortedSet<T>(comparer ?? Comparer<T>.Default)) { }
// allocation free enumerator
public new SortedSet<T>.Enumerator GetEnumerator() => ((SortedSet<T>)objects).GetEnumerator();
}
}
fileFormatVersion: 2
guid: 8a31599d9f9dd4ef9999f7b9707c832c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 7453abfe9e8b2c04a8a47eb536fe21eb, type: 3}
userData:
assetBundleName:
assetBundleVariant:
fileFormatVersion: 2
guid: 752fcafbee1ec45c9a43c0cf65da39de
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
// API consistent with Microsoft's ObjectPool<T>.
// thread safe.
using System.Runtime.CompilerServices;
namespace Mirror
{
public static class ConcurrentNetworkWriterPool
{
// initial capacity to avoid allocations in the first few frames
// 1000 * 1200 bytes = around 1 MB.
public const int InitialCapacity = 1000;
// reuse ConcurrentPool<T>
// we still wrap it in NetworkWriterPool.Get/Recycle so we can reset the
// position before reusing.
// this is also more consistent with NetworkReaderPool where we need to
// assign the internal buffer before reusing.
static readonly ConcurrentPool<ConcurrentNetworkWriterPooled> pool =
new ConcurrentPool<ConcurrentNetworkWriterPooled>(
// new object function
() => new ConcurrentNetworkWriterPooled(),
// initial capacity to avoid allocations in the first few frames
// 1000 * 1200 bytes = around 1 MB.
InitialCapacity
);
// pool size access for debugging & tests
public static int Count => pool.Count;
public static ConcurrentNetworkWriterPooled Get()
{
// grab from pool & reset position
ConcurrentNetworkWriterPooled writer = pool.Get();
writer.Position = 0;
return writer;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Return(ConcurrentNetworkWriterPooled writer)
{
pool.Return(writer);
}
}
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment