Skip to content
20 changes: 20 additions & 0 deletions Assets/FishNet/Runtime/Editor/Configuring/BetaModeMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class BetaModeMenu : MonoBehaviour
private const string THREADED_TICKSMOOTHERS_DEFINE = "FISHNET_THREADED_TICKSMOOTHERS";
private const string THREADED_COLLIDER_ROLLBACK_DEFINE = "FISHNET_THREADED_COLLIDER_ROLLBACK";
private const string ANIMATOR_CHANNEL_DEFINE = "FISHNET_ANIMATOR_CHANNEL";
private const string DELTA_PREDICTION_DEFINE = "FISHNET_DELTA_PREDICTION";
#endregion


Expand All @@ -30,6 +31,25 @@ private static void SetAnimatorChannel(bool useStable)
}
#endregion

#region Beta Delta Prediction
/* Content: Delta Prediction
* Replicates and reconciles are written as deltas for types which have delta serializers registered.
* This changes what is sent over the network, so the define is set for every build target: a server and its clients must be built with the same setting. */
#if FISHNET_DELTA_PREDICTION
[MenuItem("Tools/Fish-Networking/Beta/Disable Delta Prediction", false, -1101)]
private static void DisableBetaDeltaPrediction() => SetBetaDeltaPrediction(useStable: true);
#else
[MenuItem("Tools/Fish-Networking/Beta/Enable Delta Prediction", false, -1101)]
private static void EnableBetaDeltaPrediction() => SetBetaDeltaPrediction(useStable: false);
#endif
private static void SetBetaDeltaPrediction(bool useStable)
{
bool result = DeveloperMenu.RemoveOrAddDefineForAllBuildTargets(DELTA_PREDICTION_DEFINE, removeDefine: useStable);
if (result)
Debug.LogWarning($"Beta Delta Prediction is now {GetBetaEnabledText(useStable)} for all build targets. Servers and clients must be built with the same setting.");
}
#endregion

#region Beta Recursive Despawns
#if FISHNET_STABLE_RECURSIVE_DESPAWNS
[MenuItem("Tools/Fish-Networking/Beta/Enable Recursive Despawns", false, -1101)]
Expand Down
56 changes: 56 additions & 0 deletions Assets/FishNet/Runtime/Editor/Configuring/ConfigurationEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using FishNet.Utility.Extension;
using GameKit.Dependencies.Utilities;
using System.Collections.Generic;
using System.Reflection;
using FishNet.Configuring.EditorCloning;
using UnityEditor;
using UnityEditor.Build;
Expand Down Expand Up @@ -88,6 +89,61 @@ internal static bool RemoveOrAddDefine(string define, bool removeDefine)

return modified;
}

/// <summary>
/// Adds or removes a define for every build target rather than only the selected one.
/// </summary>
/// <remarks>Use this for defines which change what is sent over the network, so builds for different targets, such as a dedicated server and its clients, cannot disagree.</remarks>
internal static bool RemoveOrAddDefineForAllBuildTargets(string define, bool removeDefine)
{
HashSet<string> visitedTargets = new();
bool modified = false;

/* Fields are iterated rather than values because an obsolete alias shares its
* value with a current group, and the value's name could be either. */
foreach (FieldInfo field in typeof(BuildTargetGroup).GetFields(BindingFlags.Public | BindingFlags.Static))
{
if (field.IsDefined(typeof(System.ObsoleteAttribute), inherit: false))
continue;
BuildTargetGroup group = (BuildTargetGroup)field.GetValue(null);
if (group == BuildTargetGroup.Unknown)
continue;

NamedBuildTarget target;
try
{
target = NamedBuildTarget.FromBuildTargetGroup(group);
}
catch (System.ArgumentException)
{
continue;
}

if (visitedTargets.Add(target.TargetName))
modified |= RemoveOrAddDefine(target, define, removeDefine);
}

// Dedicated server builds have their own defines, which no BuildTargetGroup maps to.
if (visitedTargets.Add(NamedBuildTarget.Server.TargetName))
modified |= RemoveOrAddDefine(NamedBuildTarget.Server, define, removeDefine);

return modified;
}

/// <summary>
/// Adds or removes a define for a build target.
/// </summary>
private static bool RemoveOrAddDefine(NamedBuildTarget target, string define, bool removeDefine)
{
string currentDefines = PlayerSettings.GetScriptingDefineSymbols(target);
HashSet<string> definesHs = new(currentDefines.Split(new[] { ';' }, System.StringSplitOptions.RemoveEmptyEntries));

bool modified = removeDefine ? definesHs.Remove(define) : definesHs.Add(define);
if (modified)
PlayerSettings.SetScriptingDefineSymbols(target, string.Join(";", definesHs));

return modified;
}
}

public class RefreshDefaultPrefabsMenu : MonoBehaviour
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,34 @@ public abstract partial class NetworkBehaviour : MonoBehaviour
/// </summary>
/// <remarks>This is only used by prediction.</remarks>
private TransformProperties _lastCheckedTransformProperties;
/// <summary>
/// Tick the last full reconcile was written on. Used by delta prediction.
/// </summary>
private uint _lastFullReconcileWriteTick = TimeManager.UNSET_TICK;
/// <summary>
/// Identifies the last full reconcile written. Used by delta prediction.
/// </summary>
private byte _fullReconcileWriteId;
/// <summary>
/// Last full reconcile read, which delta reconciles are applied to. Null when there is none. Used by delta prediction.
/// </summary>
/// <remarks>This is an object because the reconcile type is not known to NetworkBehaviour; it is only set on full reconciles.</remarks>
private object _fullReconcileRead;
/// <summary>
/// Identifies the last full reconcile read. Used by delta prediction.
/// </summary>
private byte _fullReconcileReadId;
/// <summary>
/// Server tick the last full reconcile was read for. Used by delta prediction.
/// </summary>
private uint _fullReconcileReadServerTick;
#endregion

#region Const.
/// <summary>
/// Most ticks between full reconciles. Used by delta prediction.
/// </summary>
private const uint MAXIMUM_FULL_RECONCILE_INTERVAL = 60;
#endregion

/// <summary>
Expand Down Expand Up @@ -364,6 +392,10 @@ public virtual void ClearReplicateCache() { }
replicatesHistory.Clear();

ClearReconcileHistory(reconcilesHistory, uint.MaxValue);

// Delta reconciles cannot continue from reconciles written or read before the cache was cleared.
_lastFullReconcileWriteTick = TimeManager.UNSET_TICK;
_fullReconcileRead = null;
}

/// <summary>
Expand Down Expand Up @@ -406,12 +438,12 @@ private void Server_SendReconcileRpc<T>(uint hash, ref T lastReconcileData, T re
*
* The exception is for the owner, which we send the last replicate
* tick so the owner knows which to roll back to. */
#if DO_NOT_USE
methodWriter.WriteDeltaReconcile(lastReconcileData, reconcileData, GetDeltaSerializeOption());
#if FISHNET_DELTA_PREDICTION
Reconcile_WriteDelta(methodWriter, ref lastReconcileData, reconcileData);
#else
methodWriter.WriteReconcile<T>(reconcileData);
#endif
lastReconcileData = reconcileData;
#endif

PooledWriter writer;
#if DEVELOPMENT
Expand Down Expand Up @@ -458,6 +490,45 @@ private void Server_SendReconcileRpc<T>(uint hash, ref T lastReconcileData, T re
writer.Store();
}

/// <summary>
/// Writes a reconcile using delta prediction.
/// </summary>
/// <param name = "lastReconcileData">Last full reconcile written, as readers decoded it. Deltas are written against this value.</param>
private void Reconcile_WriteDelta<T>(PooledWriter methodWriter, ref T lastReconcileData, T reconcileData) where T : IReconcileData
{
/* Deltas are written against the last full reconcile rather than the previous
* reconcile. Reconciles are sent unreliably and may also be dropped unread, such as
* when reconciles are reduced with framerate. Against the previous reconcile, every
* loss would leave readers unable to use any delta until the next full reconcile;
* against the last full reconcile, a lost delta costs only itself. */
bool fullSerialize = GetDeltaSerializeOption() == DeltaSerializerOption.FullSerialize;
if (fullSerialize)
{
_lastFullReconcileWriteTick = _networkObjectCache.TimeManager.LocalTick;
_fullReconcileWriteId++;
}

int startPosition = methodWriter.Position;
methodWriter.WriteDeltaReconcile(lastReconcileData, reconcileData, _fullReconcileWriteId, fullSerialize);

if (!Writer.HasDeltaSerializers<T>())
{
lastReconcileData = reconcileData;
return;
}
if (!fullSerialize)
return;

/* Readers apply deltas to the value they decoded from the full reconcile, which
* differs from reconcileData if the serializer is lossy. Decode what was written
* and write deltas against that. */
ArraySegment<byte> written = methodWriter.GetArraySegment();
written = new(written.Array, written.Offset + startPosition, written.Count - startPosition);
PooledReader reader = ReaderPool.Retrieve(written, _networkObjectCache.NetworkManager, Reader.DataSource.Server);
lastReconcileData = reader.ReadDeltaReconcile(lastReconcileData, out _, out _);
ReaderPool.Store(reader);
}

/// <summary>
/// Returns if there is a chance the transform may change after the tick.
/// </summary>
Expand Down Expand Up @@ -844,32 +915,30 @@ internal virtual void EmptyReplicatesQueueIntoHistory_Start() { }
}

/// <summary>
/// Returns the DeltaSerializeOption to use for the tick.
/// Returns the DeltaSerializeOption to use for the next reconcile written.
/// </summary>
/// <param name = "resendsEnded"></param>
/// <returns></returns>
private DeltaSerializerOption GetDeltaSerializeOption()
{
//Everything below this is not yet used.
return DeltaSerializerOption.FullSerialize;
//
// uint localTick = _networkObjectCache.TimeManager.LocalTick;
// ushort tickRate = _networkObjectCache.TimeManager.TickRate;
// /* New observers so send a full serialize next replicate.
// * This could go out to only the newly added observers, but it
// * would generate a lot more complexity to save presumably
// * a small amount of occasional bandwidth. */
// if (_networkObjectCache.ObserverAddedTick == localTick)
// return DeltaSerializerOption.FullSerialize;
// //Send full every half a second.
// if (localTick % tickRate == 0 || localTick % (tickRate / 2) == 0)
// return DeltaSerializerOption.FullSerialize;
// //Send full every second.
// if (localTick % tickRate == 0)
// return DeltaSerializerOption.FullSerialize;
// //Otherwise return rootSerialize, the default for sending the child most data.
//
// return DeltaSerializerOption.RootSerialize;
uint localTick = _networkObjectCache.TimeManager.LocalTick;
ushort tickRate = _networkObjectCache.TimeManager.TickRate;
/* Send full when none has been written, and then every second so a reader
* which lost one recovers. Also at most MAXIMUM_FULL_RECONCILE_INTERVAL apart
* so readers can tell full reconciles apart; see Reconcile_ReadDelta. */
uint fullInterval = Math.Min((uint)tickRate, MAXIMUM_FULL_RECONCILE_INTERVAL);
if (_lastFullReconcileWriteTick == TimeManager.UNSET_TICK || localTick - _lastFullReconcileWriteTick >= fullInterval)
return DeltaSerializerOption.FullSerialize;
/* New observers so send a full serialize next reconcile.
* This could go out to only the newly added observers, but it
* would generate a lot more complexity to save presumably
* a small amount of occasional bandwidth.
*
* Compared against the last full write rather than the current tick so
* an observer added on a tick nothing was written still receives one. */
if (_networkObjectCache.ObserverAddedTick >= _lastFullReconcileWriteTick)
return DeltaSerializerOption.FullSerialize;
//Otherwise return rootSerialize, the default for sending the child most data.
return DeltaSerializerOption.RootSerialize;
}

/// <summary>
Expand Down Expand Up @@ -904,8 +973,8 @@ private DeltaSerializerOption GetDeltaSerializeOption()
* write the queueTick. */
if (!toServer)
methodWriter.WriteTickUnpacked(queuedTick);
#if DO_NOT_USE
methodWriter.WriteDeltaReplicate(replicatesHistory, offset, deltaOption);
#if FISHNET_DELTA_PREDICTION
methodWriter.WriteDeltaReplicate(replicatesHistory, offset);
#else
methodWriter.WriteReplicate<T>(replicatesHistory, offset);
#endif
Expand Down Expand Up @@ -987,8 +1056,8 @@ private DeltaSerializerOption GetDeltaSerializeOption()
else
tick = tm.LastPacketTick.LastRemoteTick;

#if DO_NOT_USE
receivedReplicatesCount = reader.ReadDeltaReplicate(lastReadReplicate, ref arrBuffer, tick);
#if FISHNET_DELTA_PREDICTION
List<ReplicateDataContainer<T>> readReplicates = reader.ReadDeltaReplicate<T>(tick);
#else
List<ReplicateDataContainer<T>> readReplicates = reader.ReadReplicate<T>(tick);
#endif
Expand Down Expand Up @@ -1077,8 +1146,8 @@ private DeltaSerializerOption GetDeltaSerializeOption()
methodWriter.WriteTickUnpacked(runTickOflastEntry);
//Write the replicates.
int redundancyCount = (int)Mathf.Min(_networkObjectCache.PredictionManager.RedundancyCount, queueCount);
#if DO_NOT_USE
methodWriter.WriteDeltaReplicate(replicatesQueue, redundancyCount, GetDeltaSerializeOption());
#if FISHNET_DELTA_PREDICTION
methodWriter.WriteDeltaReplicate(replicatesQueue, redundancyCount);
#else
methodWriter.WriteReplicate<T>(replicatesQueue, redundancyCount);
#endif
Expand Down Expand Up @@ -1465,8 +1534,9 @@ private void ClearReconcileHistory<T>(RingBuffer<LocalReconcile<T>> reconcilesHi
public void Reconcile_Reader_Remote<T>(PooledReader reader, ref T lastReconcileData) where T : IReconcileData
{
uint tick = IsOwner ? PredictionManager.ClientStateTick : PredictionManager.ServerStateTick;
#if DO_NOT_USE
T newData = reader.ReadDeltaReconcile(lastReconciledata);
#if FISHNET_DELTA_PREDICTION
if (!Reconcile_ReadDelta(reader, out T newData))
return;
#else
T newData = reader.ReadReconcile<T>();
#endif
Expand All @@ -1487,6 +1557,37 @@ public void Reconcile_Reader_Remote<T>(PooledReader reader, ref T lastReconcileD
_lastReadReconcileRemoteTick = tick;
}

/// <summary>
/// Reads a reconcile written by Reconcile_WriteDelta.
/// </summary>
/// <returns>False if the reconcile is a delta against a full reconcile this reader does not hold, in which case it must not be used.</returns>
private bool Reconcile_ReadDelta<T>(PooledReader reader, out T newData) where T : IReconcileData
{
T fullReconcile = _fullReconcileRead is T value ? value : default;
newData = reader.ReadDeltaReconcile(fullReconcile, out bool isFull, out byte fullReconcileId);

//Types without delta serializers are always read in full.
if (!Writer.HasDeltaSerializers<T>())
return true;

uint serverTick = PredictionManager.ServerStateTick;
if (isFull)
{
_fullReconcileRead = newData;
_fullReconcileReadId = fullReconcileId;
_fullReconcileReadServerTick = serverTick;
return true;
}

/* A delta can only be used with the full reconcile it was written against; until
* a newer full reconcile arrives, deltas against one which was lost are discarded
* as if they had been lost too. The id wraps, so the tick check keeps a full
* reconcile from matching one written a whole id range later: at most one reconcile
* is written per tick, and full reconciles are written at most
* MAXIMUM_FULL_RECONCILE_INTERVAL apart, doubled here for send timing. */
return _fullReconcileRead != null && fullReconcileId == _fullReconcileReadId && serverTick - _fullReconcileReadServerTick <= MAXIMUM_FULL_RECONCILE_INTERVAL * 2;
}

/// <summary>
/// Reads a local reconcile from the client.
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion Assets/FishNet/Runtime/Serializing/GenericReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public static void SetRead(Func<Reader, T> value)
bool isGenerated = value.Method.Name.StartsWith(UtilityConstants.GeneratedReaderPrefix);

//If not generated then unset any generated delta serializer.
if (!isGenerated && GenericDeltaReader<T>.HasCustomSerializer)
if (!isGenerated && !GenericDeltaReader<T>.HasCustomSerializer)
GenericDeltaReader<T>.Read = null;

//Set has custom serializer if value being used is not a generated method.
Expand Down
2 changes: 1 addition & 1 deletion Assets/FishNet/Runtime/Serializing/GenericWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public static void SetWrite(Action<Writer, T> value)
bool isGenerated = value.Method.Name.StartsWith(UtilityConstants.GeneratedWriterPrefix);

// If not generated then unset any generated delta serializer.
if (!isGenerated && GenericDeltaWriter<T>.HasCustomSerializer)
if (!isGenerated && !GenericDeltaWriter<T>.HasCustomSerializer)
GenericDeltaWriter<T>.Write = null;

// Set has custom serializer if value being used is not a generated method.
Expand Down
Loading