From d92cc65315707d1c6af563bd0c6d9a4ea24f8371 Mon Sep 17 00:00:00 2001 From: MrGadget1024 <9826063+MrGadget1024@users.noreply.github.com> Date: Fri, 24 Feb 2023 03:38:18 -0500 Subject: [PATCH] Formatting --- .../Core/NetworkClient_TimeInterpolation.cs | 294 +- .../Mirror/Core/NetworkConnectionToClient.cs | 428 +- Assets/Mirror/Core/NetworkManager.cs | 2672 ++++++------- Assets/Mirror/Core/NetworkServer.cs | 3504 ++++++++--------- .../SnapshotInterpolation.cs | 636 +-- .../Snapshot Interpolation/ClientCube.cs | 346 +- .../Editor/SnapshotInterpolationTests.cs | 50 +- 7 files changed, 3965 insertions(+), 3965 deletions(-) diff --git a/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs index a6d8e594c..a533a8ef2 100644 --- a/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs +++ b/Assets/Mirror/Core/NetworkClient_TimeInterpolation.cs @@ -3,180 +3,180 @@ namespace Mirror { - public static partial class NetworkClient - { - // TODO expose the settings to the user later. - // via NetMan or NetworkClientConfig or NetworkClient as component etc. + public static partial class NetworkClient + { + // TODO expose the settings to the user later. + // via NetMan or NetworkClientConfig or NetworkClient as component etc. - // 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("Snapshot Interpolation: 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 static double bufferTimeMultiplier = 2; - public static double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier; + // 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("Snapshot Interpolation: 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 static double bufferTimeMultiplier = 2; + public static double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier; - // - public static SortedList snapshots = new SortedList(); + // + public static SortedList snapshots = new SortedList(); - // for smooth interpolation, we need to interpolate along server time. - // any other time (arrival on client, client local time, etc.) is not - // going to give smooth results. - // in other words, this is the remote server's time, but adjusted. - // - // internal for use from NetworkTime. - // double for long running servers, see NetworkTime comments. - internal static double localTimeline; + // for smooth interpolation, we need to interpolate along server time. + // any other time (arrival on client, client local time, etc.) is not + // going to give smooth results. + // in other words, this is the remote server's time, but adjusted. + // + // internal for use from NetworkTime. + // double for long running servers, see NetworkTime comments. + internal static double localTimeline; - // catchup / slowdown adjustments are applied to timescale, - // to be adjusted in every update instead of when receiving messages. - internal static double localTimescale = 1; + // catchup / slowdown adjustments are applied to timescale, + // to be adjusted in every update instead of when receiving messages. + internal static double localTimescale = 1; - // catchup ///////////////////////////////////////////////////////////// - // catchup thresholds in 'frames'. - // half a frame might be too aggressive. - [Header("Snapshot Interpolation: 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 static float catchupNegativeThreshold = -1; // careful, don't want to run out of snapshots + // catchup ///////////////////////////////////////////////////////////// + // catchup thresholds in 'frames'. + // half a frame might be too aggressive. + [Header("Snapshot Interpolation: 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 static 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 static float catchupPositiveThreshold = 1; + [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 static float catchupPositiveThreshold = 1; - [Tooltip("Local timeline acceleration in % while catching up.")] - [Range(0, 1)] - public static double catchupSpeed = 0.01f; // 1% + [Tooltip("Local timeline acceleration in % while catching up.")] + [Range(0, 1)] + public static double catchupSpeed = 0.01f; // 1% - [Tooltip("Local timeline slowdown in % while slowing down.")] - [Range(0, 1)] - public static double slowdownSpeed = 0.01f; // 1% + [Tooltip("Local timeline slowdown in % while slowing down.")] + [Range(0, 1)] + public static double slowdownSpeed = 0.01f; // 1% - [Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")] - public static int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway + [Tooltip("Catchup/Slowdown is adjusted over n-second exponential moving average.")] + public static int driftEmaDuration = 1; // shouldn't need to modify this, but expose it anyway - // we use EMA to average the last second worth of snapshot time diffs. - // manually averaging the last second worth of values with a for loop - // would be the same, but a moving average is faster because we only - // ever add one value. - static ExponentialMovingAverage driftEma; + // we use EMA to average the last second worth of snapshot time diffs. + // manually averaging the last second worth of values with a for loop + // would be the same, but a moving average is faster because we only + // ever add one value. + static ExponentialMovingAverage driftEma; - // 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("Snapshot Interpolation: Dynamic Adjustment")] - [Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")] - public static bool dynamicAdjustment = true; + // 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("Snapshot Interpolation: Dynamic Adjustment")] + [Tooltip("Automatically adjust bufferTimeMultiplier for smooth results.\nSets a low multiplier on stable connections, and a high multiplier on jittery connections.")] + public static bool dynamicAdjustment = true; - [Tooltip("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")] - public static 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("Safety buffer that is always added to the dynamic bufferTimeMultiplier adjustment.")] + public static 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 static int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time - static ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter) + [Tooltip("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")] + public static int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time + static ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter) - // OnValidate: see NetworkClient.cs - // add snapshot & initialize client interpolation time if needed + // OnValidate: see NetworkClient.cs + // add snapshot & initialize client interpolation time if needed - // initialization called from Awake - static void InitTimeInterpolation() - { - // reset timeline, localTimescale & snapshots from last session (if any) - // Don't reset bufferTimeMultiplier here - whatever their network condition - // was when they disconnected, it won't have changed on immediate reconnect. - localTimeline = 0; - localTimescale = 1; - snapshots.Clear(); + // initialization called from Awake + static void InitTimeInterpolation() + { + // reset timeline, localTimescale & snapshots from last session (if any) + // Don't reset bufferTimeMultiplier here - whatever their network condition + // was when they disconnected, it won't have changed on immediate reconnect. + localTimeline = 0; + localTimescale = 1; + snapshots.Clear(); - // initialize EMA with 'emaDuration' seconds worth of history. - // 1 second holds 'sendRate' worth of values. - // multiplied by emaDuration gives n-seconds. - driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * driftEmaDuration); - deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * deliveryTimeEmaDuration); - } + // initialize EMA with 'emaDuration' seconds worth of history. + // 1 second holds 'sendRate' worth of values. + // multiplied by emaDuration gives n-seconds. + driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * driftEmaDuration); + deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * deliveryTimeEmaDuration); + } - // server sends TimeSnapshotMessage every sendInterval. - // batching already includes the remoteTimestamp. - // we simply insert it on-message here. - // => only for reliable channel. unreliable would always arrive earlier. - static void OnTimeSnapshotMessage(TimeSnapshotMessage _) - { - // insert another snapshot for snapshot interpolation. - // before calling OnDeserialize so components can use - // NetworkTime.time and NetworkTime.timeStamp. + // server sends TimeSnapshotMessage every sendInterval. + // batching already includes the remoteTimestamp. + // we simply insert it on-message here. + // => only for reliable channel. unreliable would always arrive earlier. + static void OnTimeSnapshotMessage(TimeSnapshotMessage _) + { + // insert another snapshot for snapshot interpolation. + // before calling OnDeserialize so components can use + // NetworkTime.time and NetworkTime.timeStamp. #if !UNITY_2020_3_OR_NEWER // Unity 2019 doesn't have Time.timeAsDouble yet OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime)); #else - OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, Time.timeAsDouble)); + OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, Time.timeAsDouble)); #endif - } + } - // see comments at the top of this file - public static void OnTimeSnapshot(TimeSnapshot snap) - { - // Debug.Log($"NetworkClient: OnTimeSnapshot @ {snap.remoteTime:F3}"); + // see comments at the top of this file + public static void OnTimeSnapshot(TimeSnapshot snap) + { + // Debug.Log($"NetworkClient: OnTimeSnapshot @ {snap.remoteTime:F3}"); - // (optional) dynamic adjustment - if (dynamicAdjustment) - { - // set bufferTime on the fly. - // shows in inspector for easier debugging :) - bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment( - NetworkServer.sendInterval, - deliveryTimeEma.StandardDeviation, - dynamicAdjustmentTolerance - ); - } + // (optional) dynamic adjustment + if (dynamicAdjustment) + { + // set bufferTime on the fly. + // shows in inspector for easier debugging :) + bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment( + NetworkServer.sendInterval, + deliveryTimeEma.StandardDeviation, + dynamicAdjustmentTolerance + ); + } - // insert into the buffer & initialize / adjust / catchup - SnapshotInterpolation.InsertAndAdjust( - snapshots, - snap, - ref localTimeline, - ref localTimescale, - NetworkServer.sendInterval, - bufferTime, - NetworkServer.bufferTimeMultiplierForClamping, - catchupSpeed, - slowdownSpeed, - ref driftEma, - catchupNegativeThreshold, - catchupPositiveThreshold, - ref deliveryTimeEma); + // insert into the buffer & initialize / adjust / catchup + SnapshotInterpolation.InsertAndAdjust( + snapshots, + snap, + ref localTimeline, + ref localTimescale, + NetworkServer.sendInterval, + bufferTime, + NetworkServer.bufferTimeMultiplierForClamping, + catchupSpeed, + slowdownSpeed, + ref driftEma, + catchupNegativeThreshold, + catchupPositiveThreshold, + ref deliveryTimeEma); - // Debug.Log($"inserted TimeSnapshot remote={snap.remoteTime:F2} local={snap.localTime:F2} total={snapshots.Count}"); - } + // Debug.Log($"inserted TimeSnapshot remote={snap.remoteTime:F2} local={snap.localTime:F2} total={snapshots.Count}"); + } - // call this from early update, so the timeline is safe to use in update - static void UpdateTimeInterpolation() - { - // only while we have snapshots. - // timeline starts when the first snapshot arrives. - if (snapshots.Count > 0) - { - // progress local timeline. - SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref localTimeline, localTimescale); + // call this from early update, so the timeline is safe to use in update + static void UpdateTimeInterpolation() + { + // only while we have snapshots. + // timeline starts when the first snapshot arrives. + if (snapshots.Count > 0) + { + // progress local timeline. + SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref localTimeline, localTimescale); - // progress local interpolation. - // TimeSnapshot doesn't interpolate anything. - // this is merely to keep removing older snapshots. - SnapshotInterpolation.StepInterpolation(snapshots, localTimeline, out _, out _, out double t); - // Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}"); - } - } - } + // progress local interpolation. + // TimeSnapshot doesn't interpolate anything. + // this is merely to keep removing older snapshots. + SnapshotInterpolation.StepInterpolation(snapshots, localTimeline, out _, out _, out double t); + // Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}"); + } + } + } } diff --git a/Assets/Mirror/Core/NetworkConnectionToClient.cs b/Assets/Mirror/Core/NetworkConnectionToClient.cs index 17f7c227f..39a38bc40 100644 --- a/Assets/Mirror/Core/NetworkConnectionToClient.cs +++ b/Assets/Mirror/Core/NetworkConnectionToClient.cs @@ -5,253 +5,253 @@ namespace Mirror { - public class NetworkConnectionToClient : NetworkConnection - { - // rpcs are collected in a buffer, and then flushed out together. - // this way we don't need one NetworkMessage per rpc. - // => prepares for LocalWorldState as well. - // ensure max size when adding! - readonly NetworkWriter reliableRpcs = new NetworkWriter(); - readonly NetworkWriter unreliableRpcs = new NetworkWriter(); + public class NetworkConnectionToClient : NetworkConnection + { + // rpcs are collected in a buffer, and then flushed out together. + // this way we don't need one NetworkMessage per rpc. + // => prepares for LocalWorldState as well. + // ensure max size when adding! + readonly NetworkWriter reliableRpcs = new NetworkWriter(); + readonly NetworkWriter unreliableRpcs = new NetworkWriter(); - public override string address => - Transport.active.ServerGetClientAddress(connectionId); + public override string address => + Transport.active.ServerGetClientAddress(connectionId); - /// NetworkIdentities that this connection can see - // TODO move to server's NetworkConnectionToClient? - public readonly HashSet observing = new HashSet(); + /// NetworkIdentities that this connection can see + // TODO move to server's NetworkConnectionToClient? + public readonly HashSet observing = new HashSet(); - // Deprecated 2022-10-13 - [Obsolete(".clientOwnedObjects was renamed to .owned :)")] - public HashSet clientOwnedObjects => owned; + // Deprecated 2022-10-13 + [Obsolete(".clientOwnedObjects was renamed to .owned :)")] + public HashSet clientOwnedObjects => owned; - // unbatcher - public Unbatcher unbatcher = new Unbatcher(); + // unbatcher + public Unbatcher unbatcher = new Unbatcher(); - // server runs a time snapshot interpolation for each client's local time. - // this is necessary for client auth movement to still be smooth on the - // server for host mode. - // TODO move them along server's timeline in the future. - // perhaps with an offset. - // for now, keep compatibility by manually constructing a timeline. - ExponentialMovingAverage driftEma; - ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter) - public double remoteTimeline; - public double remoteTimescale; - double bufferTimeMultiplier = 2; - double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier; + // server runs a time snapshot interpolation for each client's local time. + // this is necessary for client auth movement to still be smooth on the + // server for host mode. + // TODO move them along server's timeline in the future. + // perhaps with an offset. + // for now, keep compatibility by manually constructing a timeline. + ExponentialMovingAverage driftEma; + ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter) + public double remoteTimeline; + public double remoteTimescale; + double bufferTimeMultiplier = 2; + double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier; - // - readonly SortedList snapshots = new SortedList(); + // + readonly SortedList snapshots = new SortedList(); - // Snapshot Buffer size limit to avoid ever growing list memory consumption attacks from clients. - public int snapshotBufferSizeLimit = 64; + // Snapshot Buffer size limit to avoid ever growing list memory consumption attacks from clients. + public int snapshotBufferSizeLimit = 64; - public NetworkConnectionToClient(int networkConnectionId) - : base(networkConnectionId) - { - // initialize EMA with 'emaDuration' seconds worth of history. - // 1 second holds 'sendRate' worth of values. - // multiplied by emaDuration gives n-seconds. - driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.driftEmaDuration); - deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.deliveryTimeEmaDuration); + public NetworkConnectionToClient(int networkConnectionId) + : base(networkConnectionId) + { + // initialize EMA with 'emaDuration' seconds worth of history. + // 1 second holds 'sendRate' worth of values. + // multiplied by emaDuration gives n-seconds. + driftEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.driftEmaDuration); + deliveryTimeEma = new ExponentialMovingAverage(NetworkServer.sendRate * NetworkClient.deliveryTimeEmaDuration); - // buffer limit should be at least multiplier to have enough in there - snapshotBufferSizeLimit = Mathf.Max((int)NetworkClient.bufferTimeMultiplier, snapshotBufferSizeLimit); - } + // buffer limit should be at least multiplier to have enough in there + snapshotBufferSizeLimit = Mathf.Max((int)NetworkClient.bufferTimeMultiplier, snapshotBufferSizeLimit); + } - public void OnTimeSnapshot(TimeSnapshot snapshot) - { - // protect against ever growing buffer size attacks - if (snapshots.Count >= snapshotBufferSizeLimit) return; + public void OnTimeSnapshot(TimeSnapshot snapshot) + { + // protect against ever growing buffer size attacks + if (snapshots.Count >= snapshotBufferSizeLimit) return; - // (optional) dynamic adjustment - if (NetworkClient.dynamicAdjustment) - { - // set bufferTime on the fly. - // shows in inspector for easier debugging :) - bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment( - NetworkServer.sendInterval, - deliveryTimeEma.StandardDeviation, - NetworkClient.dynamicAdjustmentTolerance - ); - // Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} "); - } + // (optional) dynamic adjustment + if (NetworkClient.dynamicAdjustment) + { + // set bufferTime on the fly. + // shows in inspector for easier debugging :) + bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment( + NetworkServer.sendInterval, + deliveryTimeEma.StandardDeviation, + NetworkClient.dynamicAdjustmentTolerance + ); + // Debug.Log($"[Server]: {name} delivery std={serverDeliveryTimeEma.StandardDeviation} bufferTimeMult := {bufferTimeMultiplier} "); + } - // insert into the server buffer & initialize / adjust / catchup - SnapshotInterpolation.InsertAndAdjust( - snapshots, - snapshot, - ref remoteTimeline, - ref remoteTimescale, - NetworkServer.sendInterval, - bufferTime, - NetworkServer.bufferTimeMultiplierForClamping, - NetworkClient.catchupSpeed, - NetworkClient.slowdownSpeed, - ref driftEma, - NetworkClient.catchupNegativeThreshold, - NetworkClient.catchupPositiveThreshold, - ref deliveryTimeEma - ); - } + // insert into the server buffer & initialize / adjust / catchup + SnapshotInterpolation.InsertAndAdjust( + snapshots, + snapshot, + ref remoteTimeline, + ref remoteTimescale, + NetworkServer.sendInterval, + bufferTime, + NetworkServer.bufferTimeMultiplierForClamping, + NetworkClient.catchupSpeed, + NetworkClient.slowdownSpeed, + ref driftEma, + NetworkClient.catchupNegativeThreshold, + NetworkClient.catchupPositiveThreshold, + ref deliveryTimeEma + ); + } - public void UpdateTimeInterpolation() - { - // timeline starts when the first snapshot arrives. - if (snapshots.Count > 0) - { - // progress local timeline. - SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref remoteTimeline, remoteTimescale); + public void UpdateTimeInterpolation() + { + // timeline starts when the first snapshot arrives. + if (snapshots.Count > 0) + { + // progress local timeline. + SnapshotInterpolation.StepTime(Time.unscaledDeltaTime, ref remoteTimeline, remoteTimescale); - // progress local interpolation. - // TimeSnapshot doesn't interpolate anything. - // this is merely to keep removing older snapshots. - SnapshotInterpolation.StepInterpolation(snapshots, remoteTimeline, out _, out _, out _); - // Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}"); - } - } + // progress local interpolation. + // TimeSnapshot doesn't interpolate anything. + // this is merely to keep removing older snapshots. + SnapshotInterpolation.StepInterpolation(snapshots, remoteTimeline, out _, out _, out _); + // Debug.Log($"NetworkClient SnapshotInterpolation @ {localTimeline:F2} t={t:F2}"); + } + } - // Send stage three: hand off to transport - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected override void SendToTransport(ArraySegment segment, int channelId = Channels.Reliable) => - Transport.active.ServerSend(connectionId, segment, channelId); + // Send stage three: hand off to transport + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected override void SendToTransport(ArraySegment segment, int channelId = Channels.Reliable) => + Transport.active.ServerSend(connectionId, segment, channelId); - void FlushRpcs(NetworkWriter buffer, int channelId) - { - if (buffer.Position > 0) - { - Send(new RpcBufferMessage{ payload = buffer }, channelId); - buffer.Position = 0; - } - } + void FlushRpcs(NetworkWriter buffer, int channelId) + { + if (buffer.Position > 0) + { + Send(new RpcBufferMessage { payload = buffer }, channelId); + buffer.Position = 0; + } + } - // helper for both channels - void BufferRpc(RpcMessage message, NetworkWriter buffer, int channelId, int maxMessageSize) - { - // calculate buffer limit. we can only fit so much into a message. - // max - message header - WriteArraySegment size header - batch header - int bufferLimit = maxMessageSize - NetworkMessages.IdSize - sizeof(int) - Batcher.HeaderSize; + // helper for both channels + void BufferRpc(RpcMessage message, NetworkWriter buffer, int channelId, int maxMessageSize) + { + // calculate buffer limit. we can only fit so much into a message. + // max - message header - WriteArraySegment size header - batch header + int bufferLimit = maxMessageSize - NetworkMessages.IdSize - sizeof(int) - Batcher.HeaderSize; - // remember previous valid position - int before = buffer.Position; + // remember previous valid position + int before = buffer.Position; - // serialize the message without header - buffer.Write(message); + // serialize the message without header + buffer.Write(message); - // before we potentially flush out old messages, - // let's ensure this single message can even fit the limit. - // otherwise no point in flushing. - int messageSize = buffer.Position - before; - if (messageSize > bufferLimit) - { - Debug.LogWarning($"NetworkConnectionToClient: discarded RpcMesage for netId={message.netId} componentIndex={message.componentIndex} functionHash={message.functionHash} because it's larger than the rpc buffer limit of {bufferLimit} bytes for the channel: {channelId}"); - return; - } + // before we potentially flush out old messages, + // let's ensure this single message can even fit the limit. + // otherwise no point in flushing. + int messageSize = buffer.Position - before; + if (messageSize > bufferLimit) + { + Debug.LogWarning($"NetworkConnectionToClient: discarded RpcMesage for netId={message.netId} componentIndex={message.componentIndex} functionHash={message.functionHash} because it's larger than the rpc buffer limit of {bufferLimit} bytes for the channel: {channelId}"); + return; + } - // too much to fit into max message size? - // then flush first, then write it again. - // (message + message header + 4 bytes WriteArraySegment header) - if (buffer.Position > bufferLimit) - { - buffer.Position = before; - FlushRpcs(buffer, channelId); // this resets position - buffer.Write(message); - } - } + // too much to fit into max message size? + // then flush first, then write it again. + // (message + message header + 4 bytes WriteArraySegment header) + if (buffer.Position > bufferLimit) + { + buffer.Position = before; + FlushRpcs(buffer, channelId); // this resets position + buffer.Write(message); + } + } - internal void BufferRpc(RpcMessage message, int channelId) - { - int maxMessageSize = Transport.active.GetMaxPacketSize(channelId); - if (channelId == Channels.Reliable) - { - BufferRpc(message, reliableRpcs, Channels.Reliable, maxMessageSize); - } - else if (channelId == Channels.Unreliable) - { - BufferRpc(message, unreliableRpcs, Channels.Unreliable, maxMessageSize); - } - } + internal void BufferRpc(RpcMessage message, int channelId) + { + int maxMessageSize = Transport.active.GetMaxPacketSize(channelId); + if (channelId == Channels.Reliable) + { + BufferRpc(message, reliableRpcs, Channels.Reliable, maxMessageSize); + } + else if (channelId == Channels.Unreliable) + { + BufferRpc(message, unreliableRpcs, Channels.Unreliable, maxMessageSize); + } + } - internal override void Update() - { - // send rpc buffers - FlushRpcs(reliableRpcs, Channels.Reliable); - FlushRpcs(unreliableRpcs, Channels.Unreliable); + internal override void Update() + { + // send rpc buffers + FlushRpcs(reliableRpcs, Channels.Reliable); + FlushRpcs(unreliableRpcs, Channels.Unreliable); - // call base update to flush out batched messages - base.Update(); - } + // call base update to flush out batched messages + base.Update(); + } - /// Disconnects this connection. - public override void Disconnect() - { - // set not ready and handle clientscene disconnect in any case - // (might be client or host mode here) - isReady = false; - reliableRpcs.Position = 0; - unreliableRpcs.Position = 0; - Transport.active.ServerDisconnect(connectionId); + /// Disconnects this connection. + public override void Disconnect() + { + // set not ready and handle clientscene disconnect in any case + // (might be client or host mode here) + isReady = false; + reliableRpcs.Position = 0; + unreliableRpcs.Position = 0; + Transport.active.ServerDisconnect(connectionId); - // IMPORTANT: NetworkConnection.Disconnect() is NOT called for - // voluntary disconnects from the other end. - // -> so all 'on disconnect' cleanup code needs to be in - // OnTransportDisconnect, where it's called for both voluntary - // and involuntary disconnects! - } + // IMPORTANT: NetworkConnection.Disconnect() is NOT called for + // voluntary disconnects from the other end. + // -> so all 'on disconnect' cleanup code needs to be in + // OnTransportDisconnect, where it's called for both voluntary + // and involuntary disconnects! + } - internal void AddToObserving(NetworkIdentity netIdentity) - { - observing.Add(netIdentity); + internal void AddToObserving(NetworkIdentity netIdentity) + { + observing.Add(netIdentity); - // spawn identity for this conn - NetworkServer.ShowForConnection(netIdentity, this); - } + // spawn identity for this conn + NetworkServer.ShowForConnection(netIdentity, this); + } - internal void RemoveFromObserving(NetworkIdentity netIdentity, bool isDestroyed) - { - observing.Remove(netIdentity); + internal void RemoveFromObserving(NetworkIdentity netIdentity, bool isDestroyed) + { + observing.Remove(netIdentity); - if (!isDestroyed) - { - // hide identity for this conn - NetworkServer.HideForConnection(netIdentity, this); - } - } + if (!isDestroyed) + { + // hide identity for this conn + NetworkServer.HideForConnection(netIdentity, this); + } + } - internal void RemoveFromObservingsObservers() - { - foreach (NetworkIdentity netIdentity in observing) - { - netIdentity.RemoveObserver(this); - } - observing.Clear(); - } + internal void RemoveFromObservingsObservers() + { + foreach (NetworkIdentity netIdentity in observing) + { + netIdentity.RemoveObserver(this); + } + observing.Clear(); + } - internal void AddOwnedObject(NetworkIdentity obj) - { - owned.Add(obj); - } + internal void AddOwnedObject(NetworkIdentity obj) + { + owned.Add(obj); + } - internal void RemoveOwnedObject(NetworkIdentity obj) - { - owned.Remove(obj); - } + internal void RemoveOwnedObject(NetworkIdentity obj) + { + owned.Remove(obj); + } - internal void DestroyOwnedObjects() - { - // create a copy because the list might be modified when destroying - HashSet tmp = new HashSet(owned); - foreach (NetworkIdentity netIdentity in tmp) - { - if (netIdentity != null) - { - NetworkServer.Destroy(netIdentity.gameObject); - } - } + internal void DestroyOwnedObjects() + { + // create a copy because the list might be modified when destroying + HashSet tmp = new HashSet(owned); + foreach (NetworkIdentity netIdentity in tmp) + { + if (netIdentity != null) + { + NetworkServer.Destroy(netIdentity.gameObject); + } + } - // clear the hashset because we destroyed them all - owned.Clear(); - } - } + // clear the hashset because we destroyed them all + owned.Clear(); + } + } } diff --git a/Assets/Mirror/Core/NetworkManager.cs b/Assets/Mirror/Core/NetworkManager.cs index 592685cbb..9ea9dc096 100644 --- a/Assets/Mirror/Core/NetworkManager.cs +++ b/Assets/Mirror/Core/NetworkManager.cs @@ -7,217 +7,217 @@ namespace Mirror { - public enum PlayerSpawnMethod { Random, RoundRobin } - public enum NetworkManagerMode { Offline, ServerOnly, ClientOnly, Host } + public enum PlayerSpawnMethod { Random, RoundRobin } + public enum NetworkManagerMode { Offline, ServerOnly, ClientOnly, Host } - [DisallowMultipleComponent] - [AddComponentMenu("Network/Network Manager")] - [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-manager")] - public class NetworkManager : MonoBehaviour - { - /// Enable to keep NetworkManager alive when changing scenes. - // This should be set if your game has a single NetworkManager that exists for the lifetime of the process. If there is a NetworkManager in each scene, then this should not be set. - [Header("Configuration")] - [FormerlySerializedAs("m_DontDestroyOnLoad")] - [Tooltip("Should the Network Manager object be persisted through scene changes?")] - public bool dontDestroyOnLoad = true; + [DisallowMultipleComponent] + [AddComponentMenu("Network/Network Manager")] + [HelpURL("https://mirror-networking.gitbook.io/docs/components/network-manager")] + public class NetworkManager : MonoBehaviour + { + /// Enable to keep NetworkManager alive when changing scenes. + // This should be set if your game has a single NetworkManager that exists for the lifetime of the process. If there is a NetworkManager in each scene, then this should not be set. + [Header("Configuration")] + [FormerlySerializedAs("m_DontDestroyOnLoad")] + [Tooltip("Should the Network Manager object be persisted through scene changes?")] + public bool dontDestroyOnLoad = true; - /// Multiplayer games should always run in the background so the network doesn't time out. - [FormerlySerializedAs("m_RunInBackground")] - [Tooltip("Multiplayer games should always run in the background so the network doesn't time out.")] - public bool runInBackground = true; + /// Multiplayer games should always run in the background so the network doesn't time out. + [FormerlySerializedAs("m_RunInBackground")] + [Tooltip("Multiplayer games should always run in the background so the network doesn't time out.")] + public bool runInBackground = true; - /// Should the server auto-start when 'Server Build' is checked in build settings - [Header("Headless Builds")] - [Tooltip("Should the server auto-start when 'Server Build' is checked in build settings")] - [FormerlySerializedAs("startOnHeadless")] - public bool autoStartServerBuild = true; + /// Should the server auto-start when 'Server Build' is checked in build settings + [Header("Headless Builds")] + [Tooltip("Should the server auto-start when 'Server Build' is checked in build settings")] + [FormerlySerializedAs("startOnHeadless")] + public bool autoStartServerBuild = true; - [Tooltip("Automatically connect the client in headless builds. Useful for CCU tests with bot clients.\n\nAddress may be passed as command line argument.\n\nMake sure that only 'autostartServer' or 'autoconnectClient' is enabled, not both!")] - public bool autoConnectClientBuild; + [Tooltip("Automatically connect the client in headless builds. Useful for CCU tests with bot clients.\n\nAddress may be passed as command line argument.\n\nMake sure that only 'autostartServer' or 'autoconnectClient' is enabled, not both!")] + public bool autoConnectClientBuild; - /// Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE. - [Tooltip("Server & Client send rate per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")] - [FormerlySerializedAs("serverTickRate")] - public int sendRate = 30; - - [Tooltip("Mirror tries to maintain 2x send interval (= 1 / Send Rate) time behind server/client. If we are way out of sync by a multiple of this buffer, we simply clamp time to within this buffer.")] - public float bufferTimeMultiplierForClamping = 2; + /// Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE. + [Tooltip("Server & Client send rate per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")] + [FormerlySerializedAs("serverTickRate")] + public int sendRate = 30; - // Deprecated 2022-10-31 - [Obsolete("NetworkManager.serverTickRate was renamed to sendRate because that's what it configures for both server & client now.")] - public int serverTickRate => sendRate; + [Tooltip("Mirror tries to maintain 2x send interval (= 1 / Send Rate) time behind server/client. If we are way out of sync by a multiple of this buffer, we simply clamp time to within this buffer.")] + public float bufferTimeMultiplierForClamping = 2; - // tick rate is in Hz. - // convert to interval in seconds for convenience where needed. - // - // send interval is 1 / sendRate. - // but for tests we need a way to set it to exactly 0. - // 1 / int.max would not be exactly 0, so handel that manually. - // Deprecated 2022-10-06 - [Obsolete("NetworkManager.serverTickInterval was moved to NetworkServer.tickInterval for consistency.")] - public float serverTickInterval => NetworkServer.tickInterval; + // Deprecated 2022-10-31 + [Obsolete("NetworkManager.serverTickRate was renamed to sendRate because that's what it configures for both server & client now.")] + public int serverTickRate => sendRate; - // client send rate follows server send rate to avoid errors for now - /// Client Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE. - // [Tooltip("Client broadcasts 'sendRate' times per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")] - // public int clientSendRate = 30; // 33 ms + // tick rate is in Hz. + // convert to interval in seconds for convenience where needed. + // + // send interval is 1 / sendRate. + // but for tests we need a way to set it to exactly 0. + // 1 / int.max would not be exactly 0, so handel that manually. + // Deprecated 2022-10-06 + [Obsolete("NetworkManager.serverTickInterval was moved to NetworkServer.tickInterval for consistency.")] + public float serverTickInterval => NetworkServer.tickInterval; - /// Automatically switch to this scene upon going offline (on start / on disconnect / on shutdown). - [Header("Scene Management")] - [Scene] - [FormerlySerializedAs("m_OfflineScene")] - [Tooltip("Scene that Mirror will switch to when the client or server is stopped")] - public string offlineScene = ""; + // client send rate follows server send rate to avoid errors for now + /// Client Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE. + // [Tooltip("Client broadcasts 'sendRate' times per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE.")] + // public int clientSendRate = 30; // 33 ms - /// Automatically switch to this scene upon going online (after connect/startserver). - [Scene] - [FormerlySerializedAs("m_OnlineScene")] - [Tooltip("Scene that Mirror will switch to when the server is started. Clients will recieve a Scene Message to load the server's current scene when they connect.")] - public string onlineScene = ""; + /// Automatically switch to this scene upon going offline (on start / on disconnect / on shutdown). + [Header("Scene Management")] + [Scene] + [FormerlySerializedAs("m_OfflineScene")] + [Tooltip("Scene that Mirror will switch to when the client or server is stopped")] + public string offlineScene = ""; - // transport layer - [Header("Network Info")] - [Tooltip("Transport component attached to this object that server and client will use to connect")] - public Transport transport; + /// Automatically switch to this scene upon going online (after connect/startserver). + [Scene] + [FormerlySerializedAs("m_OnlineScene")] + [Tooltip("Scene that Mirror will switch to when the server is started. Clients will recieve a Scene Message to load the server's current scene when they connect.")] + public string onlineScene = ""; - /// Server's address for clients to connect to. - [FormerlySerializedAs("m_NetworkAddress")] - [Tooltip("Network Address where the client should connect to the server. Server does not use this for anything.")] - public string networkAddress = "localhost"; + // transport layer + [Header("Network Info")] + [Tooltip("Transport component attached to this object that server and client will use to connect")] + public Transport transport; - /// The maximum number of concurrent network connections to support. - [FormerlySerializedAs("m_MaxConnections")] - [Tooltip("Maximum number of concurrent connections.")] - public int maxConnections = 100; + /// Server's address for clients to connect to. + [FormerlySerializedAs("m_NetworkAddress")] + [Tooltip("Network Address where the client should connect to the server. Server does not use this for anything.")] + public string networkAddress = "localhost"; - [Header("Authentication")] - [Tooltip("Authentication component attached to this object")] - public NetworkAuthenticator authenticator; + /// The maximum number of concurrent network connections to support. + [FormerlySerializedAs("m_MaxConnections")] + [Tooltip("Maximum number of concurrent connections.")] + public int maxConnections = 100; - /// The default prefab to be used to create player objects on the server. - // Player objects are created in the default handler for AddPlayer() on - // the server. Implementing OnServerAddPlayer overrides this behaviour. - [Header("Player Object")] - [FormerlySerializedAs("m_PlayerPrefab")] - [Tooltip("Prefab of the player object. Prefab must have a Network Identity component. May be an empty game object or a full avatar.")] - public GameObject playerPrefab; + [Header("Authentication")] + [Tooltip("Authentication component attached to this object")] + public NetworkAuthenticator authenticator; - /// Enable to automatically create player objects on connect and on scene change. - [FormerlySerializedAs("m_AutoCreatePlayer")] - [Tooltip("Should Mirror automatically spawn the player after scene change?")] - public bool autoCreatePlayer = true; + /// The default prefab to be used to create player objects on the server. + // Player objects are created in the default handler for AddPlayer() on + // the server. Implementing OnServerAddPlayer overrides this behaviour. + [Header("Player Object")] + [FormerlySerializedAs("m_PlayerPrefab")] + [Tooltip("Prefab of the player object. Prefab must have a Network Identity component. May be an empty game object or a full avatar.")] + public GameObject playerPrefab; - /// Where to spawn players. - [FormerlySerializedAs("m_PlayerSpawnMethod")] - [Tooltip("Round Robin or Random order of Start Position selection")] - public PlayerSpawnMethod playerSpawnMethod; + /// Enable to automatically create player objects on connect and on scene change. + [FormerlySerializedAs("m_AutoCreatePlayer")] + [Tooltip("Should Mirror automatically spawn the player after scene change?")] + public bool autoCreatePlayer = true; - /// Prefabs that can be spawned over the network need to be registered here. - [FormerlySerializedAs("m_SpawnPrefabs"), HideInInspector] - public List spawnPrefabs = new List(); + /// Where to spawn players. + [FormerlySerializedAs("m_PlayerSpawnMethod")] + [Tooltip("Round Robin or Random order of Start Position selection")] + public PlayerSpawnMethod playerSpawnMethod; - /// List of transforms populated by NetworkStartPositions - public static List startPositions = new List(); - public static int startPositionIndex; + /// Prefabs that can be spawned over the network need to be registered here. + [FormerlySerializedAs("m_SpawnPrefabs"), HideInInspector] + public List spawnPrefabs = new List(); - [Header("Debug")] - public bool timeInterpolationGui = false; + /// List of transforms populated by NetworkStartPositions + public static List startPositions = new List(); + public static int startPositionIndex; - /// The one and only NetworkManager - public static NetworkManager singleton { get; internal set; } + [Header("Debug")] + public bool timeInterpolationGui = false; - /// Number of active player objects across all connections on the server. - public int numPlayers => NetworkServer.connections.Count(kv => kv.Value.identity != null); + /// The one and only NetworkManager + public static NetworkManager singleton { get; internal set; } - /// True if the server is running or client is connected/connecting. - public bool isNetworkActive => NetworkServer.active || NetworkClient.active; + /// Number of active player objects across all connections on the server. + public int numPlayers => NetworkServer.connections.Count(kv => kv.Value.identity != null); - // TODO remove this - // internal for tests - internal static NetworkConnection clientReadyConnection; + /// True if the server is running or client is connected/connecting. + public bool isNetworkActive => NetworkServer.active || NetworkClient.active; - /// True if the client loaded a new scene when connecting to the server. - // This is set before OnClientConnect is called, so it can be checked - // there to perform different logic if a scene load occurred. - protected bool clientLoadedScene; + // TODO remove this + // internal for tests + internal static NetworkConnection clientReadyConnection; - // helper enum to know if we started the networkmanager as server/client/host. - // -> this is necessary because when StartHost changes server scene to - // online scene, FinishLoadScene is called and the host client isn't - // connected yet (no need to connect it before server was fully set up). - // in other words, we need this to know which mode we are running in - // during FinishLoadScene. - public NetworkManagerMode mode { get; private set; } + /// True if the client loaded a new scene when connecting to the server. + // This is set before OnClientConnect is called, so it can be checked + // there to perform different logic if a scene load occurred. + protected bool clientLoadedScene; - // virtual so that inheriting classes' OnValidate() can call base.OnValidate() too - public virtual void OnValidate() - { - // always >= 0 - maxConnections = Mathf.Max(maxConnections, 0); + // helper enum to know if we started the networkmanager as server/client/host. + // -> this is necessary because when StartHost changes server scene to + // online scene, FinishLoadScene is called and the host client isn't + // connected yet (no need to connect it before server was fully set up). + // in other words, we need this to know which mode we are running in + // during FinishLoadScene. + public NetworkManagerMode mode { get; private set; } - if (playerPrefab != null && !playerPrefab.TryGetComponent(out NetworkIdentity _)) - { - Debug.LogError("NetworkManager - Player Prefab must have a NetworkIdentity."); - playerPrefab = null; - } + // virtual so that inheriting classes' OnValidate() can call base.OnValidate() too + public virtual void OnValidate() + { + // always >= 0 + maxConnections = Mathf.Max(maxConnections, 0); - // This avoids the mysterious "Replacing existing prefab with assetId ... Old prefab 'Player', New prefab 'Player'" warning. - if (playerPrefab != null && spawnPrefabs.Contains(playerPrefab)) - { - Debug.LogWarning("NetworkManager - Player Prefab should not be added to Registered Spawnable Prefabs list...removed it."); - spawnPrefabs.Remove(playerPrefab); - } - - if (bufferTimeMultiplierForClamping < 1) bufferTimeMultiplierForClamping = 1; - } + if (playerPrefab != null && !playerPrefab.TryGetComponent(out NetworkIdentity _)) + { + Debug.LogError("NetworkManager - Player Prefab must have a NetworkIdentity."); + playerPrefab = null; + } - // virtual so that inheriting classes' Reset() can call base.Reset() too - // Reset only gets called when the component is added or the user resets the component - // Thats why we validate these things that only need to be validated on adding the NetworkManager here - // If we would do it in OnValidate() then it would run this everytime a value changes - public virtual void Reset() - { - // make sure someone doesn't accidentally add another NetworkManager - // need transform.root because when adding to a child, the parent's - // Reset isn't called. - foreach (NetworkManager manager in transform.root.GetComponentsInChildren()) - { - if (manager != this) - { - Debug.LogError($"{name} detected another component of type {typeof(NetworkManager)} in its hierarchy on {manager.name}. There can only be one, please remove one of them."); - // return early so that transport component isn't auto-added - // to the duplicate NetworkManager. - return; - } - } - } + // This avoids the mysterious "Replacing existing prefab with assetId ... Old prefab 'Player', New prefab 'Player'" warning. + if (playerPrefab != null && spawnPrefabs.Contains(playerPrefab)) + { + Debug.LogWarning("NetworkManager - Player Prefab should not be added to Registered Spawnable Prefabs list...removed it."); + spawnPrefabs.Remove(playerPrefab); + } - // virtual so that inheriting classes' Awake() can call base.Awake() too - public virtual void Awake() - { - // Don't allow collision-destroyed second instance to continue. - if (!InitializeSingleton()) return; + if (bufferTimeMultiplierForClamping < 1) bufferTimeMultiplierForClamping = 1; + } - // Apply configuration in Awake once already - ApplyConfiguration(); + // virtual so that inheriting classes' Reset() can call base.Reset() too + // Reset only gets called when the component is added or the user resets the component + // Thats why we validate these things that only need to be validated on adding the NetworkManager here + // If we would do it in OnValidate() then it would run this everytime a value changes + public virtual void Reset() + { + // make sure someone doesn't accidentally add another NetworkManager + // need transform.root because when adding to a child, the parent's + // Reset isn't called. + foreach (NetworkManager manager in transform.root.GetComponentsInChildren()) + { + if (manager != this) + { + Debug.LogError($"{name} detected another component of type {typeof(NetworkManager)} in its hierarchy on {manager.name}. There can only be one, please remove one of them."); + // return early so that transport component isn't auto-added + // to the duplicate NetworkManager. + return; + } + } + } - // Set the networkSceneName to prevent a scene reload - // if client connection to server fails. - networkSceneName = offlineScene; + // virtual so that inheriting classes' Awake() can call base.Awake() too + public virtual void Awake() + { + // Don't allow collision-destroyed second instance to continue. + if (!InitializeSingleton()) return; - // setup OnSceneLoaded callback - SceneManager.sceneLoaded += OnSceneLoaded; - } + // Apply configuration in Awake once already + ApplyConfiguration(); - // virtual so that inheriting classes' Start() can call base.Start() too - public virtual void Start() - { - // headless mode? then start the server - // can't do this in Awake because Awake is for initialization. - // some transports might not be ready until Start. - // - // (tick rate is applied in StartServer!) + // Set the networkSceneName to prevent a scene reload + // if client connection to server fails. + networkSceneName = offlineScene; + + // setup OnSceneLoaded callback + SceneManager.sceneLoaded += OnSceneLoaded; + } + + // virtual so that inheriting classes' Start() can call base.Start() too + public virtual void Start() + { + // headless mode? then start the server + // can't do this in Awake because Awake is for initialization. + // some transports might not be ready until Start. + // + // (tick rate is applied in StartServer!) #if UNITY_SERVER if (autoStartServerBuild) { @@ -229,1190 +229,1190 @@ public virtual void Start() StartClient(); } #endif - } - - // make sure to call base.Update() when overwriting - public virtual void Update() - { - ApplyConfiguration(); - } - - // virtual so that inheriting classes' LateUpdate() can call base.LateUpdate() too - public virtual void LateUpdate() - { - UpdateScene(); - } - - // keep the online scene change check in a separate function. - // only change scene if the requested online scene is not blank, and is not already loaded. - bool IsServerOnlineSceneChangeNeeded() => - !string.IsNullOrWhiteSpace(onlineScene) && - !Utils.IsSceneActive(onlineScene) && - onlineScene != offlineScene; - - // Deprecated 2022-12-12 - [Obsolete("NetworkManager.IsSceneActive moved to Utils.IsSceneActive")] - public static bool IsSceneActive(string scene) => Utils.IsSceneActive(scene); - - // NetworkManager exposes some NetworkServer/Client configuration. - // we apply it every Update() in order to avoid two sources of truth. - // fixes issues where NetworkServer.sendRate was never set because - // NetworkManager.StartServer was never called, etc. - // => all exposed settings should be applied at all times if NM exists. - void ApplyConfiguration() - { - NetworkServer.tickRate = sendRate; - NetworkServer.bufferTimeMultiplierForClamping = bufferTimeMultiplierForClamping; - } - - // full server setup code, without spawning objects yet - void SetupServer() - { - // Debug.Log("NetworkManager SetupServer"); - InitializeSingleton(); - - if (runInBackground) - Application.runInBackground = true; - - if (authenticator != null) - { - authenticator.OnStartServer(); - authenticator.OnServerAuthenticated.AddListener(OnServerAuthenticated); - } - - ConfigureHeadlessFrameRate(); - - // start listening to network connections - NetworkServer.Listen(maxConnections); - - // this must be after Listen(), since that registers the default message handlers - RegisterServerMessages(); - - // do not call OnStartServer here yet. - // this is up to the caller. different for server-only vs. host mode. - } - - /// Starts the server, listening for incoming connections. - public void StartServer() - { - if (NetworkServer.active) - { - Debug.LogWarning("Server already started."); - return; - } - - mode = NetworkManagerMode.ServerOnly; - - // StartServer is inherently ASYNCHRONOUS (=doesn't finish immediately) - // - // Here is what it does: - // Listen - // if onlineScene: - // LoadSceneAsync - // ... - // FinishLoadSceneServerOnly - // SpawnObjects - // else: - // SpawnObjects - // - // there is NO WAY to make it synchronous because both LoadSceneAsync - // and LoadScene do not finish loading immediately. as long as we - // have the onlineScene feature, it will be asynchronous! - - SetupServer(); - - // call OnStartServer AFTER Listen, so that NetworkServer.active is - // true and we can call NetworkServer.Spawn in OnStartServer - // overrides. - // (useful for loading & spawning stuff from database etc.) - // - // note: there is no risk of someone connecting after Listen() and - // before OnStartServer() because this all runs in one thread - // and we don't start processing connects until Update. - OnStartServer(); - - // scene change needed? then change scene and spawn afterwards. - if (IsServerOnlineSceneChangeNeeded()) - { - ServerChangeScene(onlineScene); - } - // otherwise spawn directly - else - { - NetworkServer.SpawnObjects(); - } - } - - void SetupClient() - { - InitializeSingleton(); - - if (runInBackground) - Application.runInBackground = true; - - if (authenticator != null) - { - authenticator.OnStartClient(); - authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); - } - - // NetworkClient.sendRate = clientSendRate; - } - - /// Starts the client, connects it to the server with networkAddress. - public void StartClient() - { - if (NetworkClient.active) - { - Debug.LogWarning("Client already started."); - return; - } - - mode = NetworkManagerMode.ClientOnly; - - SetupClient(); - - // In case this is a headless client... - ConfigureHeadlessFrameRate(); - - RegisterClientMessages(); - - if (string.IsNullOrWhiteSpace(networkAddress)) - { - Debug.LogError("Must set the Network Address field in the manager"); - return; - } - // Debug.Log($"NetworkManager StartClient address:{networkAddress}"); - - NetworkClient.Connect(networkAddress); - - OnStartClient(); - } - - /// Starts the client, connects it to the server via Uri - public void StartClient(Uri uri) - { - if (NetworkClient.active) - { - Debug.LogWarning("Client already started."); - return; - } - - mode = NetworkManagerMode.ClientOnly; - - SetupClient(); - - RegisterClientMessages(); - - // Debug.Log($"NetworkManager StartClient address:{uri}"); - networkAddress = uri.Host; - - NetworkClient.Connect(uri); - - OnStartClient(); - } - - /// Starts a network "host" - a server and client in the same application. - public void StartHost() - { - if (NetworkServer.active || NetworkClient.active) - { - Debug.LogWarning("Server or Client already started."); - return; - } - - mode = NetworkManagerMode.Host; - - // StartHost is inherently ASYNCHRONOUS (=doesn't finish immediately) - // - // Here is what it does: - // Listen - // ConnectHost - // if onlineScene: - // LoadSceneAsync - // ... - // FinishLoadSceneHost - // FinishStartHost - // SpawnObjects - // StartHostClient <= not guaranteed to happen after SpawnObjects if onlineScene is set! - // ClientAuth - // success: server sends changescene msg to client - // else: - // FinishStartHost - // - // there is NO WAY to make it synchronous because both LoadSceneAsync - // and LoadScene do not finish loading immediately. as long as we - // have the onlineScene feature, it will be asynchronous! - - // setup server first - SetupServer(); - - // scene change needed? then change scene and spawn afterwards. - // => BEFORE host client connects. if client auth succeeds then the - // server tells it to load 'onlineScene'. we can't do that if - // server is still in 'offlineScene'. so load on server first. - if (IsServerOnlineSceneChangeNeeded()) - { - // call FinishStartHost after changing scene. - finishStartHostPending = true; - ServerChangeScene(onlineScene); - } - // otherwise call FinishStartHost directly - else - { - FinishStartHost(); - } - } - - // This may be set true in StartHost and is evaluated in FinishStartHost - bool finishStartHostPending; - - // FinishStartHost is guaranteed to be called after the host server was - // fully started and all the asynchronous StartHost magic is finished - // (= scene loading), or immediately if there was no asynchronous magic. - // - // note: we don't really need FinishStartClient/FinishStartServer. the - // host version is enough. - void FinishStartHost() - { - // ConnectHost needs to be called BEFORE SpawnObjects: - // https://github.com/vis2k/Mirror/pull/1249/ - // -> this sets NetworkServer.localConnection. - // -> localConnection needs to be set before SpawnObjects because: - // -> SpawnObjects calls OnStartServer in all NetworkBehaviours - // -> OnStartServer might spawn an object and set [SyncVar(hook="OnColorChanged")] object.color = green; - // -> this calls SyncVar.set (generated by Weaver), which has - // a custom case for host mode (because host mode doesn't - // get OnDeserialize calls, where SyncVar hooks are usually - // called): - // - // if (!SyncVarEqual(value, ref color)) - // { - // if (NetworkServer.localClientActive && !getSyncVarHookGuard(1uL)) - // { - // setSyncVarHookGuard(1uL, value: true); - // OnColorChangedHook(value); - // setSyncVarHookGuard(1uL, value: false); - // } - // SetSyncVar(value, ref color, 1uL); - // } - // - // -> localClientActive needs to be true, otherwise the hook - // isn't called in host mode! - // - // TODO call this after spawnobjects and worry about the syncvar hook fix later? - NetworkClient.ConnectHost(); - - // invoke user callbacks AFTER ConnectHost has set .activeHost. - // this way initialization can properly handle host mode. - // - // fixes: https://github.com/MirrorNetworking/Mirror/issues/3302 - // where [SyncVar] hooks wouldn't be called for objects spawned in - // NetworkManager.OnStartServer, because .activeHost was still false. - // - // TODO is there a risk of someone connecting between Listen() and FinishStartHost()? - OnStartServer(); - - // call OnStartHost AFTER SetupServer. this way we can use - // NetworkServer.Spawn etc. in there too. just like OnStartServer - // is called after the server is actually properly started. - OnStartHost(); - - // server scene was loaded. now spawn all the objects - NetworkServer.SpawnObjects(); - - // connect client and call OnStartClient AFTER server scene was - // loaded and all objects were spawned. - // DO NOT do this earlier. it would cause race conditions where a - // client will do things before the server is even fully started. - //Debug.Log("StartHostClient called"); - SetupClient(); - - networkAddress = "localhost"; - RegisterClientMessages(); - - // call OnConencted needs to be called AFTER RegisterClientMessages - // (https://github.com/vis2k/Mirror/pull/1249/) - HostMode.InvokeOnConnected(); - - OnStartClient(); - } - - /// This stops both the client and the server that the manager is using. - public void StopHost() - { - OnStopHost(); - - // calling OnTransportDisconnected was needed to fix - // https://github.com/vis2k/Mirror/issues/1515 - // so that the host client receives a DisconnectMessage - // TODO reevaluate if this is still needed after all the disconnect - // fixes, and try to put this into LocalConnection.Disconnect! - NetworkServer.OnTransportDisconnected(NetworkConnection.LocalConnectionId); - - StopClient(); - StopServer(); - } - - /// Stops the server from listening and simulating the game. - public void StopServer() - { - // return if already stopped to avoid recursion deadlock - if (!NetworkServer.active) - return; - - if (authenticator != null) - { - authenticator.OnServerAuthenticated.RemoveListener(OnServerAuthenticated); - authenticator.OnStopServer(); - } - - // Get Network Manager out of DDOL before going to offline scene - // to avoid collision and let a fresh Network Manager be created. - // IMPORTANT: .gameObject can be null if StopClient is called from - // OnApplicationQuit or from tests! - if (gameObject != null - && gameObject.scene.name == "DontDestroyOnLoad" - && !string.IsNullOrWhiteSpace(offlineScene) - && SceneManager.GetActiveScene().path != offlineScene) - SceneManager.MoveGameObjectToScene(gameObject, SceneManager.GetActiveScene()); - - OnStopServer(); - - //Debug.Log("NetworkManager StopServer"); - NetworkServer.Shutdown(); - - // set offline mode BEFORE changing scene so that FinishStartScene - // doesn't think we need initialize anything. - mode = NetworkManagerMode.Offline; - - if (!string.IsNullOrWhiteSpace(offlineScene)) - { - ServerChangeScene(offlineScene); - } - - startPositionIndex = 0; - - networkSceneName = ""; - } - - /// Stops and disconnects the client. - public void StopClient() - { - if (mode == NetworkManagerMode.Offline) - return; - - // ask client -> transport to disconnect. - // handle voluntary and involuntary disconnects in OnClientDisconnect. - // - // StopClient - // NetworkClient.Disconnect - // Transport.Disconnect - // ... - // Transport.OnClientDisconnect - // NetworkClient.OnTransportDisconnect - // NetworkManager.OnClientDisconnect - NetworkClient.Disconnect(); - - // UNET invoked OnDisconnected cleanup immediately. - // let's keep it for now, in case any projects depend on it. - // TODO simply remove this in the future. - OnClientDisconnectInternal(); - } - - // called when quitting the application by closing the window / pressing - // stop in the editor. virtual so that inheriting classes' - // OnApplicationQuit() can call base.OnApplicationQuit() too - public virtual void OnApplicationQuit() - { - // stop client first - // (we want to send the quit packet to the server instead of waiting - // for a timeout) - if (NetworkClient.isConnected) - { - StopClient(); - //Debug.Log("OnApplicationQuit: stopped client"); - } - - // stop server after stopping client (for proper host mode stopping) - if (NetworkServer.active) - { - StopServer(); - //Debug.Log("OnApplicationQuit: stopped server"); - } - - // Call ResetStatics to reset statics and singleton - ResetStatics(); - } - - /// Set the frame rate for a headless builds. Override to disable or modify. - // useful for dedicated servers. - // useful for headless benchmark clients. - public virtual void ConfigureHeadlessFrameRate() - { + } + + // make sure to call base.Update() when overwriting + public virtual void Update() + { + ApplyConfiguration(); + } + + // virtual so that inheriting classes' LateUpdate() can call base.LateUpdate() too + public virtual void LateUpdate() + { + UpdateScene(); + } + + // keep the online scene change check in a separate function. + // only change scene if the requested online scene is not blank, and is not already loaded. + bool IsServerOnlineSceneChangeNeeded() => + !string.IsNullOrWhiteSpace(onlineScene) && + !Utils.IsSceneActive(onlineScene) && + onlineScene != offlineScene; + + // Deprecated 2022-12-12 + [Obsolete("NetworkManager.IsSceneActive moved to Utils.IsSceneActive")] + public static bool IsSceneActive(string scene) => Utils.IsSceneActive(scene); + + // NetworkManager exposes some NetworkServer/Client configuration. + // we apply it every Update() in order to avoid two sources of truth. + // fixes issues where NetworkServer.sendRate was never set because + // NetworkManager.StartServer was never called, etc. + // => all exposed settings should be applied at all times if NM exists. + void ApplyConfiguration() + { + NetworkServer.tickRate = sendRate; + NetworkServer.bufferTimeMultiplierForClamping = bufferTimeMultiplierForClamping; + } + + // full server setup code, without spawning objects yet + void SetupServer() + { + // Debug.Log("NetworkManager SetupServer"); + InitializeSingleton(); + + if (runInBackground) + Application.runInBackground = true; + + if (authenticator != null) + { + authenticator.OnStartServer(); + authenticator.OnServerAuthenticated.AddListener(OnServerAuthenticated); + } + + ConfigureHeadlessFrameRate(); + + // start listening to network connections + NetworkServer.Listen(maxConnections); + + // this must be after Listen(), since that registers the default message handlers + RegisterServerMessages(); + + // do not call OnStartServer here yet. + // this is up to the caller. different for server-only vs. host mode. + } + + /// Starts the server, listening for incoming connections. + public void StartServer() + { + if (NetworkServer.active) + { + Debug.LogWarning("Server already started."); + return; + } + + mode = NetworkManagerMode.ServerOnly; + + // StartServer is inherently ASYNCHRONOUS (=doesn't finish immediately) + // + // Here is what it does: + // Listen + // if onlineScene: + // LoadSceneAsync + // ... + // FinishLoadSceneServerOnly + // SpawnObjects + // else: + // SpawnObjects + // + // there is NO WAY to make it synchronous because both LoadSceneAsync + // and LoadScene do not finish loading immediately. as long as we + // have the onlineScene feature, it will be asynchronous! + + SetupServer(); + + // call OnStartServer AFTER Listen, so that NetworkServer.active is + // true and we can call NetworkServer.Spawn in OnStartServer + // overrides. + // (useful for loading & spawning stuff from database etc.) + // + // note: there is no risk of someone connecting after Listen() and + // before OnStartServer() because this all runs in one thread + // and we don't start processing connects until Update. + OnStartServer(); + + // scene change needed? then change scene and spawn afterwards. + if (IsServerOnlineSceneChangeNeeded()) + { + ServerChangeScene(onlineScene); + } + // otherwise spawn directly + else + { + NetworkServer.SpawnObjects(); + } + } + + void SetupClient() + { + InitializeSingleton(); + + if (runInBackground) + Application.runInBackground = true; + + if (authenticator != null) + { + authenticator.OnStartClient(); + authenticator.OnClientAuthenticated.AddListener(OnClientAuthenticated); + } + + // NetworkClient.sendRate = clientSendRate; + } + + /// Starts the client, connects it to the server with networkAddress. + public void StartClient() + { + if (NetworkClient.active) + { + Debug.LogWarning("Client already started."); + return; + } + + mode = NetworkManagerMode.ClientOnly; + + SetupClient(); + + // In case this is a headless client... + ConfigureHeadlessFrameRate(); + + RegisterClientMessages(); + + if (string.IsNullOrWhiteSpace(networkAddress)) + { + Debug.LogError("Must set the Network Address field in the manager"); + return; + } + // Debug.Log($"NetworkManager StartClient address:{networkAddress}"); + + NetworkClient.Connect(networkAddress); + + OnStartClient(); + } + + /// Starts the client, connects it to the server via Uri + public void StartClient(Uri uri) + { + if (NetworkClient.active) + { + Debug.LogWarning("Client already started."); + return; + } + + mode = NetworkManagerMode.ClientOnly; + + SetupClient(); + + RegisterClientMessages(); + + // Debug.Log($"NetworkManager StartClient address:{uri}"); + networkAddress = uri.Host; + + NetworkClient.Connect(uri); + + OnStartClient(); + } + + /// Starts a network "host" - a server and client in the same application. + public void StartHost() + { + if (NetworkServer.active || NetworkClient.active) + { + Debug.LogWarning("Server or Client already started."); + return; + } + + mode = NetworkManagerMode.Host; + + // StartHost is inherently ASYNCHRONOUS (=doesn't finish immediately) + // + // Here is what it does: + // Listen + // ConnectHost + // if onlineScene: + // LoadSceneAsync + // ... + // FinishLoadSceneHost + // FinishStartHost + // SpawnObjects + // StartHostClient <= not guaranteed to happen after SpawnObjects if onlineScene is set! + // ClientAuth + // success: server sends changescene msg to client + // else: + // FinishStartHost + // + // there is NO WAY to make it synchronous because both LoadSceneAsync + // and LoadScene do not finish loading immediately. as long as we + // have the onlineScene feature, it will be asynchronous! + + // setup server first + SetupServer(); + + // scene change needed? then change scene and spawn afterwards. + // => BEFORE host client connects. if client auth succeeds then the + // server tells it to load 'onlineScene'. we can't do that if + // server is still in 'offlineScene'. so load on server first. + if (IsServerOnlineSceneChangeNeeded()) + { + // call FinishStartHost after changing scene. + finishStartHostPending = true; + ServerChangeScene(onlineScene); + } + // otherwise call FinishStartHost directly + else + { + FinishStartHost(); + } + } + + // This may be set true in StartHost and is evaluated in FinishStartHost + bool finishStartHostPending; + + // FinishStartHost is guaranteed to be called after the host server was + // fully started and all the asynchronous StartHost magic is finished + // (= scene loading), or immediately if there was no asynchronous magic. + // + // note: we don't really need FinishStartClient/FinishStartServer. the + // host version is enough. + void FinishStartHost() + { + // ConnectHost needs to be called BEFORE SpawnObjects: + // https://github.com/vis2k/Mirror/pull/1249/ + // -> this sets NetworkServer.localConnection. + // -> localConnection needs to be set before SpawnObjects because: + // -> SpawnObjects calls OnStartServer in all NetworkBehaviours + // -> OnStartServer might spawn an object and set [SyncVar(hook="OnColorChanged")] object.color = green; + // -> this calls SyncVar.set (generated by Weaver), which has + // a custom case for host mode (because host mode doesn't + // get OnDeserialize calls, where SyncVar hooks are usually + // called): + // + // if (!SyncVarEqual(value, ref color)) + // { + // if (NetworkServer.localClientActive && !getSyncVarHookGuard(1uL)) + // { + // setSyncVarHookGuard(1uL, value: true); + // OnColorChangedHook(value); + // setSyncVarHookGuard(1uL, value: false); + // } + // SetSyncVar(value, ref color, 1uL); + // } + // + // -> localClientActive needs to be true, otherwise the hook + // isn't called in host mode! + // + // TODO call this after spawnobjects and worry about the syncvar hook fix later? + NetworkClient.ConnectHost(); + + // invoke user callbacks AFTER ConnectHost has set .activeHost. + // this way initialization can properly handle host mode. + // + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3302 + // where [SyncVar] hooks wouldn't be called for objects spawned in + // NetworkManager.OnStartServer, because .activeHost was still false. + // + // TODO is there a risk of someone connecting between Listen() and FinishStartHost()? + OnStartServer(); + + // call OnStartHost AFTER SetupServer. this way we can use + // NetworkServer.Spawn etc. in there too. just like OnStartServer + // is called after the server is actually properly started. + OnStartHost(); + + // server scene was loaded. now spawn all the objects + NetworkServer.SpawnObjects(); + + // connect client and call OnStartClient AFTER server scene was + // loaded and all objects were spawned. + // DO NOT do this earlier. it would cause race conditions where a + // client will do things before the server is even fully started. + //Debug.Log("StartHostClient called"); + SetupClient(); + + networkAddress = "localhost"; + RegisterClientMessages(); + + // call OnConencted needs to be called AFTER RegisterClientMessages + // (https://github.com/vis2k/Mirror/pull/1249/) + HostMode.InvokeOnConnected(); + + OnStartClient(); + } + + /// This stops both the client and the server that the manager is using. + public void StopHost() + { + OnStopHost(); + + // calling OnTransportDisconnected was needed to fix + // https://github.com/vis2k/Mirror/issues/1515 + // so that the host client receives a DisconnectMessage + // TODO reevaluate if this is still needed after all the disconnect + // fixes, and try to put this into LocalConnection.Disconnect! + NetworkServer.OnTransportDisconnected(NetworkConnection.LocalConnectionId); + + StopClient(); + StopServer(); + } + + /// Stops the server from listening and simulating the game. + public void StopServer() + { + // return if already stopped to avoid recursion deadlock + if (!NetworkServer.active) + return; + + if (authenticator != null) + { + authenticator.OnServerAuthenticated.RemoveListener(OnServerAuthenticated); + authenticator.OnStopServer(); + } + + // Get Network Manager out of DDOL before going to offline scene + // to avoid collision and let a fresh Network Manager be created. + // IMPORTANT: .gameObject can be null if StopClient is called from + // OnApplicationQuit or from tests! + if (gameObject != null + && gameObject.scene.name == "DontDestroyOnLoad" + && !string.IsNullOrWhiteSpace(offlineScene) + && SceneManager.GetActiveScene().path != offlineScene) + SceneManager.MoveGameObjectToScene(gameObject, SceneManager.GetActiveScene()); + + OnStopServer(); + + //Debug.Log("NetworkManager StopServer"); + NetworkServer.Shutdown(); + + // set offline mode BEFORE changing scene so that FinishStartScene + // doesn't think we need initialize anything. + mode = NetworkManagerMode.Offline; + + if (!string.IsNullOrWhiteSpace(offlineScene)) + { + ServerChangeScene(offlineScene); + } + + startPositionIndex = 0; + + networkSceneName = ""; + } + + /// Stops and disconnects the client. + public void StopClient() + { + if (mode == NetworkManagerMode.Offline) + return; + + // ask client -> transport to disconnect. + // handle voluntary and involuntary disconnects in OnClientDisconnect. + // + // StopClient + // NetworkClient.Disconnect + // Transport.Disconnect + // ... + // Transport.OnClientDisconnect + // NetworkClient.OnTransportDisconnect + // NetworkManager.OnClientDisconnect + NetworkClient.Disconnect(); + + // UNET invoked OnDisconnected cleanup immediately. + // let's keep it for now, in case any projects depend on it. + // TODO simply remove this in the future. + OnClientDisconnectInternal(); + } + + // called when quitting the application by closing the window / pressing + // stop in the editor. virtual so that inheriting classes' + // OnApplicationQuit() can call base.OnApplicationQuit() too + public virtual void OnApplicationQuit() + { + // stop client first + // (we want to send the quit packet to the server instead of waiting + // for a timeout) + if (NetworkClient.isConnected) + { + StopClient(); + //Debug.Log("OnApplicationQuit: stopped client"); + } + + // stop server after stopping client (for proper host mode stopping) + if (NetworkServer.active) + { + StopServer(); + //Debug.Log("OnApplicationQuit: stopped server"); + } + + // Call ResetStatics to reset statics and singleton + ResetStatics(); + } + + /// Set the frame rate for a headless builds. Override to disable or modify. + // useful for dedicated servers. + // useful for headless benchmark clients. + public virtual void ConfigureHeadlessFrameRate() + { #if UNITY_SERVER Application.targetFrameRate = sendRate; // Debug.Log($"Server Tick Rate set to {Application.targetFrameRate} Hz."); #endif - } - - bool InitializeSingleton() - { - if (singleton != null && singleton == this) - return true; - - if (dontDestroyOnLoad) - { - if (singleton != null) - { - Debug.LogWarning("Multiple NetworkManagers detected in the scene. Only one NetworkManager can exist at a time. The duplicate NetworkManager will be destroyed."); - Destroy(gameObject); - - // Return false to not allow collision-destroyed second instance to continue. - return false; - } - //Debug.Log("NetworkManager created singleton (DontDestroyOnLoad)"); - singleton = this; - if (Application.isPlaying) - { - // Force the object to scene root, in case user made it a child of something - // in the scene since DDOL is only allowed for scene root objects - transform.SetParent(null); - DontDestroyOnLoad(gameObject); - } - } - else - { - //Debug.Log("NetworkManager created singleton (ForScene)"); - singleton = this; - } - - // set active transport AFTER setting singleton. - // so only if we didn't destroy ourselves. - Transport.active = transport; - return true; - } - - void RegisterServerMessages() - { - NetworkServer.OnConnectedEvent = OnServerConnectInternal; - NetworkServer.OnDisconnectedEvent = OnServerDisconnect; - NetworkServer.OnErrorEvent = OnServerError; - NetworkServer.RegisterHandler(OnServerAddPlayerInternal); - - // Network Server initially registers its own handler for this, so we replace it here. - NetworkServer.ReplaceHandler(OnServerReadyMessageInternal); - } - - void RegisterClientMessages() - { - NetworkClient.OnConnectedEvent = OnClientConnectInternal; - NetworkClient.OnDisconnectedEvent = OnClientDisconnectInternal; - NetworkClient.OnErrorEvent = OnClientError; - NetworkClient.RegisterHandler(OnClientNotReadyMessageInternal); - NetworkClient.RegisterHandler(OnClientSceneInternal, false); - - if (playerPrefab != null) - NetworkClient.RegisterPrefab(playerPrefab); - - foreach (GameObject prefab in spawnPrefabs.Where(t => t != null)) - NetworkClient.RegisterPrefab(prefab); - } - - // This is the only way to clear the singleton, so another instance can be created. - // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] - public static void ResetStatics() - { - // call StopHost if we have a singleton - if (singleton) - singleton.StopHost(); - - // reset all statics - startPositions.Clear(); - startPositionIndex = 0; - clientReadyConnection = null; - loadingSceneAsync = null; - networkSceneName = string.Empty; - - // and finally (in case it isn't null already)... - singleton = null; - } - - // virtual so that inheriting classes' OnDestroy() can call base.OnDestroy() too - public virtual void OnDestroy() - { - //Debug.Log("NetworkManager destroyed"); - } - - /// The name of the current network scene. - // set by NetworkManager when changing the scene. - // new clients will automatically load this scene. - // Loading a scene manually won't set it. - public static string networkSceneName { get; protected set; } = ""; - - public static AsyncOperation loadingSceneAsync; - - /// Change the server scene and all client's scenes across the network. - // Called automatically if onlineScene or offlineScene are set, but it - // can be called from user code to switch scenes again while the game is - // in progress. This automatically sets clients to be not-ready during - // the change and ready again to participate in the new scene. - public virtual void ServerChangeScene(string newSceneName) - { - if (string.IsNullOrWhiteSpace(newSceneName)) - { - Debug.LogError("ServerChangeScene empty scene name"); - return; - } - - if (NetworkServer.isLoadingScene && newSceneName == networkSceneName) - { - Debug.LogError($"Scene change is already in progress for {newSceneName}"); - return; - } - - // Debug.Log($"ServerChangeScene {newSceneName}"); - NetworkServer.SetAllClientsNotReady(); - networkSceneName = newSceneName; - - // Let server prepare for scene change - OnServerChangeScene(newSceneName); - - // set server flag to stop processing messages while changing scenes - // it will be re-enabled in FinishLoadScene. - NetworkServer.isLoadingScene = true; - - loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName); - - // ServerChangeScene can be called when stopping the server - // when this happens the server is not active so does not need to tell clients about the change - if (NetworkServer.active) - { - // notify all clients about the new scene - NetworkServer.SendToAll(new SceneMessage - { - sceneName = newSceneName - }); - } - - startPositionIndex = 0; - startPositions.Clear(); - } - - // This is only set in ClientChangeScene below...never on server. - // We need to check this in OnClientSceneChanged called from FinishLoadSceneClientOnly - // to prevent AddPlayer message after loading/unloading additive scenes - SceneOperation clientSceneOperation = SceneOperation.Normal; - - internal void ClientChangeScene(string newSceneName, SceneOperation sceneOperation = SceneOperation.Normal, bool customHandling = false) - { - if (string.IsNullOrWhiteSpace(newSceneName)) - { - Debug.LogError("ClientChangeScene empty scene name"); - return; - } - - //Debug.Log($"ClientChangeScene newSceneName: {newSceneName} networkSceneName{networkSceneName}"); - - // Let client prepare for scene change - OnClientChangeScene(newSceneName, sceneOperation, customHandling); - - // After calling OnClientChangeScene, exit if server since server is already doing - // the actual scene change, and we don't need to do it for the host client - if (NetworkServer.active) - return; - - // set client flag to stop processing messages while loading scenes. - // otherwise we would process messages and then lose all the state - // as soon as the load is finishing, causing all kinds of bugs - // because of missing state. - // (client may be null after StopClient etc.) - // Debug.Log("ClientChangeScene: pausing handlers while scene is loading to avoid data loss after scene was loaded."); - NetworkClient.isLoadingScene = true; - - // Cache sceneOperation so we know what was requested by the - // Scene message in OnClientChangeScene and OnClientSceneChanged - clientSceneOperation = sceneOperation; - - // scene handling will happen in overrides of OnClientChangeScene and/or OnClientSceneChanged - // Do not call FinishLoadScene here. Custom handler will assign loadingSceneAsync and we need - // to wait for that to finish. UpdateScene already checks for that to be not null and isDone. - if (customHandling) - return; - - switch (sceneOperation) - { - case SceneOperation.Normal: - loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName); - break; - case SceneOperation.LoadAdditive: - // Ensure additive scene is not already loaded on client by name or path - // since we don't know which was passed in the Scene message - if (!SceneManager.GetSceneByName(newSceneName).IsValid() && !SceneManager.GetSceneByPath(newSceneName).IsValid()) - loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName, LoadSceneMode.Additive); - else - { - Debug.LogWarning($"Scene {newSceneName} is already loaded"); - - // Reset the flag that we disabled before entering this switch - NetworkClient.isLoadingScene = false; - } - break; - case SceneOperation.UnloadAdditive: - // Ensure additive scene is actually loaded on client by name or path - // since we don't know which was passed in the Scene message - if (SceneManager.GetSceneByName(newSceneName).IsValid() || SceneManager.GetSceneByPath(newSceneName).IsValid()) - loadingSceneAsync = SceneManager.UnloadSceneAsync(newSceneName, UnloadSceneOptions.UnloadAllEmbeddedSceneObjects); - else - { - Debug.LogWarning($"Cannot unload {newSceneName} with UnloadAdditive operation"); - - // Reset the flag that we disabled before entering this switch - NetworkClient.isLoadingScene = false; - } - break; - } - - // don't change the client's current networkSceneName when loading additive scene content - if (sceneOperation == SceneOperation.Normal) - networkSceneName = newSceneName; - } - - // support additive scene loads: - // NetworkScenePostProcess disables all scene objects on load, and - // * NetworkServer.SpawnObjects enables them again on the server when - // calling OnStartServer - // * NetworkClient.PrepareToSpawnSceneObjects enables them again on the - // client after the server sends ObjectSpawnStartedMessage to client - // in SpawnObserversForConnection. this is only called when the - // client joins, so we need to rebuild scene objects manually again - // TODO merge this with FinishLoadScene()? - void OnSceneLoaded(Scene scene, LoadSceneMode mode) - { - if (mode == LoadSceneMode.Additive) - { - if (NetworkServer.active) - { - // TODO only respawn the server objects from that scene later! - NetworkServer.SpawnObjects(); - // Debug.Log($"Respawned Server objects after additive scene load: {scene.name}"); - } - if (NetworkClient.active) - { - NetworkClient.PrepareToSpawnSceneObjects(); - // Debug.Log($"Rebuild Client spawnableObjects after additive scene load: {scene.name}"); - } - } - } - - void UpdateScene() - { - if (loadingSceneAsync != null && loadingSceneAsync.isDone) - { - //Debug.Log($"ClientChangeScene done readyConn {clientReadyConnection}"); - - // try-finally to guarantee loadingSceneAsync being cleared. - // fixes https://github.com/vis2k/Mirror/issues/2517 where if - // FinishLoadScene throws an exception, loadingSceneAsync would - // never be cleared and this code would run every Update. - try - { - FinishLoadScene(); - } - finally - { - loadingSceneAsync.allowSceneActivation = true; - loadingSceneAsync = null; - } - } - } - - protected void FinishLoadScene() - { - // NOTE: this cannot use NetworkClient.allClients[0] - that client may be for a completely different purpose. - - // process queued messages that we received while loading the scene - //Debug.Log("FinishLoadScene: resuming handlers after scene was loading."); - NetworkServer.isLoadingScene = false; - NetworkClient.isLoadingScene = false; - - // host mode? - if (mode == NetworkManagerMode.Host) - { - FinishLoadSceneHost(); - } - // server-only mode? - else if (mode == NetworkManagerMode.ServerOnly) - { - FinishLoadSceneServerOnly(); - } - // client-only mode? - else if (mode == NetworkManagerMode.ClientOnly) - { - FinishLoadSceneClientOnly(); - } - // otherwise we called it after stopping when loading offline scene. - // do nothing then. - } - - // finish load scene part for host mode. makes code easier and is - // necessary for FinishStartHost later. - // (the 3 things have to happen in that exact order) - void FinishLoadSceneHost() - { - // debug message is very important. if we ever break anything then - // it's very obvious to notice. - //Debug.Log("Finished loading scene in host mode."); - - if (clientReadyConnection != null) - { - clientLoadedScene = true; - clientReadyConnection = null; - } - - // do we need to finish a StartHost() call? - // then call FinishStartHost and let it take care of spawning etc. - if (finishStartHostPending) - { - finishStartHostPending = false; - FinishStartHost(); - - // call OnServerSceneChanged - OnServerSceneChanged(networkSceneName); - - // DO NOT call OnClientSceneChanged here. - // the scene change happened because StartHost loaded the - // server's online scene. it has nothing to do with the client. - // this was not meant as a client scene load, so don't call it. - // - // otherwise AddPlayer would be called twice: - // -> once for client OnConnected - // -> once in OnClientSceneChanged - } - // otherwise we just changed a scene in host mode - else - { - // spawn server objects - NetworkServer.SpawnObjects(); - - // call OnServerSceneChanged - OnServerSceneChanged(networkSceneName); - - if (NetworkClient.isConnected) - OnClientSceneChanged(); - } - } - - // finish load scene part for server-only. . makes code easier and is - // necessary for FinishStartServer later. - void FinishLoadSceneServerOnly() - { - // debug message is very important. if we ever break anything then - // it's very obvious to notice. - //Debug.Log("Finished loading scene in server-only mode."); - - NetworkServer.SpawnObjects(); - OnServerSceneChanged(networkSceneName); - } - - // finish load scene part for client-only. makes code easier and is - // necessary for FinishStartClient later. - void FinishLoadSceneClientOnly() - { - // debug message is very important. if we ever break anything then - // it's very obvious to notice. - //Debug.Log("Finished loading scene in client-only mode."); - - if (clientReadyConnection != null) - { - clientLoadedScene = true; - clientReadyConnection = null; - } - - if (NetworkClient.isConnected) - OnClientSceneChanged(); - } - - /// - /// Registers the transform of a game object as a player spawn location. - /// This is done automatically by NetworkStartPosition components, but can be done manually from user script code. - /// - /// Transform to register. - // Static because it's called from NetworkStartPosition::Awake - // and singleton may not exist yet - public static void RegisterStartPosition(Transform start) - { - // Debug.Log($"RegisterStartPosition: {start.gameObject.name} {start.position}"); - startPositions.Add(start); - - // reorder the list so that round-robin spawning uses the start positions - // in hierarchy order. This assumes all objects with NetworkStartPosition - // component are siblings, either in the scene root or together as children - // under a single parent in the scene. - startPositions = startPositions.OrderBy(transform => transform.GetSiblingIndex()).ToList(); - } - - /// Unregister a Transform from start positions. - // Static because it's called from NetworkStartPosition::OnDestroy - // and singleton may not exist yet - public static void UnRegisterStartPosition(Transform start) - { - //Debug.Log($"UnRegisterStartPosition: {start.name} {start.position}"); - startPositions.Remove(start); - } - - /// Get the next NetworkStartPosition based on the selected PlayerSpawnMethod. - public virtual Transform GetStartPosition() - { - // first remove any dead transforms - startPositions.RemoveAll(t => t == null); - - if (startPositions.Count == 0) - return null; - - if (playerSpawnMethod == PlayerSpawnMethod.Random) - { - return startPositions[UnityEngine.Random.Range(0, startPositions.Count)]; - } - else - { - Transform startPosition = startPositions[startPositionIndex]; - startPositionIndex = (startPositionIndex + 1) % startPositions.Count; - return startPosition; - } - } - - void OnServerConnectInternal(NetworkConnectionToClient conn) - { - //Debug.Log("NetworkManager.OnServerConnectInternal"); - - if (authenticator != null) - { - // we have an authenticator - let it handle authentication - authenticator.OnServerAuthenticate(conn); - } - else - { - // authenticate immediately - OnServerAuthenticated(conn); - } - } - - // called after successful authentication - // TODO do the NetworkServer.OnAuthenticated thing from x branch - void OnServerAuthenticated(NetworkConnectionToClient conn) - { - //Debug.Log("NetworkManager.OnServerAuthenticated"); - - // set connection to authenticated - conn.isAuthenticated = true; - - // proceed with the login handshake by calling OnServerConnect - if (networkSceneName != "" && networkSceneName != offlineScene) - { - SceneMessage msg = new SceneMessage() - { - sceneName = networkSceneName - }; - conn.Send(msg); - } - - OnServerConnect(conn); - } - - void OnServerReadyMessageInternal(NetworkConnectionToClient conn, ReadyMessage msg) - { - //Debug.Log("NetworkManager.OnServerReadyMessageInternal"); - OnServerReady(conn); - } - - void OnServerAddPlayerInternal(NetworkConnectionToClient conn, AddPlayerMessage msg) - { - //Debug.Log("NetworkManager.OnServerAddPlayer"); - - if (autoCreatePlayer && playerPrefab == null) - { - Debug.LogError("The PlayerPrefab is empty on the NetworkManager. Please setup a PlayerPrefab object."); - return; - } - - if (autoCreatePlayer && !playerPrefab.TryGetComponent(out NetworkIdentity _)) - { - Debug.LogError("The PlayerPrefab does not have a NetworkIdentity. Please add a NetworkIdentity to the player prefab."); - return; - } - - if (conn.identity != null) - { - Debug.LogError("There is already a player for this connection."); - return; - } - - OnServerAddPlayer(conn); - } - - void OnClientConnectInternal() - { - //Debug.Log("NetworkManager.OnClientConnectInternal"); - - if (authenticator != null) - { - // we have an authenticator - let it handle authentication - authenticator.OnClientAuthenticate(); - } - else - { - // authenticate immediately - OnClientAuthenticated(); - } - } - - // called after successful authentication - void OnClientAuthenticated() - { - //Debug.Log("NetworkManager.OnClientAuthenticated"); - - // set connection to authenticated - NetworkClient.connection.isAuthenticated = true; - - // Set flag to wait for scene change? - if (string.IsNullOrWhiteSpace(onlineScene) || onlineScene == offlineScene || Utils.IsSceneActive(onlineScene)) - { - clientLoadedScene = false; - } - else - { - // Scene message expected from server. - clientLoadedScene = true; - clientReadyConnection = NetworkClient.connection; - } - - // Call virtual method regardless of whether a scene change is expected or not. - OnClientConnect(); - } - - // Transport callback, invoked after client fully disconnected. - // the call order should always be: - // Disconnect() -> ask Transport -> Transport.OnDisconnected -> Cleanup - void OnClientDisconnectInternal() - { - //Debug.Log("NetworkManager.OnClientDisconnectInternal"); - - // Only let this run once. StopClient in Host mode changes to ServerOnly - if (mode == NetworkManagerMode.ServerOnly || mode == NetworkManagerMode.Offline) - return; - - // user callback - OnClientDisconnect(); - - if (authenticator != null) - { - authenticator.OnClientAuthenticated.RemoveListener(OnClientAuthenticated); - authenticator.OnStopClient(); - } - - // set mode BEFORE changing scene so FinishStartScene doesn't re-initialize anything. - // set mode BEFORE NetworkClient.Disconnect so StopClient only runs once. - // set mode BEFORE OnStopClient so StopClient only runs once. - // If we got here from StopClient in Host mode, change to ServerOnly. - // - If StopHost was called, StopServer will put us in Offline mode. - if (mode == NetworkManagerMode.Host) - mode = NetworkManagerMode.ServerOnly; - else - mode = NetworkManagerMode.Offline; - - //Debug.Log("NetworkManager StopClient"); - OnStopClient(); - - // shutdown client - NetworkClient.Shutdown(); - - // Exit here if we're now in ServerOnly mode (StopClient called in Host mode). - if (mode == NetworkManagerMode.ServerOnly) return; - - // Get Network Manager out of DDOL before going to offline scene - // to avoid collision and let a fresh Network Manager be created. - // IMPORTANT: .gameObject can be null if StopClient is called from - // OnApplicationQuit or from tests! - if (gameObject != null - && gameObject.scene.name == "DontDestroyOnLoad" - && !string.IsNullOrWhiteSpace(offlineScene) - && SceneManager.GetActiveScene().path != offlineScene) - SceneManager.MoveGameObjectToScene(gameObject, SceneManager.GetActiveScene()); - - // If StopHost called in Host mode, StopServer will change scenes after this. - // Check loadingSceneAsync to ensure we don't double-invoke the scene change. - // Check if NetworkServer.active because we can get here via Disconnect before server has started to change scenes. - if (!string.IsNullOrWhiteSpace(offlineScene) && !Utils.IsSceneActive(offlineScene) && loadingSceneAsync == null && !NetworkServer.active) - { - ClientChangeScene(offlineScene, SceneOperation.Normal); - } - - networkSceneName = ""; - } - - void OnClientNotReadyMessageInternal(NotReadyMessage msg) - { - //Debug.Log("NetworkManager.OnClientNotReadyMessageInternal"); - NetworkClient.ready = false; - OnClientNotReady(); - - // NOTE: clientReadyConnection is not set here! don't want OnClientConnect to be invoked again after scene changes. - } - - void OnClientSceneInternal(SceneMessage msg) - { - //Debug.Log("NetworkManager.OnClientSceneInternal"); - - // This needs to run for host client too. NetworkServer.active is checked there - if (NetworkClient.isConnected) - { - ClientChangeScene(msg.sceneName, msg.sceneOperation, msg.customHandling); - } - } - - /// Called on the server when a new client connects. - public virtual void OnServerConnect(NetworkConnectionToClient conn) {} - - /// Called on the server when a client disconnects. - // Called by NetworkServer.OnTransportDisconnect! - public virtual void OnServerDisconnect(NetworkConnectionToClient conn) - { - // by default, this function destroys the connection's player. - // can be overwritten for cases like delayed logouts in MMOs to - // avoid players escaping from PvP situations by logging out. - NetworkServer.DestroyPlayerForConnection(conn); - //Debug.Log("OnServerDisconnect: Client disconnected."); - } - - /// Called on the server when a client is ready (= loaded the scene) - public virtual void OnServerReady(NetworkConnectionToClient conn) - { - if (conn.identity == null) - { - // this is now allowed (was not for a while) - //Debug.Log("Ready with no player object"); - } - NetworkServer.SetClientReady(conn); - } - - /// Called on server when a client requests to add the player. Adds playerPrefab by default. Can be overwritten. - // The default implementation for this function creates a new player object from the playerPrefab. - public virtual void OnServerAddPlayer(NetworkConnectionToClient conn) - { - Transform startPos = GetStartPosition(); - GameObject player = startPos != null - ? Instantiate(playerPrefab, startPos.position, startPos.rotation) - : Instantiate(playerPrefab); - - // instantiating a "Player" prefab gives it the name "Player(clone)" - // => appending the connectionId is WAY more useful for debugging! - player.name = $"{playerPrefab.name} [connId={conn.connectionId}]"; - NetworkServer.AddPlayerForConnection(conn, player); - } - - // Deprecated 2022-05-12 - [Obsolete("OnServerError(conn, Exception) was changed to OnServerError(conn, TransportError, string)")] - public virtual void OnServerError(NetworkConnectionToClient conn, Exception exception) {} - /// Called on server when transport raises an exception. NetworkConnection may be null. - public virtual void OnServerError(NetworkConnectionToClient conn, TransportError error, string reason) - { + } + + bool InitializeSingleton() + { + if (singleton != null && singleton == this) + return true; + + if (dontDestroyOnLoad) + { + if (singleton != null) + { + Debug.LogWarning("Multiple NetworkManagers detected in the scene. Only one NetworkManager can exist at a time. The duplicate NetworkManager will be destroyed."); + Destroy(gameObject); + + // Return false to not allow collision-destroyed second instance to continue. + return false; + } + //Debug.Log("NetworkManager created singleton (DontDestroyOnLoad)"); + singleton = this; + if (Application.isPlaying) + { + // Force the object to scene root, in case user made it a child of something + // in the scene since DDOL is only allowed for scene root objects + transform.SetParent(null); + DontDestroyOnLoad(gameObject); + } + } + else + { + //Debug.Log("NetworkManager created singleton (ForScene)"); + singleton = this; + } + + // set active transport AFTER setting singleton. + // so only if we didn't destroy ourselves. + Transport.active = transport; + return true; + } + + void RegisterServerMessages() + { + NetworkServer.OnConnectedEvent = OnServerConnectInternal; + NetworkServer.OnDisconnectedEvent = OnServerDisconnect; + NetworkServer.OnErrorEvent = OnServerError; + NetworkServer.RegisterHandler(OnServerAddPlayerInternal); + + // Network Server initially registers its own handler for this, so we replace it here. + NetworkServer.ReplaceHandler(OnServerReadyMessageInternal); + } + + void RegisterClientMessages() + { + NetworkClient.OnConnectedEvent = OnClientConnectInternal; + NetworkClient.OnDisconnectedEvent = OnClientDisconnectInternal; + NetworkClient.OnErrorEvent = OnClientError; + NetworkClient.RegisterHandler(OnClientNotReadyMessageInternal); + NetworkClient.RegisterHandler(OnClientSceneInternal, false); + + if (playerPrefab != null) + NetworkClient.RegisterPrefab(playerPrefab); + + foreach (GameObject prefab in spawnPrefabs.Where(t => t != null)) + NetworkClient.RegisterPrefab(prefab); + } + + // This is the only way to clear the singleton, so another instance can be created. + // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + public static void ResetStatics() + { + // call StopHost if we have a singleton + if (singleton) + singleton.StopHost(); + + // reset all statics + startPositions.Clear(); + startPositionIndex = 0; + clientReadyConnection = null; + loadingSceneAsync = null; + networkSceneName = string.Empty; + + // and finally (in case it isn't null already)... + singleton = null; + } + + // virtual so that inheriting classes' OnDestroy() can call base.OnDestroy() too + public virtual void OnDestroy() + { + //Debug.Log("NetworkManager destroyed"); + } + + /// The name of the current network scene. + // set by NetworkManager when changing the scene. + // new clients will automatically load this scene. + // Loading a scene manually won't set it. + public static string networkSceneName { get; protected set; } = ""; + + public static AsyncOperation loadingSceneAsync; + + /// Change the server scene and all client's scenes across the network. + // Called automatically if onlineScene or offlineScene are set, but it + // can be called from user code to switch scenes again while the game is + // in progress. This automatically sets clients to be not-ready during + // the change and ready again to participate in the new scene. + public virtual void ServerChangeScene(string newSceneName) + { + if (string.IsNullOrWhiteSpace(newSceneName)) + { + Debug.LogError("ServerChangeScene empty scene name"); + return; + } + + if (NetworkServer.isLoadingScene && newSceneName == networkSceneName) + { + Debug.LogError($"Scene change is already in progress for {newSceneName}"); + return; + } + + // Debug.Log($"ServerChangeScene {newSceneName}"); + NetworkServer.SetAllClientsNotReady(); + networkSceneName = newSceneName; + + // Let server prepare for scene change + OnServerChangeScene(newSceneName); + + // set server flag to stop processing messages while changing scenes + // it will be re-enabled in FinishLoadScene. + NetworkServer.isLoadingScene = true; + + loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName); + + // ServerChangeScene can be called when stopping the server + // when this happens the server is not active so does not need to tell clients about the change + if (NetworkServer.active) + { + // notify all clients about the new scene + NetworkServer.SendToAll(new SceneMessage + { + sceneName = newSceneName + }); + } + + startPositionIndex = 0; + startPositions.Clear(); + } + + // This is only set in ClientChangeScene below...never on server. + // We need to check this in OnClientSceneChanged called from FinishLoadSceneClientOnly + // to prevent AddPlayer message after loading/unloading additive scenes + SceneOperation clientSceneOperation = SceneOperation.Normal; + + internal void ClientChangeScene(string newSceneName, SceneOperation sceneOperation = SceneOperation.Normal, bool customHandling = false) + { + if (string.IsNullOrWhiteSpace(newSceneName)) + { + Debug.LogError("ClientChangeScene empty scene name"); + return; + } + + //Debug.Log($"ClientChangeScene newSceneName: {newSceneName} networkSceneName{networkSceneName}"); + + // Let client prepare for scene change + OnClientChangeScene(newSceneName, sceneOperation, customHandling); + + // After calling OnClientChangeScene, exit if server since server is already doing + // the actual scene change, and we don't need to do it for the host client + if (NetworkServer.active) + return; + + // set client flag to stop processing messages while loading scenes. + // otherwise we would process messages and then lose all the state + // as soon as the load is finishing, causing all kinds of bugs + // because of missing state. + // (client may be null after StopClient etc.) + // Debug.Log("ClientChangeScene: pausing handlers while scene is loading to avoid data loss after scene was loaded."); + NetworkClient.isLoadingScene = true; + + // Cache sceneOperation so we know what was requested by the + // Scene message in OnClientChangeScene and OnClientSceneChanged + clientSceneOperation = sceneOperation; + + // scene handling will happen in overrides of OnClientChangeScene and/or OnClientSceneChanged + // Do not call FinishLoadScene here. Custom handler will assign loadingSceneAsync and we need + // to wait for that to finish. UpdateScene already checks for that to be not null and isDone. + if (customHandling) + return; + + switch (sceneOperation) + { + case SceneOperation.Normal: + loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName); + break; + case SceneOperation.LoadAdditive: + // Ensure additive scene is not already loaded on client by name or path + // since we don't know which was passed in the Scene message + if (!SceneManager.GetSceneByName(newSceneName).IsValid() && !SceneManager.GetSceneByPath(newSceneName).IsValid()) + loadingSceneAsync = SceneManager.LoadSceneAsync(newSceneName, LoadSceneMode.Additive); + else + { + Debug.LogWarning($"Scene {newSceneName} is already loaded"); + + // Reset the flag that we disabled before entering this switch + NetworkClient.isLoadingScene = false; + } + break; + case SceneOperation.UnloadAdditive: + // Ensure additive scene is actually loaded on client by name or path + // since we don't know which was passed in the Scene message + if (SceneManager.GetSceneByName(newSceneName).IsValid() || SceneManager.GetSceneByPath(newSceneName).IsValid()) + loadingSceneAsync = SceneManager.UnloadSceneAsync(newSceneName, UnloadSceneOptions.UnloadAllEmbeddedSceneObjects); + else + { + Debug.LogWarning($"Cannot unload {newSceneName} with UnloadAdditive operation"); + + // Reset the flag that we disabled before entering this switch + NetworkClient.isLoadingScene = false; + } + break; + } + + // don't change the client's current networkSceneName when loading additive scene content + if (sceneOperation == SceneOperation.Normal) + networkSceneName = newSceneName; + } + + // support additive scene loads: + // NetworkScenePostProcess disables all scene objects on load, and + // * NetworkServer.SpawnObjects enables them again on the server when + // calling OnStartServer + // * NetworkClient.PrepareToSpawnSceneObjects enables them again on the + // client after the server sends ObjectSpawnStartedMessage to client + // in SpawnObserversForConnection. this is only called when the + // client joins, so we need to rebuild scene objects manually again + // TODO merge this with FinishLoadScene()? + void OnSceneLoaded(Scene scene, LoadSceneMode mode) + { + if (mode == LoadSceneMode.Additive) + { + if (NetworkServer.active) + { + // TODO only respawn the server objects from that scene later! + NetworkServer.SpawnObjects(); + // Debug.Log($"Respawned Server objects after additive scene load: {scene.name}"); + } + if (NetworkClient.active) + { + NetworkClient.PrepareToSpawnSceneObjects(); + // Debug.Log($"Rebuild Client spawnableObjects after additive scene load: {scene.name}"); + } + } + } + + void UpdateScene() + { + if (loadingSceneAsync != null && loadingSceneAsync.isDone) + { + //Debug.Log($"ClientChangeScene done readyConn {clientReadyConnection}"); + + // try-finally to guarantee loadingSceneAsync being cleared. + // fixes https://github.com/vis2k/Mirror/issues/2517 where if + // FinishLoadScene throws an exception, loadingSceneAsync would + // never be cleared and this code would run every Update. + try + { + FinishLoadScene(); + } + finally + { + loadingSceneAsync.allowSceneActivation = true; + loadingSceneAsync = null; + } + } + } + + protected void FinishLoadScene() + { + // NOTE: this cannot use NetworkClient.allClients[0] - that client may be for a completely different purpose. + + // process queued messages that we received while loading the scene + //Debug.Log("FinishLoadScene: resuming handlers after scene was loading."); + NetworkServer.isLoadingScene = false; + NetworkClient.isLoadingScene = false; + + // host mode? + if (mode == NetworkManagerMode.Host) + { + FinishLoadSceneHost(); + } + // server-only mode? + else if (mode == NetworkManagerMode.ServerOnly) + { + FinishLoadSceneServerOnly(); + } + // client-only mode? + else if (mode == NetworkManagerMode.ClientOnly) + { + FinishLoadSceneClientOnly(); + } + // otherwise we called it after stopping when loading offline scene. + // do nothing then. + } + + // finish load scene part for host mode. makes code easier and is + // necessary for FinishStartHost later. + // (the 3 things have to happen in that exact order) + void FinishLoadSceneHost() + { + // debug message is very important. if we ever break anything then + // it's very obvious to notice. + //Debug.Log("Finished loading scene in host mode."); + + if (clientReadyConnection != null) + { + clientLoadedScene = true; + clientReadyConnection = null; + } + + // do we need to finish a StartHost() call? + // then call FinishStartHost and let it take care of spawning etc. + if (finishStartHostPending) + { + finishStartHostPending = false; + FinishStartHost(); + + // call OnServerSceneChanged + OnServerSceneChanged(networkSceneName); + + // DO NOT call OnClientSceneChanged here. + // the scene change happened because StartHost loaded the + // server's online scene. it has nothing to do with the client. + // this was not meant as a client scene load, so don't call it. + // + // otherwise AddPlayer would be called twice: + // -> once for client OnConnected + // -> once in OnClientSceneChanged + } + // otherwise we just changed a scene in host mode + else + { + // spawn server objects + NetworkServer.SpawnObjects(); + + // call OnServerSceneChanged + OnServerSceneChanged(networkSceneName); + + if (NetworkClient.isConnected) + OnClientSceneChanged(); + } + } + + // finish load scene part for server-only. . makes code easier and is + // necessary for FinishStartServer later. + void FinishLoadSceneServerOnly() + { + // debug message is very important. if we ever break anything then + // it's very obvious to notice. + //Debug.Log("Finished loading scene in server-only mode."); + + NetworkServer.SpawnObjects(); + OnServerSceneChanged(networkSceneName); + } + + // finish load scene part for client-only. makes code easier and is + // necessary for FinishStartClient later. + void FinishLoadSceneClientOnly() + { + // debug message is very important. if we ever break anything then + // it's very obvious to notice. + //Debug.Log("Finished loading scene in client-only mode."); + + if (clientReadyConnection != null) + { + clientLoadedScene = true; + clientReadyConnection = null; + } + + if (NetworkClient.isConnected) + OnClientSceneChanged(); + } + + /// + /// Registers the transform of a game object as a player spawn location. + /// This is done automatically by NetworkStartPosition components, but can be done manually from user script code. + /// + /// Transform to register. + // Static because it's called from NetworkStartPosition::Awake + // and singleton may not exist yet + public static void RegisterStartPosition(Transform start) + { + // Debug.Log($"RegisterStartPosition: {start.gameObject.name} {start.position}"); + startPositions.Add(start); + + // reorder the list so that round-robin spawning uses the start positions + // in hierarchy order. This assumes all objects with NetworkStartPosition + // component are siblings, either in the scene root or together as children + // under a single parent in the scene. + startPositions = startPositions.OrderBy(transform => transform.GetSiblingIndex()).ToList(); + } + + /// Unregister a Transform from start positions. + // Static because it's called from NetworkStartPosition::OnDestroy + // and singleton may not exist yet + public static void UnRegisterStartPosition(Transform start) + { + //Debug.Log($"UnRegisterStartPosition: {start.name} {start.position}"); + startPositions.Remove(start); + } + + /// Get the next NetworkStartPosition based on the selected PlayerSpawnMethod. + public virtual Transform GetStartPosition() + { + // first remove any dead transforms + startPositions.RemoveAll(t => t == null); + + if (startPositions.Count == 0) + return null; + + if (playerSpawnMethod == PlayerSpawnMethod.Random) + { + return startPositions[UnityEngine.Random.Range(0, startPositions.Count)]; + } + else + { + Transform startPosition = startPositions[startPositionIndex]; + startPositionIndex = (startPositionIndex + 1) % startPositions.Count; + return startPosition; + } + } + + void OnServerConnectInternal(NetworkConnectionToClient conn) + { + //Debug.Log("NetworkManager.OnServerConnectInternal"); + + if (authenticator != null) + { + // we have an authenticator - let it handle authentication + authenticator.OnServerAuthenticate(conn); + } + else + { + // authenticate immediately + OnServerAuthenticated(conn); + } + } + + // called after successful authentication + // TODO do the NetworkServer.OnAuthenticated thing from x branch + void OnServerAuthenticated(NetworkConnectionToClient conn) + { + //Debug.Log("NetworkManager.OnServerAuthenticated"); + + // set connection to authenticated + conn.isAuthenticated = true; + + // proceed with the login handshake by calling OnServerConnect + if (networkSceneName != "" && networkSceneName != offlineScene) + { + SceneMessage msg = new SceneMessage() + { + sceneName = networkSceneName + }; + conn.Send(msg); + } + + OnServerConnect(conn); + } + + void OnServerReadyMessageInternal(NetworkConnectionToClient conn, ReadyMessage msg) + { + //Debug.Log("NetworkManager.OnServerReadyMessageInternal"); + OnServerReady(conn); + } + + void OnServerAddPlayerInternal(NetworkConnectionToClient conn, AddPlayerMessage msg) + { + //Debug.Log("NetworkManager.OnServerAddPlayer"); + + if (autoCreatePlayer && playerPrefab == null) + { + Debug.LogError("The PlayerPrefab is empty on the NetworkManager. Please setup a PlayerPrefab object."); + return; + } + + if (autoCreatePlayer && !playerPrefab.TryGetComponent(out NetworkIdentity _)) + { + Debug.LogError("The PlayerPrefab does not have a NetworkIdentity. Please add a NetworkIdentity to the player prefab."); + return; + } + + if (conn.identity != null) + { + Debug.LogError("There is already a player for this connection."); + return; + } + + OnServerAddPlayer(conn); + } + + void OnClientConnectInternal() + { + //Debug.Log("NetworkManager.OnClientConnectInternal"); + + if (authenticator != null) + { + // we have an authenticator - let it handle authentication + authenticator.OnClientAuthenticate(); + } + else + { + // authenticate immediately + OnClientAuthenticated(); + } + } + + // called after successful authentication + void OnClientAuthenticated() + { + //Debug.Log("NetworkManager.OnClientAuthenticated"); + + // set connection to authenticated + NetworkClient.connection.isAuthenticated = true; + + // Set flag to wait for scene change? + if (string.IsNullOrWhiteSpace(onlineScene) || onlineScene == offlineScene || Utils.IsSceneActive(onlineScene)) + { + clientLoadedScene = false; + } + else + { + // Scene message expected from server. + clientLoadedScene = true; + clientReadyConnection = NetworkClient.connection; + } + + // Call virtual method regardless of whether a scene change is expected or not. + OnClientConnect(); + } + + // Transport callback, invoked after client fully disconnected. + // the call order should always be: + // Disconnect() -> ask Transport -> Transport.OnDisconnected -> Cleanup + void OnClientDisconnectInternal() + { + //Debug.Log("NetworkManager.OnClientDisconnectInternal"); + + // Only let this run once. StopClient in Host mode changes to ServerOnly + if (mode == NetworkManagerMode.ServerOnly || mode == NetworkManagerMode.Offline) + return; + + // user callback + OnClientDisconnect(); + + if (authenticator != null) + { + authenticator.OnClientAuthenticated.RemoveListener(OnClientAuthenticated); + authenticator.OnStopClient(); + } + + // set mode BEFORE changing scene so FinishStartScene doesn't re-initialize anything. + // set mode BEFORE NetworkClient.Disconnect so StopClient only runs once. + // set mode BEFORE OnStopClient so StopClient only runs once. + // If we got here from StopClient in Host mode, change to ServerOnly. + // - If StopHost was called, StopServer will put us in Offline mode. + if (mode == NetworkManagerMode.Host) + mode = NetworkManagerMode.ServerOnly; + else + mode = NetworkManagerMode.Offline; + + //Debug.Log("NetworkManager StopClient"); + OnStopClient(); + + // shutdown client + NetworkClient.Shutdown(); + + // Exit here if we're now in ServerOnly mode (StopClient called in Host mode). + if (mode == NetworkManagerMode.ServerOnly) return; + + // Get Network Manager out of DDOL before going to offline scene + // to avoid collision and let a fresh Network Manager be created. + // IMPORTANT: .gameObject can be null if StopClient is called from + // OnApplicationQuit or from tests! + if (gameObject != null + && gameObject.scene.name == "DontDestroyOnLoad" + && !string.IsNullOrWhiteSpace(offlineScene) + && SceneManager.GetActiveScene().path != offlineScene) + SceneManager.MoveGameObjectToScene(gameObject, SceneManager.GetActiveScene()); + + // If StopHost called in Host mode, StopServer will change scenes after this. + // Check loadingSceneAsync to ensure we don't double-invoke the scene change. + // Check if NetworkServer.active because we can get here via Disconnect before server has started to change scenes. + if (!string.IsNullOrWhiteSpace(offlineScene) && !Utils.IsSceneActive(offlineScene) && loadingSceneAsync == null && !NetworkServer.active) + { + ClientChangeScene(offlineScene, SceneOperation.Normal); + } + + networkSceneName = ""; + } + + void OnClientNotReadyMessageInternal(NotReadyMessage msg) + { + //Debug.Log("NetworkManager.OnClientNotReadyMessageInternal"); + NetworkClient.ready = false; + OnClientNotReady(); + + // NOTE: clientReadyConnection is not set here! don't want OnClientConnect to be invoked again after scene changes. + } + + void OnClientSceneInternal(SceneMessage msg) + { + //Debug.Log("NetworkManager.OnClientSceneInternal"); + + // This needs to run for host client too. NetworkServer.active is checked there + if (NetworkClient.isConnected) + { + ClientChangeScene(msg.sceneName, msg.sceneOperation, msg.customHandling); + } + } + + /// Called on the server when a new client connects. + public virtual void OnServerConnect(NetworkConnectionToClient conn) { } + + /// Called on the server when a client disconnects. + // Called by NetworkServer.OnTransportDisconnect! + public virtual void OnServerDisconnect(NetworkConnectionToClient conn) + { + // by default, this function destroys the connection's player. + // can be overwritten for cases like delayed logouts in MMOs to + // avoid players escaping from PvP situations by logging out. + NetworkServer.DestroyPlayerForConnection(conn); + //Debug.Log("OnServerDisconnect: Client disconnected."); + } + + /// Called on the server when a client is ready (= loaded the scene) + public virtual void OnServerReady(NetworkConnectionToClient conn) + { + if (conn.identity == null) + { + // this is now allowed (was not for a while) + //Debug.Log("Ready with no player object"); + } + NetworkServer.SetClientReady(conn); + } + + /// Called on server when a client requests to add the player. Adds playerPrefab by default. Can be overwritten. + // The default implementation for this function creates a new player object from the playerPrefab. + public virtual void OnServerAddPlayer(NetworkConnectionToClient conn) + { + Transform startPos = GetStartPosition(); + GameObject player = startPos != null + ? Instantiate(playerPrefab, startPos.position, startPos.rotation) + : Instantiate(playerPrefab); + + // instantiating a "Player" prefab gives it the name "Player(clone)" + // => appending the connectionId is WAY more useful for debugging! + player.name = $"{playerPrefab.name} [connId={conn.connectionId}]"; + NetworkServer.AddPlayerForConnection(conn, player); + } + + // Deprecated 2022-05-12 + [Obsolete("OnServerError(conn, Exception) was changed to OnServerError(conn, TransportError, string)")] + public virtual void OnServerError(NetworkConnectionToClient conn, Exception exception) { } + /// Called on server when transport raises an exception. NetworkConnection may be null. + public virtual void OnServerError(NetworkConnectionToClient conn, TransportError error, string reason) + { #pragma warning disable CS0618 - OnServerError(conn, new Exception(reason)); + OnServerError(conn, new Exception(reason)); #pragma warning restore CS0618 - } + } - /// Called from ServerChangeScene immediately before SceneManager.LoadSceneAsync is executed - public virtual void OnServerChangeScene(string newSceneName) {} + /// Called from ServerChangeScene immediately before SceneManager.LoadSceneAsync is executed + public virtual void OnServerChangeScene(string newSceneName) { } - /// Called on server after a scene load with ServerChangeScene() is completed. - public virtual void OnServerSceneChanged(string sceneName) {} + /// Called on server after a scene load with ServerChangeScene() is completed. + public virtual void OnServerSceneChanged(string sceneName) { } - /// Called on the client when connected to a server. By default it sets client as ready and adds a player. - public virtual void OnClientConnect() - { - // OnClientConnect by default calls AddPlayer but it should not do - // that when we have online/offline scenes. so we need the - // clientLoadedScene flag to prevent it. - if (!clientLoadedScene) - { - // Ready/AddPlayer is usually triggered by a scene load completing. - // if no scene was loaded, then Ready/AddPlayer it here instead. - if (!NetworkClient.ready) - NetworkClient.Ready(); + /// Called on the client when connected to a server. By default it sets client as ready and adds a player. + public virtual void OnClientConnect() + { + // OnClientConnect by default calls AddPlayer but it should not do + // that when we have online/offline scenes. so we need the + // clientLoadedScene flag to prevent it. + if (!clientLoadedScene) + { + // Ready/AddPlayer is usually triggered by a scene load completing. + // if no scene was loaded, then Ready/AddPlayer it here instead. + if (!NetworkClient.ready) + NetworkClient.Ready(); - if (autoCreatePlayer) - NetworkClient.AddPlayer(); - } - } + if (autoCreatePlayer) + NetworkClient.AddPlayer(); + } + } - /// Called on clients when disconnected from a server. - public virtual void OnClientDisconnect() {} + /// Called on clients when disconnected from a server. + public virtual void OnClientDisconnect() { } - // Deprecated 2022-05-12 - [Obsolete("OnClientError(Exception) was changed to OnClientError(TransportError, string)")] - public virtual void OnClientError(Exception exception) {} - /// Called on client when transport raises an exception. - public virtual void OnClientError(TransportError error, string reason) - { + // Deprecated 2022-05-12 + [Obsolete("OnClientError(Exception) was changed to OnClientError(TransportError, string)")] + public virtual void OnClientError(Exception exception) { } + /// Called on client when transport raises an exception. + public virtual void OnClientError(TransportError error, string reason) + { #pragma warning disable CS0618 - OnClientError(new Exception(reason)); + OnClientError(new Exception(reason)); #pragma warning restore CS0618 - } + } - /// Called on clients when a servers tells the client it is no longer ready, e.g. when switching scenes. - public virtual void OnClientNotReady() {} + /// Called on clients when a servers tells the client it is no longer ready, e.g. when switching scenes. + public virtual void OnClientNotReady() { } - /// Called from ClientChangeScene immediately before SceneManager.LoadSceneAsync is executed - // customHandling: indicates if scene loading will be handled through overrides - public virtual void OnClientChangeScene(string newSceneName, SceneOperation sceneOperation, bool customHandling) {} + /// Called from ClientChangeScene immediately before SceneManager.LoadSceneAsync is executed + // customHandling: indicates if scene loading will be handled through overrides + public virtual void OnClientChangeScene(string newSceneName, SceneOperation sceneOperation, bool customHandling) { } - /// Called on clients when a scene has completed loaded, when the scene load was initiated by the server. - // Scene changes can cause player objects to be destroyed. The default - // implementation of OnClientSceneChanged in the NetworkManager is to - // add a player object for the connection if no player object exists. - public virtual void OnClientSceneChanged() - { - // always become ready. - if (NetworkClient.connection.isAuthenticated && !NetworkClient.ready) NetworkClient.Ready(); + /// Called on clients when a scene has completed loaded, when the scene load was initiated by the server. + // Scene changes can cause player objects to be destroyed. The default + // implementation of OnClientSceneChanged in the NetworkManager is to + // add a player object for the connection if no player object exists. + public virtual void OnClientSceneChanged() + { + // always become ready. + if (NetworkClient.connection.isAuthenticated && !NetworkClient.ready) NetworkClient.Ready(); - // Only call AddPlayer for normal scene changes, not additive load/unload - if (NetworkClient.connection.isAuthenticated && clientSceneOperation == SceneOperation.Normal && autoCreatePlayer && NetworkClient.localPlayer == null) - { - // add player if existing one is null - NetworkClient.AddPlayer(); - } - } + // Only call AddPlayer for normal scene changes, not additive load/unload + if (NetworkClient.connection.isAuthenticated && clientSceneOperation == SceneOperation.Normal && autoCreatePlayer && NetworkClient.localPlayer == null) + { + // add player if existing one is null + NetworkClient.AddPlayer(); + } + } - // Since there are multiple versions of StartServer, StartClient and - // StartHost, to reliably customize their functionality, users would - // need override all the versions. Instead these callbacks are invoked - // from all versions, so users only need to implement this one case. + // Since there are multiple versions of StartServer, StartClient and + // StartHost, to reliably customize their functionality, users would + // need override all the versions. Instead these callbacks are invoked + // from all versions, so users only need to implement this one case. - /// This is invoked when a host is started. - public virtual void OnStartHost() {} + /// This is invoked when a host is started. + public virtual void OnStartHost() { } - /// This is invoked when a server is started - including when a host is started. - public virtual void OnStartServer() {} + /// This is invoked when a server is started - including when a host is started. + public virtual void OnStartServer() { } - /// This is invoked when the client is started. - public virtual void OnStartClient() {} + /// This is invoked when the client is started. + public virtual void OnStartClient() { } - /// This is called when a server is stopped - including when a host is stopped. - public virtual void OnStopServer() {} + /// This is called when a server is stopped - including when a host is stopped. + public virtual void OnStopServer() { } - /// This is called when a client is stopped. - public virtual void OnStopClient() {} + /// This is called when a client is stopped. + public virtual void OnStopClient() { } - /// This is called when a host is stopped. - public virtual void OnStopHost() {} + /// This is called when a host is stopped. + public virtual void OnStopHost() { } - // keep OnGUI even in builds. useful to debug snap interp. - void OnGUI() - { - if (!timeInterpolationGui) return; - NetworkClient.OnGUI(); - } - } + // keep OnGUI even in builds. useful to debug snap interp. + void OnGUI() + { + if (!timeInterpolationGui) return; + NetworkClient.OnGUI(); + } + } } diff --git a/Assets/Mirror/Core/NetworkServer.cs b/Assets/Mirror/Core/NetworkServer.cs index 12cf05577..b6a781e01 100644 --- a/Assets/Mirror/Core/NetworkServer.cs +++ b/Assets/Mirror/Core/NetworkServer.cs @@ -6,1826 +6,1826 @@ namespace Mirror { - /// NetworkServer handles remote connections and has a local connection for a local client. - public static partial class NetworkServer - { - static bool initialized; - public static int maxConnections; + /// NetworkServer handles remote connections and has a local connection for a local client. + public static partial class NetworkServer + { + static bool initialized; + public static int maxConnections; - /// Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE. - // overwritten by NetworkManager (if any) - public static int tickRate = 30; + /// Server Update frequency, per second. Use around 60Hz for fast paced games like Counter-Strike to minimize latency. Use around 30Hz for games like WoW to minimize computations. Use around 1-10Hz for slow paced games like EVE. + // overwritten by NetworkManager (if any) + public static int tickRate = 30; - // tick rate is in Hz. - // convert to interval in seconds for convenience where needed. - // - // send interval is 1 / sendRate. - // but for tests we need a way to set it to exactly 0. - // 1 / int.max would not be exactly 0, so handel that manually. - public static float tickInterval => tickRate < int.MaxValue ? 1f / tickRate : 0; // for 30 Hz, that's 33ms + // tick rate is in Hz. + // convert to interval in seconds for convenience where needed. + // + // send interval is 1 / sendRate. + // but for tests we need a way to set it to exactly 0. + // 1 / int.max would not be exactly 0, so handel that manually. + public static float tickInterval => tickRate < int.MaxValue ? 1f / tickRate : 0; // for 30 Hz, that's 33ms - // time & value snapshot interpolation are separate. - // -> time is interpolated globally on NetworkClient / NetworkConnection - // -> value is interpolated per-component, i.e. NetworkTransform. - // however, both need to be on the same send interval. - public static int sendRate => tickRate; - public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms - static double lastSendTime; + // time & value snapshot interpolation are separate. + // -> time is interpolated globally on NetworkClient / NetworkConnection + // -> value is interpolated per-component, i.e. NetworkTransform. + // however, both need to be on the same send interval. + public static int sendRate => tickRate; + public static float sendInterval => sendRate < int.MaxValue ? 1f / sendRate : 0; // for 30 Hz, that's 33ms + static double lastSendTime; - // Target time for sync = client/server time - buffer time. - // Time difference = latest time snapshot time - buffer time - // If time difference > clampingBufferMultiplier(below) * buffer time, we clamp it to within - // clampingBufferMultiplier(below) * buffer time. - public static float bufferTimeMultiplierForClamping = 1; - - /// Connection to host mode client (if any) - public static LocalConnectionToClient localConnection { get; private set; } + // Target time for sync = client/server time - buffer time. + // Time difference = latest time snapshot time - buffer time + // If time difference > clampingBufferMultiplier(below) * buffer time, we clamp it to within + // clampingBufferMultiplier(below) * buffer time. + public static float bufferTimeMultiplierForClamping = 1; - /// Dictionary of all server connections, with connectionId as key - public static Dictionary connections = - new Dictionary(); + /// Connection to host mode client (if any) + public static LocalConnectionToClient localConnection { get; private set; } - /// Message Handlers dictionary, with messageId as key - internal static Dictionary handlers = - new Dictionary(); + /// Dictionary of all server connections, with connectionId as key + public static Dictionary connections = + new Dictionary(); - /// All spawned NetworkIdentities by netId. - // server sees ALL spawned ones. - public static readonly Dictionary spawned = - new Dictionary(); + /// Message Handlers dictionary, with messageId as key + internal static Dictionary handlers = + new Dictionary(); - /// Single player mode can use dontListen to not accept incoming connections - // see also: https://github.com/vis2k/Mirror/pull/2595 - public static bool dontListen; + /// All spawned NetworkIdentities by netId. + // server sees ALL spawned ones. + public static readonly Dictionary spawned = + new Dictionary(); - // Deprecated 2022-12-12 - [Obsolete("NetworkServer.localClientActive was renamed to .activeHost to be more obvious")] - public static bool localClientActive => activeHost; + /// Single player mode can use dontListen to not accept incoming connections + // see also: https://github.com/vis2k/Mirror/pull/2595 + public static bool dontListen; - /// active checks if the server has been started either has standalone or as host server. - public static bool active { get; internal set; } + // Deprecated 2022-12-12 + [Obsolete("NetworkServer.localClientActive was renamed to .activeHost to be more obvious")] + public static bool localClientActive => activeHost; - /// active checks if the server has been started in host mode. - // naming consistent with NetworkClient.activeHost. - public static bool activeHost => localConnection != null; + /// active checks if the server has been started either has standalone or as host server. + public static bool active { get; internal set; } - // scene loading - public static bool isLoadingScene; + /// active checks if the server has been started in host mode. + // naming consistent with NetworkClient.activeHost. + public static bool activeHost => localConnection != null; - // interest management component (optional) - // by default, everyone observes everyone - public static InterestManagementBase aoi; + // scene loading + public static bool isLoadingScene; - // OnConnected / OnDisconnected used to be NetworkMessages that were - // invoked. this introduced a bug where external clients could send - // Connected/Disconnected messages over the network causing undefined - // behaviour. - // => public so that custom NetworkManagers can hook into it - public static Action OnConnectedEvent; - public static Action OnDisconnectedEvent; - public static Action OnErrorEvent; + // interest management component (optional) + // by default, everyone observes everyone + public static InterestManagementBase aoi; - // keep track of actual achieved tick rate. - // might become lower under heavy load. - // very useful for profiling etc. - // measured over 1s each, same as frame rate. no EMA here. - public static int actualTickRate; - static double actualTickRateStart; // start time when counting - static int actualTickRateCounter; // current counter since start + // OnConnected / OnDisconnected used to be NetworkMessages that were + // invoked. this introduced a bug where external clients could send + // Connected/Disconnected messages over the network causing undefined + // behaviour. + // => public so that custom NetworkManagers can hook into it + public static Action OnConnectedEvent; + public static Action OnDisconnectedEvent; + public static Action OnErrorEvent; - // profiling - // includes transport update time, because transport calls handlers etc. - // averaged over 1s by passing 'tickRate' to constructor. - public static TimeSample earlyUpdateDuration; - public static TimeSample lateUpdateDuration; + // keep track of actual achieved tick rate. + // might become lower under heavy load. + // very useful for profiling etc. + // measured over 1s each, same as frame rate. no EMA here. + public static int actualTickRate; + static double actualTickRateStart; // start time when counting + static int actualTickRateCounter; // current counter since start - // capture full Unity update time from before Early- to after LateUpdate - public static TimeSample fullUpdateDuration; + // profiling + // includes transport update time, because transport calls handlers etc. + // averaged over 1s by passing 'tickRate' to constructor. + public static TimeSample earlyUpdateDuration; + public static TimeSample lateUpdateDuration; - /// Starts server and listens to incoming connections with max connections limit. - public static void Listen(int maxConns) - { - Initialize(); - maxConnections = maxConns; + // capture full Unity update time from before Early- to after LateUpdate + public static TimeSample fullUpdateDuration; - // only start server if we want to listen - if (!dontListen) - { - Transport.active.ServerStart(); - //Debug.Log("Server started listening"); - } + /// Starts server and listens to incoming connections with max connections limit. + public static void Listen(int maxConns) + { + Initialize(); + maxConnections = maxConns; - active = true; - RegisterMessageHandlers(); - } + // only start server if we want to listen + if (!dontListen) + { + Transport.active.ServerStart(); + //Debug.Log("Server started listening"); + } - // initialization / shutdown /////////////////////////////////////////// - static void Initialize() - { - if (initialized) - return; + active = true; + RegisterMessageHandlers(); + } - // Debug.Log($"NetworkServer Created version {Version.Current}"); + // initialization / shutdown /////////////////////////////////////////// + static void Initialize() + { + if (initialized) + return; - //Make sure connections are cleared in case any old connections references exist from previous sessions - connections.Clear(); + // Debug.Log($"NetworkServer Created version {Version.Current}"); - // reset Interest Management so that rebuild intervals - // start at 0 when starting again. - if (aoi != null) aoi.Reset(); + //Make sure connections are cleared in case any old connections references exist from previous sessions + connections.Clear(); - // reset NetworkTime - NetworkTime.ResetStatics(); + // reset Interest Management so that rebuild intervals + // start at 0 when starting again. + if (aoi != null) aoi.Reset(); - Debug.Assert(Transport.active != null, "There was no active transport when calling NetworkServer.Listen, If you are calling Listen manually then make sure to set 'Transport.active' first"); - AddTransportHandlers(); + // reset NetworkTime + NetworkTime.ResetStatics(); - initialized = true; + Debug.Assert(Transport.active != null, "There was no active transport when calling NetworkServer.Listen, If you are calling Listen manually then make sure to set 'Transport.active' first"); + AddTransportHandlers(); - // profiling - earlyUpdateDuration = new TimeSample(sendRate); - lateUpdateDuration = new TimeSample(sendRate); - fullUpdateDuration = new TimeSample(sendRate); - } + initialized = true; - static void AddTransportHandlers() - { - // += so that other systems can also hook into it (i.e. statistics) - Transport.active.OnServerConnected += OnTransportConnected; - Transport.active.OnServerDataReceived += OnTransportData; - Transport.active.OnServerDisconnected += OnTransportDisconnected; - Transport.active.OnServerError += OnTransportError; - } + // profiling + earlyUpdateDuration = new TimeSample(sendRate); + lateUpdateDuration = new TimeSample(sendRate); + fullUpdateDuration = new TimeSample(sendRate); + } - /// Shuts down the server and disconnects all clients - // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] - public static void Shutdown() - { - if (initialized) - { - DisconnectAll(); + static void AddTransportHandlers() + { + // += so that other systems can also hook into it (i.e. statistics) + Transport.active.OnServerConnected += OnTransportConnected; + Transport.active.OnServerDataReceived += OnTransportData; + Transport.active.OnServerDisconnected += OnTransportDisconnected; + Transport.active.OnServerError += OnTransportError; + } - // stop the server. - // we do NOT call Transport.Shutdown, because someone only - // called NetworkServer.Shutdown. we can't assume that the - // client is supposed to be shut down too! - // - // NOTE: stop no matter what, even if 'dontListen': - // someone might enabled dontListen at runtime. - // but we still need to stop the server. - // fixes https://github.com/vis2k/Mirror/issues/2536 - Transport.active.ServerStop(); + /// Shuts down the server and disconnects all clients + // RuntimeInitializeOnLoadMethod -> fast playmode without domain reload + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] + public static void Shutdown() + { + if (initialized) + { + DisconnectAll(); - // transport handlers are hooked into when initializing. - // so only remove them when shutting down. - RemoveTransportHandlers(); + // stop the server. + // we do NOT call Transport.Shutdown, because someone only + // called NetworkServer.Shutdown. we can't assume that the + // client is supposed to be shut down too! + // + // NOTE: stop no matter what, even if 'dontListen': + // someone might enabled dontListen at runtime. + // but we still need to stop the server. + // fixes https://github.com/vis2k/Mirror/issues/2536 + Transport.active.ServerStop(); - initialized = false; - } + // transport handlers are hooked into when initializing. + // so only remove them when shutting down. + RemoveTransportHandlers(); - // Reset all statics here.... - dontListen = false; - isLoadingScene = false; - lastSendTime = 0; - actualTickRate = 0; + initialized = false; + } - localConnection = null; + // Reset all statics here.... + dontListen = false; + isLoadingScene = false; + lastSendTime = 0; + actualTickRate = 0; - connections.Clear(); - connectionsCopy.Clear(); - handlers.Clear(); + localConnection = null; - // destroy all spawned objects, _then_ set inactive. - // make sure .active is still true before calling this. - // otherwise modifying SyncLists in OnStopServer would throw - // because .IsWritable() check checks if NetworkServer.active. - // https://github.com/MirrorNetworking/Mirror/issues/3344 - CleanupSpawned(); - active = false; + connections.Clear(); + connectionsCopy.Clear(); + handlers.Clear(); - // sets nextNetworkId to 1 - // sets clientAuthorityCallback to null - // sets previousLocalPlayer to null - NetworkIdentity.ResetStatics(); + // destroy all spawned objects, _then_ set inactive. + // make sure .active is still true before calling this. + // otherwise modifying SyncLists in OnStopServer would throw + // because .IsWritable() check checks if NetworkServer.active. + // https://github.com/MirrorNetworking/Mirror/issues/3344 + CleanupSpawned(); + active = false; - // clear events. someone might have hooked into them before, but - // we don't want to use those hooks after Shutdown anymore. - OnConnectedEvent = null; - OnDisconnectedEvent = null; - OnErrorEvent = null; + // sets nextNetworkId to 1 + // sets clientAuthorityCallback to null + // sets previousLocalPlayer to null + NetworkIdentity.ResetStatics(); - if (aoi != null) aoi.Reset(); - } + // clear events. someone might have hooked into them before, but + // we don't want to use those hooks after Shutdown anymore. + OnConnectedEvent = null; + OnDisconnectedEvent = null; + OnErrorEvent = null; - static void RemoveTransportHandlers() - { - // -= so that other systems can also hook into it (i.e. statistics) - Transport.active.OnServerConnected -= OnTransportConnected; - Transport.active.OnServerDataReceived -= OnTransportData; - Transport.active.OnServerDisconnected -= OnTransportDisconnected; - Transport.active.OnServerError -= OnTransportError; - } + if (aoi != null) aoi.Reset(); + } - // Note: NetworkClient.DestroyAllClientObjects does the same on client. - static void CleanupSpawned() - { - // iterate a COPY of spawned. - // DestroyObject removes them from the original collection. - // removing while iterating is not allowed. - foreach (NetworkIdentity identity in spawned.Values.ToList()) - { - if (identity != null) - { - // scene object - if (identity.sceneId != 0) - { - // spawned scene objects are unspawned and reset. - // afterwards we disable them again. - // (they always stay in the scene, we don't destroy them) - DestroyObject(identity, DestroyMode.Reset); - identity.gameObject.SetActive(false); - } - // spawned prefabs - else - { - // spawned prefabs are unspawned and destroyed. - DestroyObject(identity, DestroyMode.Destroy); - } - } - } + static void RemoveTransportHandlers() + { + // -= so that other systems can also hook into it (i.e. statistics) + Transport.active.OnServerConnected -= OnTransportConnected; + Transport.active.OnServerDataReceived -= OnTransportData; + Transport.active.OnServerDisconnected -= OnTransportDisconnected; + Transport.active.OnServerError -= OnTransportError; + } - spawned.Clear(); - } + // Note: NetworkClient.DestroyAllClientObjects does the same on client. + static void CleanupSpawned() + { + // iterate a COPY of spawned. + // DestroyObject removes them from the original collection. + // removing while iterating is not allowed. + foreach (NetworkIdentity identity in spawned.Values.ToList()) + { + if (identity != null) + { + // scene object + if (identity.sceneId != 0) + { + // spawned scene objects are unspawned and reset. + // afterwards we disable them again. + // (they always stay in the scene, we don't destroy them) + DestroyObject(identity, DestroyMode.Reset); + identity.gameObject.SetActive(false); + } + // spawned prefabs + else + { + // spawned prefabs are unspawned and destroyed. + DestroyObject(identity, DestroyMode.Destroy); + } + } + } - internal static void RegisterMessageHandlers() - { - RegisterHandler(OnClientReadyMessage); - RegisterHandler(OnCommandMessage); - RegisterHandler(NetworkTime.OnServerPing, false); - RegisterHandler(OnEntityStateMessage, true); - RegisterHandler(OnTimeSnapshotMessage, true); - } + spawned.Clear(); + } - // remote calls //////////////////////////////////////////////////////// - // Handle command from specific player, this could be one of multiple - // players on a single client - // default ready handler. - static void OnClientReadyMessage(NetworkConnectionToClient conn, ReadyMessage msg) - { - // Debug.Log($"Default handler for ready message from {conn}"); - SetClientReady(conn); - } + internal static void RegisterMessageHandlers() + { + RegisterHandler(OnClientReadyMessage); + RegisterHandler(OnCommandMessage); + RegisterHandler(NetworkTime.OnServerPing, false); + RegisterHandler(OnEntityStateMessage, true); + RegisterHandler(OnTimeSnapshotMessage, true); + } - static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg, int channelId) - { - if (!conn.isReady) - { - // Clients may be set NotReady due to scene change or other game logic by user, e.g. respawning. - // Ignore commands that may have been in flight before client received NotReadyMessage message. - // Unreliable messages may be out of order, so don't spam warnings for those. - if (channelId == Channels.Reliable) - Debug.LogWarning("Command received while client is not ready.\nThis may be ignored if client intentionally set NotReady."); - return; - } + // remote calls //////////////////////////////////////////////////////// + // Handle command from specific player, this could be one of multiple + // players on a single client + // default ready handler. + static void OnClientReadyMessage(NetworkConnectionToClient conn, ReadyMessage msg) + { + // Debug.Log($"Default handler for ready message from {conn}"); + SetClientReady(conn); + } - if (!spawned.TryGetValue(msg.netId, out NetworkIdentity identity)) - { - // over reliable channel, commands should always come after spawn. - // over unreliable, they might come in before the object was spawned. - // for example, NetworkTransform. - // let's not spam the console for unreliable out of order messages. - if (channelId == Channels.Reliable) - Debug.LogWarning($"Spawned object not found when handling Command message [netId={msg.netId}]"); - return; - } + static void OnCommandMessage(NetworkConnectionToClient conn, CommandMessage msg, int channelId) + { + if (!conn.isReady) + { + // Clients may be set NotReady due to scene change or other game logic by user, e.g. respawning. + // Ignore commands that may have been in flight before client received NotReadyMessage message. + // Unreliable messages may be out of order, so don't spam warnings for those. + if (channelId == Channels.Reliable) + Debug.LogWarning("Command received while client is not ready.\nThis may be ignored if client intentionally set NotReady."); + return; + } - // Commands can be for player objects, OR other objects with client-authority - // -> so if this connection's controller has a different netId then - // only allow the command if clientAuthorityOwner - bool requiresAuthority = RemoteProcedureCalls.CommandRequiresAuthority(msg.functionHash); - if (requiresAuthority && identity.connectionToClient != conn) - { - Debug.LogWarning($"Command for object without authority [netId={msg.netId}]"); - return; - } + if (!spawned.TryGetValue(msg.netId, out NetworkIdentity identity)) + { + // over reliable channel, commands should always come after spawn. + // over unreliable, they might come in before the object was spawned. + // for example, NetworkTransform. + // let's not spam the console for unreliable out of order messages. + if (channelId == Channels.Reliable) + Debug.LogWarning($"Spawned object not found when handling Command message [netId={msg.netId}]"); + return; + } - // Debug.Log($"OnCommandMessage for netId:{msg.netId} conn:{conn}"); + // Commands can be for player objects, OR other objects with client-authority + // -> so if this connection's controller has a different netId then + // only allow the command if clientAuthorityOwner + bool requiresAuthority = RemoteProcedureCalls.CommandRequiresAuthority(msg.functionHash); + if (requiresAuthority && identity.connectionToClient != conn) + { + Debug.LogWarning($"Command for object without authority [netId={msg.netId}]"); + return; + } - using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(msg.payload)) - identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn); - } + // Debug.Log($"OnCommandMessage for netId:{msg.netId} conn:{conn}"); - // client to server broadcast ////////////////////////////////////////// - // for client's owned ClientToServer components. - static void OnEntityStateMessage(NetworkConnectionToClient connection, EntityStateMessage message) - { - // need to validate permissions carefully. - // an attacker may attempt to modify a not-owned or not-ClientToServer component. + using (NetworkReaderPooled networkReader = NetworkReaderPool.Get(msg.payload)) + identity.HandleRemoteCall(msg.componentIndex, msg.functionHash, RemoteCallType.Command, networkReader, conn); + } - // valid netId? - if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) - { - // owned by the connection? - if (identity.connectionToClient == connection) - { - using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) - { - // DeserializeServer checks permissions internally. - // failure to deserialize disconnects to prevent exploits. - if (!identity.DeserializeServer(reader)) - { - Debug.LogWarning($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}, Disconnecting."); - connection.Disconnect(); - } - } - } - // an attacker may attempt to modify another connection's entity - else - { - Debug.LogWarning($"Connection {connection.connectionId} attempted to modify {identity} which is not owned by the connection. Disconnecting the connection."); - connection.Disconnect(); - } - } - // no warning. don't spam server logs. - // else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); - } + // client to server broadcast ////////////////////////////////////////// + // for client's owned ClientToServer components. + static void OnEntityStateMessage(NetworkConnectionToClient connection, EntityStateMessage message) + { + // need to validate permissions carefully. + // an attacker may attempt to modify a not-owned or not-ClientToServer component. - // client sends TimeSnapshotMessage every sendInterval. - // batching already includes the remoteTimestamp. - // we simply insert it on-message here. - // => only for reliable channel. unreliable would always arrive earlier. - static void OnTimeSnapshotMessage(NetworkConnectionToClient connection, TimeSnapshotMessage _) - { - // insert another snapshot for snapshot interpolation. - // before calling OnDeserialize so components can use - // NetworkTime.time and NetworkTime.timeStamp. + // valid netId? + if (spawned.TryGetValue(message.netId, out NetworkIdentity identity) && identity != null) + { + // owned by the connection? + if (identity.connectionToClient == connection) + { + using (NetworkReaderPooled reader = NetworkReaderPool.Get(message.payload)) + { + // DeserializeServer checks permissions internally. + // failure to deserialize disconnects to prevent exploits. + if (!identity.DeserializeServer(reader)) + { + Debug.LogWarning($"Server failed to deserialize client state for {identity.name} with netId={identity.netId}, Disconnecting."); + connection.Disconnect(); + } + } + } + // an attacker may attempt to modify another connection's entity + else + { + Debug.LogWarning($"Connection {connection.connectionId} attempted to modify {identity} which is not owned by the connection. Disconnecting the connection."); + connection.Disconnect(); + } + } + // no warning. don't spam server logs. + // else Debug.LogWarning($"Did not find target for sync message for {message.netId} . Note: this can be completely normal because UDP messages may arrive out of order, so this message might have arrived after a Destroy message."); + } - // TODO validation? - // maybe we shouldn't allow timeline to deviate more than a certain %. - // for now, this is only used for client authority movement. + // client sends TimeSnapshotMessage every sendInterval. + // batching already includes the remoteTimestamp. + // we simply insert it on-message here. + // => only for reliable channel. unreliable would always arrive earlier. + static void OnTimeSnapshotMessage(NetworkConnectionToClient connection, TimeSnapshotMessage _) + { + // insert another snapshot for snapshot interpolation. + // before calling OnDeserialize so components can use + // NetworkTime.time and NetworkTime.timeStamp. + + // TODO validation? + // maybe we shouldn't allow timeline to deviate more than a certain %. + // for now, this is only used for client authority movement. #if !UNITY_2020_3_OR_NEWER // Unity 2019 doesn't have Time.timeAsDouble yet connection.OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, NetworkTime.localTime)); #else - connection.OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, Time.timeAsDouble)); + connection.OnTimeSnapshot(new TimeSnapshot(connection.remoteTimeStamp, Time.timeAsDouble)); #endif - } - - // connections ///////////////////////////////////////////////////////// - /// Add a connection and setup callbacks. Returns true if not added yet. - public static bool AddConnection(NetworkConnectionToClient conn) - { - if (!connections.ContainsKey(conn.connectionId)) - { - // connection cannot be null here or conn.connectionId - // would throw NRE - connections[conn.connectionId] = conn; - return true; - } - // already a connection with this id - return false; - } - - /// Removes a connection by connectionId. Returns true if removed. - public static bool RemoveConnection(int connectionId) => - connections.Remove(connectionId); - - // called by LocalClient to add itself. don't call directly. - // TODO consider internal setter instead? - internal static void SetLocalConnection(LocalConnectionToClient conn) - { - if (localConnection != null) - { - Debug.LogError("Local Connection already exists"); - return; - } - - localConnection = conn; - } - - // removes local connection to client - internal static void RemoveLocalConnection() - { - if (localConnection != null) - { - localConnection.Disconnect(); - localConnection = null; - } - RemoveConnection(0); - } - - /// True if we have external connections (that are not host) - public static bool HasExternalConnections() - { - // any connections? - if (connections.Count > 0) - { - // only host connection? - if (connections.Count == 1 && localConnection != null) - return false; - - // otherwise we have real external connections - return true; - } - return false; - } - - // send //////////////////////////////////////////////////////////////// - /// Send a message to all clients, even those that haven't joined the world yet (non ready) - public static void SendToAll(T message, int channelId = Channels.Reliable, bool sendToReadyOnly = false) - where T : struct, NetworkMessage - { - if (!active) - { - Debug.LogWarning("Can not send using NetworkServer.SendToAll(T msg) because NetworkServer is not active"); - return; - } - - // Debug.Log($"Server.SendToAll {typeof(T)}"); - using (NetworkWriterPooled writer = NetworkWriterPool.Get()) - { - // pack message only once - NetworkMessages.Pack(message, writer); - ArraySegment segment = writer.ToArraySegment(); - - // filter and then send to all internet connections at once - // -> makes code more complicated, but is HIGHLY worth it to - // avoid allocations, allow for multicast, etc. - int count = 0; - foreach (NetworkConnectionToClient conn in connections.Values) - { - if (sendToReadyOnly && !conn.isReady) - continue; - - count++; - conn.Send(segment, channelId); - } - - NetworkDiagnostics.OnSend(message, channelId, segment.Count, count); - } - } - - /// Send a message to all clients which have joined the world (are ready). - // TODO put rpcs into NetworkServer.Update WorldState packet, then finally remove SendToReady! - public static void SendToReady(T message, int channelId = Channels.Reliable) - where T : struct, NetworkMessage - { - if (!active) - { - Debug.LogWarning("Can not send using NetworkServer.SendToReady(T msg) because NetworkServer is not active"); - return; - } - - SendToAll(message, channelId, true); - } - - // this is like SendToReadyObservers - but it doesn't check the ready flag on the connection. - // this is used for ObjectDestroy messages. - static void SendToObservers(NetworkIdentity identity, T message, int channelId = Channels.Reliable) - where T : struct, NetworkMessage - { - // Debug.Log($"Server.SendToObservers {typeof(T)}"); - if (identity == null || identity.observers.Count == 0) - return; - - using (NetworkWriterPooled writer = NetworkWriterPool.Get()) - { - // pack message into byte[] once - NetworkMessages.Pack(message, writer); - ArraySegment segment = writer.ToArraySegment(); - - foreach (NetworkConnectionToClient conn in identity.observers.Values) - { - conn.Send(segment, channelId); - } - - NetworkDiagnostics.OnSend(message, channelId, segment.Count, identity.observers.Count); - } - } - - /// Send a message to only clients which are ready with option to include the owner of the object identity - // TODO obsolete this later. it's not used anymore - public static void SendToReadyObservers(NetworkIdentity identity, T message, bool includeOwner = true, int channelId = Channels.Reliable) - where T : struct, NetworkMessage - { - // Debug.Log($"Server.SendToReady {typeof(T)}"); - if (identity == null || identity.observers.Count == 0) - return; - - using (NetworkWriterPooled writer = NetworkWriterPool.Get()) - { - // pack message only once - NetworkMessages.Pack(message, writer); - ArraySegment segment = writer.ToArraySegment(); - - int count = 0; - foreach (NetworkConnectionToClient conn in identity.observers.Values) - { - bool isOwner = conn == identity.connectionToClient; - if ((!isOwner || includeOwner) && conn.isReady) - { - count++; - conn.Send(segment, channelId); - } - } - - NetworkDiagnostics.OnSend(message, channelId, segment.Count, count); - } - } - - /// Send a message to only clients which are ready including the owner of the NetworkIdentity - // TODO obsolete this later. it's not used anymore - public static void SendToReadyObservers(NetworkIdentity identity, T message, int channelId) - where T : struct, NetworkMessage - { - SendToReadyObservers(identity, message, true, channelId); - } - - // transport events //////////////////////////////////////////////////// - // called by transport - static void OnTransportConnected(int connectionId) - { - // Debug.Log($"Server accepted client:{connectionId}"); - - // connectionId needs to be != 0 because 0 is reserved for local player - // note that some transports like kcp generate connectionId by - // hashing which can be < 0 as well, so we need to allow < 0! - if (connectionId == 0) - { - Debug.LogError($"Server.HandleConnect: invalid connectionId: {connectionId} . Needs to be != 0, because 0 is reserved for local player."); - Transport.active.ServerDisconnect(connectionId); - return; - } - - // connectionId not in use yet? - if (connections.ContainsKey(connectionId)) - { - Transport.active.ServerDisconnect(connectionId); - // Debug.Log($"Server connectionId {connectionId} already in use...kicked client"); - return; - } - - // are more connections allowed? if not, kick - // (it's easier to handle this in Mirror, so Transports can have - // less code and third party transport might not do that anyway) - // (this way we could also send a custom 'tooFull' message later, - // Transport can't do that) - if (connections.Count < maxConnections) - { - // add connection - NetworkConnectionToClient conn = new NetworkConnectionToClient(connectionId); - OnConnected(conn); - } - else - { - // kick - Transport.active.ServerDisconnect(connectionId); - // Debug.Log($"Server full, kicked client {connectionId}"); - } - } - - internal static void OnConnected(NetworkConnectionToClient conn) - { - // Debug.Log($"Server accepted client:{conn}"); - - // add connection and invoke connected event - AddConnection(conn); - OnConnectedEvent?.Invoke(conn); - } - - static bool UnpackAndInvoke(NetworkConnectionToClient connection, NetworkReader reader, int channelId) - { - if (NetworkMessages.UnpackId(reader, out ushort msgType)) - { - // try to invoke the handler for that message - if (handlers.TryGetValue(msgType, out NetworkMessageDelegate handler)) - { - handler.Invoke(connection, reader, channelId); - connection.lastMessageTime = Time.time; - return true; - } - else - { - // message in a batch are NOT length prefixed to save bandwidth. - // every message needs to be handled and read until the end. - // otherwise it would overlap into the next message. - // => need to warn and disconnect to avoid undefined behaviour. - // => WARNING, not error. can happen if attacker sends random data. - Debug.LogWarning($"Unknown message id: {msgType} for connection: {connection}. This can happen if no handler was registered for this message."); - // simply return false. caller is responsible for disconnecting. - //connection.Disconnect(); - return false; - } - } - else - { - // => WARNING, not error. can happen if attacker sends random data. - Debug.LogWarning($"Invalid message header for connection: {connection}."); - // simply return false. caller is responsible for disconnecting. - //connection.Disconnect(); - return false; - } - } - - // called by transport - internal static void OnTransportData(int connectionId, ArraySegment data, int channelId) - { - if (connections.TryGetValue(connectionId, out NetworkConnectionToClient connection)) - { - // client might batch multiple messages into one packet. - // feed it to the Unbatcher. - // NOTE: we don't need to associate a channelId because we - // always process all messages in the batch. - if (!connection.unbatcher.AddBatch(data)) - { - Debug.LogWarning($"NetworkServer: received Message was too short (messages should start with message id)"); - connection.Disconnect(); - return; - } - - // process all messages in the batch. - // only while NOT loading a scene. - // if we get a scene change message, then we need to stop - // processing. otherwise we might apply them to the old scene. - // => fixes https://github.com/vis2k/Mirror/issues/2651 - // - // NOTE: if scene starts loading, then the rest of the batch - // would only be processed when OnTransportData is called - // the next time. - // => consider moving processing to NetworkEarlyUpdate. - while (!isLoadingScene && - connection.unbatcher.GetNextMessage(out NetworkReader reader, out double remoteTimestamp)) - { - // enough to read at least header size? - if (reader.Remaining >= NetworkMessages.IdSize) - { - // make remoteTimeStamp available to the user - connection.remoteTimeStamp = remoteTimestamp; - - // handle message - if (!UnpackAndInvoke(connection, reader, channelId)) - { - // warn, disconnect and return if failed - // -> warning because attackers might send random data - // -> messages in a batch aren't length prefixed. - // failing to read one would cause undefined - // behaviour for every message afterwards. - // so we need to disconnect. - // -> return to avoid the below unbatches.count error. - // we already disconnected and handled it. - Debug.LogWarning($"NetworkServer: failed to unpack and invoke message. Disconnecting {connectionId}."); - connection.Disconnect(); - return; - } - } - // otherwise disconnect - else - { - // WARNING, not error. can happen if attacker sends random data. - Debug.LogWarning($"NetworkServer: received Message was too short (messages should start with message id). Disconnecting {connectionId}"); - connection.Disconnect(); - return; - } - } - - // if we weren't interrupted by a scene change, - // then all batched messages should have been processed now. - // otherwise batches would silently grow. - // we need to log an error to avoid debugging hell. - // - // EXAMPLE: https://github.com/vis2k/Mirror/issues/2882 - // -> UnpackAndInvoke silently returned because no handler for id - // -> Reader would never be read past the end - // -> Batch would never be retired because end is never reached - // - // NOTE: prefixing every message in a batch with a length would - // avoid ever not reading to the end. for extra bandwidth. - // - // IMPORTANT: always keep this check to detect memory leaks. - // this took half a day to debug last time. - if (!isLoadingScene && connection.unbatcher.BatchesCount > 0) - { - Debug.LogError($"Still had {connection.unbatcher.BatchesCount} batches remaining after processing, even though processing was not interrupted by a scene change. This should never happen, as it would cause ever growing batches.\nPossible reasons:\n* A message didn't deserialize as much as it serialized\n*There was no message handler for a message id, so the reader wasn't read until the end."); - } - } - else Debug.LogError($"HandleData Unknown connectionId:{connectionId}"); - } - - // called by transport - // IMPORTANT: often times when disconnecting, we call this from Mirror - // too because we want to remove the connection and handle - // the disconnect immediately. - // => which is fine as long as we guarantee it only runs once - // => which we do by removing the connection! - internal static void OnTransportDisconnected(int connectionId) - { - // Debug.Log($"Server disconnect client:{connectionId}"); - if (connections.TryGetValue(connectionId, out NetworkConnectionToClient conn)) - { - RemoveConnection(connectionId); - // Debug.Log($"Server lost client:{connectionId}"); - - // NetworkManager hooks into OnDisconnectedEvent to make - // DestroyPlayerForConnection(conn) optional, e.g. for PvP MMOs - // where players shouldn't be able to escape combat instantly. - if (OnDisconnectedEvent != null) - { - OnDisconnectedEvent.Invoke(conn); - } - // if nobody hooked into it, then simply call DestroyPlayerForConnection - else - { - DestroyPlayerForConnection(conn); - } - } - } - - // transport errors are forwarded to high level - static void OnTransportError(int connectionId, TransportError error, string reason) - { - // transport errors will happen. logging a warning is enough. - // make sure the user does not panic. - Debug.LogWarning($"Server Transport Error for connId={connectionId}: {error}: {reason}. This is fine."); - // try get connection. passes null otherwise. - connections.TryGetValue(connectionId, out NetworkConnectionToClient conn); - OnErrorEvent?.Invoke(conn, error, reason); - } - - /// Destroys all of the connection's owned objects on the server. - // This is used when a client disconnects, to remove the players for - // that client. This also destroys non-player objects that have client - // authority set for this connection. - public static void DestroyPlayerForConnection(NetworkConnectionToClient conn) - { - // destroy all objects owned by this connection, including the player object - conn.DestroyOwnedObjects(); - // remove connection from all of its observing entities observers - // fixes https://github.com/vis2k/Mirror/issues/2737 - // -> cleaning those up in NetworkConnection.Disconnect is NOT enough - // because voluntary disconnects from the other end don't call - // NetworkConnection.Disconnect() - conn.RemoveFromObservingsObservers(); - conn.identity = null; - } - - // message handlers //////////////////////////////////////////////////// - /// Register a handler for message type T. Most should require authentication. - // TODO obsolete this some day to always use the channelId version. - // all handlers in this version are wrapped with 1 extra action. - public static void RegisterHandler(Action handler, bool requireAuthentication = true) - where T : struct, NetworkMessage - { - ushort msgType = NetworkMessageId.Id; - if (handlers.ContainsKey(msgType)) - { - Debug.LogWarning($"NetworkServer.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); - } - handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); - } - - /// Register a handler for message type T. Most should require authentication. - // This version passes channelId to the handler. - public static void RegisterHandler(Action handler, bool requireAuthentication = true) - where T : struct, NetworkMessage - { - ushort msgType = NetworkMessageId.Id; - if (handlers.ContainsKey(msgType)) - { - Debug.LogWarning($"NetworkServer.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); - } - handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); - } - - /// Replace a handler for message type T. Most should require authentication. - public static void ReplaceHandler(Action handler, bool requireAuthentication = true) - where T : struct, NetworkMessage - { - ReplaceHandler((_, value) => { handler(value); }, requireAuthentication); - } - - /// Replace a handler for message type T. Most should require authentication. - public static void ReplaceHandler(Action handler, bool requireAuthentication = true) - where T : struct, NetworkMessage - { - ushort msgType = NetworkMessageId.Id; - handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); - } - - /// Unregister a handler for a message type T. - public static void UnregisterHandler() - where T : struct, NetworkMessage - { - ushort msgType = NetworkMessageId.Id; - handlers.Remove(msgType); - } - - /// Clears all registered message handlers. - public static void ClearHandlers() => handlers.Clear(); - - internal static bool GetNetworkIdentity(GameObject go, out NetworkIdentity identity) - { - if (!go.TryGetComponent(out identity)) - { - Debug.LogError($"GameObject {go.name} doesn't have NetworkIdentity."); - return false; - } - return true; - } - - // disconnect ////////////////////////////////////////////////////////// - /// Disconnect all connections, including the local connection. - // synchronous: handles disconnect events and cleans up fully before returning! - public static void DisconnectAll() - { - // disconnect and remove all connections. - // we can not use foreach here because if - // conn.Disconnect -> Transport.ServerDisconnect calls - // OnDisconnect -> NetworkServer.OnDisconnect(connectionId) - // immediately then OnDisconnect would remove the connection while - // we are iterating here. - // see also: https://github.com/vis2k/Mirror/issues/2357 - // this whole process should be simplified some day. - // until then, let's copy .Values to avoid InvalidOperationException. - // note that this is only called when stopping the server, so the - // copy is no performance problem. - foreach (NetworkConnectionToClient conn in connections.Values.ToList()) - { - // disconnect via connection->transport - conn.Disconnect(); - - // we want this function to be synchronous: handle disconnect - // events and clean up fully before returning. - // -> OnTransportDisconnected can safely be called without - // waiting for the Transport's callback. - // -> it has checks to only run once. - - // call OnDisconnected unless local player in host mod - // TODO unnecessary check? - if (conn.connectionId != NetworkConnection.LocalConnectionId) - OnTransportDisconnected(conn.connectionId); - } - - // cleanup - connections.Clear(); - localConnection = null; - // this used to set active=false. - // however, then Shutdown can't properly destroy objects: - // https://github.com/MirrorNetworking/Mirror/issues/3344 - // "DisconnectAll" should only disconnect all, not set inactive. - // active = false; - } - - // add/remove/replace player /////////////////////////////////////////// - /// Called by server after AddPlayer message to add the player for the connection. - // When a player is added for a connection, the client for that - // connection is made ready automatically. The player object is - // automatically spawned, so you do not need to call NetworkServer.Spawn - // for that object. This function is used for "adding" a player, not for - // "replacing" the player on a connection. If there is already a player - // on this playerControllerId for this connection, this will fail. - public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId) - { - if (GetNetworkIdentity(player, out NetworkIdentity identity)) - { - identity.assetId = assetId; - } - return AddPlayerForConnection(conn, player); - } - - /// Called by server after AddPlayer message to add the player for the connection. - // When a player is added for a connection, the client for that - // connection is made ready automatically. The player object is - // automatically spawned, so you do not need to call NetworkServer.Spawn - // for that object. This function is used for "adding" a player, not for - // "replacing" the player on a connection. If there is already a player - // on this playerControllerId for this connection, this will fail. - public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player) - { - if (!player.TryGetComponent(out NetworkIdentity identity)) - { - Debug.LogWarning($"AddPlayer: playerGameObject has no NetworkIdentity. Please add a NetworkIdentity to {player}"); - return false; - } - - // cannot have a player object in "Add" version - if (conn.identity != null) - { - Debug.Log("AddPlayer: player object already exists"); - return false; - } - - // make sure we have a controller before we call SetClientReady - // because the observers will be rebuilt only if we have a controller - conn.identity = identity; - - // Set the connection on the NetworkIdentity on the server, NetworkIdentity.SetLocalPlayer is not called on the server (it is on clients) - identity.SetClientOwner(conn); - - // special case, we are in host mode, set hasAuthority to true so that all overrides see it - if (conn is LocalConnectionToClient) - { - identity.isOwned = true; - NetworkClient.InternalAddPlayer(identity); - } - - // set ready if not set yet - SetClientReady(conn); - - // Debug.Log($"Adding new playerGameObject object netId: {identity.netId} asset ID: {identity.assetId}"); - - Respawn(identity); - return true; - } - - /// Replaces connection's player object. The old object is not destroyed. - // This does NOT change the ready state of the connection, so it can - // safely be used while changing scenes. - public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, bool keepAuthority = false) - { - if (!player.TryGetComponent(out NetworkIdentity identity)) - { - Debug.LogError($"ReplacePlayer: playerGameObject has no NetworkIdentity. Please add a NetworkIdentity to {player}"); - return false; - } - - if (identity.connectionToClient != null && identity.connectionToClient != conn) - { - Debug.LogError($"Cannot replace player for connection. New player is already owned by a different connection{player}"); - return false; - } - - //NOTE: there can be an existing player - //Debug.Log("NetworkServer ReplacePlayer"); - - NetworkIdentity previousPlayer = conn.identity; - - conn.identity = identity; - - // Set the connection on the NetworkIdentity on the server, NetworkIdentity.SetLocalPlayer is not called on the server (it is on clients) - identity.SetClientOwner(conn); - - // special case, we are in host mode, set hasAuthority to true so that all overrides see it - if (conn is LocalConnectionToClient) - { - identity.isOwned = true; - NetworkClient.InternalAddPlayer(identity); - } - - // add connection to observers AFTER the playerController was set. - // by definition, there is nothing to observe if there is no player - // controller. - // - // IMPORTANT: do this in AddPlayerForConnection & ReplacePlayerForConnection! - SpawnObserversForConnection(conn); - - //Debug.Log($"Replacing playerGameObject object netId:{player.GetComponent().netId} asset ID {player.GetComponent().assetId}"); - - Respawn(identity); - - if (keepAuthority) - { - // This needs to be sent to clear isLocalPlayer on - // client while keeping hasAuthority true - SendChangeOwnerMessage(previousPlayer, conn); - } - else - { - // This clears both isLocalPlayer and hasAuthority on client - previousPlayer.RemoveClientAuthority(); - } - - return true; - } - - /// Replaces connection's player object. The old object is not destroyed. - // This does NOT change the ready state of the connection, so it can - // safely be used while changing scenes. - public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId, bool keepAuthority = false) - { - if (GetNetworkIdentity(player, out NetworkIdentity identity)) - { - identity.assetId = assetId; - } - return ReplacePlayerForConnection(conn, player, keepAuthority); - } - - /// Removes the player object from the connection - // destroyServerObject: Indicates whether the server object should be destroyed - public static void RemovePlayerForConnection(NetworkConnection conn, bool destroyServerObject) - { - if (conn.identity != null) - { - if (destroyServerObject) - Destroy(conn.identity.gameObject); - else - UnSpawn(conn.identity.gameObject); - - conn.identity = null; - } - //else Debug.Log($"Connection {conn} has no identity"); - } - - // ready /////////////////////////////////////////////////////////////// - /// Flags client connection as ready (=joined world). - // When a client has signaled that it is ready, this method tells the - // server that the client is ready to receive spawned objects and state - // synchronization updates. This is usually called in a handler for the - // SYSTEM_READY message. If there is not specific action a game needs to - // take for this message, relying on the default ready handler function - // is probably fine, so this call wont be needed. - public static void SetClientReady(NetworkConnectionToClient conn) - { - // Debug.Log($"SetClientReadyInternal for conn:{conn}"); - - // set ready - conn.isReady = true; - - // client is ready to start spawning objects - if (conn.identity != null) - SpawnObserversForConnection(conn); - } - - static void SpawnObserversForConnection(NetworkConnectionToClient conn) - { - //Debug.Log($"Spawning {spawned.Count} objects for conn {conn}"); - - if (!conn.isReady) - { - // client needs to finish initializing before we can spawn objects - // otherwise it would not find them. - return; - } - - // let connection know that we are about to start spawning... - conn.Send(new ObjectSpawnStartedMessage()); - - // add connection to each nearby NetworkIdentity's observers, which - // internally sends a spawn message for each one to the connection. - foreach (NetworkIdentity identity in spawned.Values) - { - // try with far away ones in ummorpg! - if (identity.gameObject.activeSelf) //TODO this is different - { - //Debug.Log($"Sending spawn message for current server objects name:{identity.name} netId:{identity.netId} sceneId:{identity.sceneId:X}"); - - // we need to support three cases: - // - legacy system (identity has .visibility) - // - new system (networkserver has .aoi) - // - default case: no .visibility and no .aoi means add all - // connections by default) - // - // ForceHidden/ForceShown overwrite all systems so check it - // first! - - // ForceShown: add no matter what - if (identity.visible == Visibility.ForceShown) - { - identity.AddObserver(conn); - } - // ForceHidden: don't show no matter what - else if (identity.visible == Visibility.ForceHidden) - { - // do nothing - } - // default: legacy system / new system / no system support - else if (identity.visible == Visibility.Default) - { - // aoi system - if (aoi != null) - { - // call OnCheckObserver - if (aoi.OnCheckObserver(identity, conn)) - identity.AddObserver(conn); - } - // no system: add all observers by default - else - { - identity.AddObserver(conn); - } - } - } - } - - // let connection know that we finished spawning, so it can call - // OnStartClient on each one (only after all were spawned, which - // is how Unity's Start() function works too) - conn.Send(new ObjectSpawnFinishedMessage()); - } - - /// Marks the client of the connection to be not-ready. - // Clients that are not ready do not receive spawned objects or state - // synchronization updates. They client can be made ready again by - // calling SetClientReady(). - public static void SetClientNotReady(NetworkConnectionToClient conn) - { - conn.isReady = false; - conn.RemoveFromObservingsObservers(); - conn.Send(new NotReadyMessage()); - } - - /// Marks all connected clients as no longer ready. - // All clients will no longer be sent state synchronization updates. The - // player's clients can call ClientManager.Ready() again to re-enter the - // ready state. This is useful when switching scenes. - public static void SetAllClientsNotReady() - { - foreach (NetworkConnectionToClient conn in connections.Values) - { - SetClientNotReady(conn); - } - } - - // show / hide for connection ////////////////////////////////////////// - internal static void ShowForConnection(NetworkIdentity identity, NetworkConnection conn) - { - if (conn.isReady) - SendSpawnMessage(identity, conn); - } - - internal static void HideForConnection(NetworkIdentity identity, NetworkConnection conn) - { - ObjectHideMessage msg = new ObjectHideMessage - { - netId = identity.netId - }; - conn.Send(msg); - } - - // spawning //////////////////////////////////////////////////////////// - internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnection conn) - { - if (identity.serverOnly) return; - - //Debug.Log($"Server SendSpawnMessage: name:{identity.name} sceneId:{identity.sceneId:X} netid:{identity.netId}"); - - // one writer for owner, one for observers - using (NetworkWriterPooled ownerWriter = NetworkWriterPool.Get(), observersWriter = NetworkWriterPool.Get()) - { - bool isOwner = identity.connectionToClient == conn; - ArraySegment payload = CreateSpawnMessagePayload(isOwner, identity, ownerWriter, observersWriter); - SpawnMessage message = new SpawnMessage - { - netId = identity.netId, - isLocalPlayer = conn.identity == identity, - isOwner = isOwner, - sceneId = identity.sceneId, - assetId = identity.assetId, - // use local values for VR support - position = identity.transform.localPosition, - rotation = identity.transform.localRotation, - scale = identity.transform.localScale, - payload = payload - }; - conn.Send(message); - } - } - - static ArraySegment CreateSpawnMessagePayload(bool isOwner, NetworkIdentity identity, NetworkWriterPooled ownerWriter, NetworkWriterPooled observersWriter) - { - // Only call SerializeAll if there are NetworkBehaviours - if (identity.NetworkBehaviours.Length == 0) - { - return default; - } - - // serialize all components with initialState = true - // (can be null if has none) - identity.SerializeServer(true, ownerWriter, observersWriter); - - // convert to ArraySegment to avoid reader allocations - // if nothing was written, .ToArraySegment returns an empty segment. - ArraySegment ownerSegment = ownerWriter.ToArraySegment(); - ArraySegment observersSegment = observersWriter.ToArraySegment(); - - // use owner segment if 'conn' owns this identity, otherwise - // use observers segment - ArraySegment payload = isOwner ? ownerSegment : observersSegment; - - return payload; - } - - internal static void SendChangeOwnerMessage(NetworkIdentity identity, NetworkConnectionToClient conn) - { - // Don't send if identity isn't spawned or only exists on server - if (identity.netId == 0 || identity.serverOnly) return; - - // Don't send if conn doesn't have the identity spawned yet - // May be excluded from the client by interest management - if (!conn.observing.Contains(identity)) return; - - //Debug.Log($"Server SendChangeOwnerMessage: name={identity.name} netid={identity.netId}"); - - conn.Send(new ChangeOwnerMessage - { - netId = identity.netId, - isOwner = identity.connectionToClient == conn, - isLocalPlayer = conn.identity == identity - }); - } - - // check NetworkIdentity parent before spawning it. - // - without parent, they are spawned - // - with parent, only if the parent is active in hierarchy - // - // note that active parents may have inactive parents of their own. - // we need to check .activeInHierarchy. - // - // fixes: https://github.com/MirrorNetworking/Mirror/issues/3330 - // https://github.com/vis2k/Mirror/issues/2778 - static bool ValidParent(NetworkIdentity identity) => - identity.transform.parent == null || - identity.transform.parent.gameObject.activeInHierarchy; - - /// Spawns NetworkIdentities in the scene on the server. - // NetworkIdentity objects in a scene are disabled by default. Calling - // SpawnObjects() causes these scene objects to be enabled and spawned. - // It is like calling NetworkServer.Spawn() for each of them. - public static bool SpawnObjects() - { - // only if server active - if (!active) - return false; - - // find all NetworkIdentities in the scene. - // all of them are disabled because of NetworkScenePostProcess. - NetworkIdentity[] identities = Resources.FindObjectsOfTypeAll(); - - // first pass: activate all scene objects - foreach (NetworkIdentity identity in identities) - { - // only spawn scene objects which haven't been spawned yet. - // SpawnObjects may be called multiple times for additive scenes. - // https://github.com/MirrorNetworking/Mirror/issues/3318 - // - // note that we even activate objects under inactive parents. - // while they are not spawned, they do need to be activated - // in order to be spawned later. so here, we don't check parents. - // https://github.com/MirrorNetworking/Mirror/issues/3330 - if (Utils.IsSceneObject(identity) && identity.netId == 0) - { - // Debug.Log($"SpawnObjects sceneId:{identity.sceneId:X} name:{identity.gameObject.name}"); - identity.gameObject.SetActive(true); - } - } - - // second pass: spawn all scene objects - foreach (NetworkIdentity identity in identities) - { - // scene objects may be children of inactive parents. - // users would put them under disabled parents to 'deactivate' them. - // those should not be used by Mirror at all. - // fixes: https://github.com/MirrorNetworking/Mirror/issues/3330 - // https://github.com/vis2k/Mirror/issues/2778 - if (Utils.IsSceneObject(identity) && identity.netId == 0 && ValidParent(identity)) - { - // pass connection so that authority is not lost when server loads a scene - // https://github.com/vis2k/Mirror/pull/2987 - Spawn(identity.gameObject, identity.connectionToClient); - } - } - - return true; - } - - /// Spawns an object and also assigns Client Authority to the specified client. - // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. - public static void Spawn(GameObject obj, GameObject ownerPlayer) - { - if (!ownerPlayer.TryGetComponent(out NetworkIdentity identity)) - { - Debug.LogError("Player object has no NetworkIdentity"); - return; - } - - if (identity.connectionToClient == null) - { - Debug.LogError("Player object is not a player."); - return; - } - - Spawn(obj, identity.connectionToClient); - } - - static void Respawn(NetworkIdentity identity) - { - if (identity.netId == 0) - { - // If the object has not been spawned, then do a full spawn and update observers - Spawn(identity.gameObject, identity.connectionToClient); - } - else - { - // otherwise just replace his data - SendSpawnMessage(identity, identity.connectionToClient); - } - } - - /// Spawn the given game object on all clients which are ready. - // This will cause a new object to be instantiated from the registered - // prefab, or from a custom spawn function. - public static void Spawn(GameObject obj, NetworkConnection ownerConnection = null) - { - SpawnObject(obj, ownerConnection); - } - - /// Spawns an object and also assigns Client Authority to the specified client. - // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. - public static void Spawn(GameObject obj, uint assetId, NetworkConnection ownerConnection = null) - { - if (GetNetworkIdentity(obj, out NetworkIdentity identity)) - { - identity.assetId = assetId; - } - SpawnObject(obj, ownerConnection); - } - - static void SpawnObject(GameObject obj, NetworkConnection ownerConnection) - { - // verify if we can spawn this - if (Utils.IsPrefab(obj)) - { - Debug.LogError($"GameObject {obj.name} is a prefab, it can't be spawned. Instantiate it first.", obj); - return; - } - - if (!active) - { - Debug.LogError($"SpawnObject for {obj}, NetworkServer is not active. Cannot spawn objects without an active server.", obj); - return; - } - - if (!obj.TryGetComponent(out NetworkIdentity identity)) - { - Debug.LogError($"SpawnObject {obj} has no NetworkIdentity. Please add a NetworkIdentity to {obj}", obj); - return; - } - - if (identity.SpawnedFromInstantiate) - { - // Using Instantiate on SceneObject is not allowed, so stop spawning here - // NetworkIdentity.Awake already logs error, no need to log a second error here - return; - } - - // Spawn should only be called once per netId. - // calling it twice would lead to undefined behaviour. - // https://github.com/MirrorNetworking/Mirror/pull/3205 - if (spawned.ContainsKey(identity.netId)) - { - Debug.LogWarning($"{identity} with netId={identity.netId} was already spawned.", identity.gameObject); - return; - } - - identity.connectionToClient = (NetworkConnectionToClient)ownerConnection; - - // special case to make sure hasAuthority is set - // on start server in host mode - if (ownerConnection is LocalConnectionToClient) - identity.isOwned = true; - - // only call OnStartServer if not spawned yet. - // check used to be in NetworkIdentity. may not be necessary anymore. - if (!identity.isServer && identity.netId == 0) - { - // configure NetworkIdentity - // this may be called in host mode, so we need to initialize - // isLocalPlayer/isClient flags too. - identity.isLocalPlayer = NetworkClient.localPlayer == identity; - identity.isClient = NetworkClient.active; - identity.isServer = true; - identity.netId = NetworkIdentity.GetNextNetworkId(); - - // add to spawned (after assigning netId) - spawned[identity.netId] = identity; - - // callback after all fields were set - identity.OnStartServer(); - } - - // Debug.Log($"SpawnObject instance ID {identity.netId} asset ID {identity.assetId}"); - - if (aoi) - { - // This calls user code which might throw exceptions - // We don't want this to leave us in bad state - try - { - aoi.OnSpawned(identity); - } - catch (Exception e) - { - Debug.LogException(e); - } - } - - RebuildObservers(identity, true); - } - - /// This takes an object that has been spawned and un-spawns it. - // The object will be removed from clients that it was spawned on, or - // the custom spawn handler function on the client will be called for - // the object. - // Unlike when calling NetworkServer.Destroy(), on the server the object - // will NOT be destroyed. This allows the server to re-use the object, - // even spawn it again later. - public static void UnSpawn(GameObject obj) => DestroyObject(obj, DestroyMode.Reset); - - // destroy ///////////////////////////////////////////////////////////// - // sometimes we want to GameObject.Destroy it. - // sometimes we want to just unspawn on clients and .Reset() it on server. - // => 'bool destroy' isn't obvious enough. it's really destroy OR reset! - enum DestroyMode { Destroy, Reset } - - /// Destroys this object and corresponding objects on all clients. - // In some cases it is useful to remove an object but not delete it on - // the server. For that, use NetworkServer.UnSpawn() instead of - // NetworkServer.Destroy(). - public static void Destroy(GameObject obj) => DestroyObject(obj, DestroyMode.Destroy); - - static void DestroyObject(GameObject obj, DestroyMode mode) - { - if (obj == null) - { - Debug.Log("NetworkServer DestroyObject is null"); - return; - } - - if (GetNetworkIdentity(obj, out NetworkIdentity identity)) - { - DestroyObject(identity, mode); - } - } - - static void DestroyObject(NetworkIdentity identity, DestroyMode mode) - { - // Debug.Log($"DestroyObject instance:{identity.netId}"); - - // only call OnRebuildObservers while active, - // not while shutting down - // (https://github.com/vis2k/Mirror/issues/2977) - if (active && aoi) - { - // This calls user code which might throw exceptions - // We don't want this to leave us in bad state - try - { - aoi.OnDestroyed(identity); - } - catch (Exception e) - { - Debug.LogException(e); - } - } - - // remove from NetworkServer (this) dictionary - spawned.Remove(identity.netId); - - identity.connectionToClient?.RemoveOwnedObject(identity); - - // send object destroy message to all observers, clear observers - SendToObservers(identity, new ObjectDestroyMessage - { - netId = identity.netId - }); - identity.ClearObservers(); - - // in host mode, call OnStopClient/OnStopLocalPlayer manually - if (NetworkClient.active && activeHost) - { - if (identity.isLocalPlayer) - identity.OnStopLocalPlayer(); - - identity.OnStopClient(); - // The object may have been spawned with host client ownership, - // e.g. a pet so we need to clear hasAuthority and call - // NotifyAuthority which invokes OnStopAuthority if hasAuthority. - identity.isOwned = false; - identity.NotifyAuthority(); - - // remove from NetworkClient dictionary - NetworkClient.connection.owned.Remove(identity); - NetworkClient.spawned.Remove(identity.netId); - } - - // we are on the server. call OnStopServer. - identity.OnStopServer(); - - // are we supposed to GameObject.Destroy() it completely? - if (mode == DestroyMode.Destroy) - { - identity.destroyCalled = true; - - // Destroy if application is running - if (Application.isPlaying) - { - UnityEngine.Object.Destroy(identity.gameObject); - } - // Destroy can't be used in Editor during tests. use DestroyImmediate. - else - { - GameObject.DestroyImmediate(identity.gameObject); - } - } - // otherwise simply .Reset() and set inactive again - else if (mode == DestroyMode.Reset) - { - identity.Reset(); - } - } - - // interest management ///////////////////////////////////////////////// - - // Helper function to add all server connections as observers. - // This is used if none of the components provides their own - // OnRebuildObservers function. - // rebuild observers default method (no AOI) - adds all connections - static void RebuildObserversDefault(NetworkIdentity identity, bool initialize) - { - // only add all connections when rebuilding the first time. - // second time we just keep them without rebuilding anything. - if (initialize) - { - // not force hidden? - if (identity.visible != Visibility.ForceHidden) - { - AddAllReadyServerConnectionsToObservers(identity); - } - } - } - - internal static void AddAllReadyServerConnectionsToObservers(NetworkIdentity identity) - { - // add all server connections - foreach (NetworkConnectionToClient conn in connections.Values) - { - // only if authenticated (don't send to people during logins) - if (conn.isReady) - identity.AddObserver(conn); - } - - // add local host connection (if any) - if (localConnection != null && localConnection.isReady) - { - identity.AddObserver(localConnection); - } - } - - // RebuildObservers does a local rebuild for the NetworkIdentity. - // This causes the set of players that can see this object to be rebuild. - // - // IMPORTANT: - // => global rebuild would be more simple, BUT - // => local rebuild is way faster for spawn/despawn because we can - // simply rebuild a select NetworkIdentity only - // => having both .observers and .observing is necessary for local - // rebuilds - // - // in other words, this is the perfect solution even though it's not - // completely simple (due to .observers & .observing) - // - // Mirror maintains .observing automatically in the background. best of - // both worlds without any worrying now! - public static void RebuildObservers(NetworkIdentity identity, bool initialize) - { - // if there is no interest management system, - // or if 'force shown' then add all connections - if (aoi == null || identity.visible == Visibility.ForceShown) - { - RebuildObserversDefault(identity, initialize); - } - // otherwise let interest management system rebuild - else - { - aoi.Rebuild(identity, initialize); - } - } - - - // broadcasting //////////////////////////////////////////////////////// - // helper function to get the right serialization for a connection - static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkConnectionToClient connection) - { - // get serialization for this entity (cached) - // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks - NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount); - - // is this entity owned by this connection? - bool owned = identity.connectionToClient == connection; - - // send serialized data - // owner writer if owned - if (owned) - { - // was it dirty / did we actually serialize anything? - if (serialization.ownerWriter.Position > 0) - return serialization.ownerWriter; - } - // observers writer if not owned - else - { - // was it dirty / did we actually serialize anything? - if (serialization.observersWriter.Position > 0) - return serialization.observersWriter; - } - - // nothing was serialized - return null; - } - - // helper function to broadcast the world to a connection - static void BroadcastToConnection(NetworkConnectionToClient connection) - { - // for each entity that this connection is seeing - foreach (NetworkIdentity identity in connection.observing) - { - // make sure it's not null or destroyed. - // (which can happen if someone uses - // GameObject.Destroy instead of - // NetworkServer.Destroy) - if (identity != null) - { - // get serialization for this entity viewed by this connection - // (if anything was serialized this time) - NetworkWriter serialization = SerializeForConnection(identity, connection); - if (serialization != null) - { - EntityStateMessage message = new EntityStateMessage - { - netId = identity.netId, - payload = serialization.ToArraySegment() - }; - connection.Send(message); - } - } - // spawned list should have no null entries because we - // always call Remove in OnObjectDestroy everywhere. - // if it does have null then someone used - // GameObject.Destroy instead of NetworkServer.Destroy. - else Debug.LogWarning($"Found 'null' entry in observing list for connectionId={connection.connectionId}. Please call NetworkServer.Destroy to destroy networked objects. Don't use GameObject.Destroy."); - } - } - - // NetworkLateUpdate called after any Update/FixedUpdate/LateUpdate - // (we add this to the UnityEngine in NetworkLoop) - // internal for tests - internal static readonly List connectionsCopy = - new List(); - - static void Broadcast() - { - // copy all connections into a helper collection so that - // OnTransportDisconnected can be called while iterating. - // -> OnTransportDisconnected removes from the collection - // -> which would throw 'can't modify while iterating' errors - // => see also: https://github.com/vis2k/Mirror/issues/2739 - // (copy nonalloc) - // TODO remove this when we move to 'lite' transports with only - // socket send/recv later. - connectionsCopy.Clear(); - connections.Values.CopyTo(connectionsCopy); - - // go through all connections - foreach (NetworkConnectionToClient connection in connectionsCopy) - { - // has this connection joined the world yet? - // for each READY connection: - // pull in UpdateVarsMessage for each entity it observes - if (connection.isReady) - { - // send time for snapshot interpolation every sendInterval. - // BroadcastToConnection() may not send if nothing is new. - // - // sent over unreliable. - // NetworkTime / Transform both use unreliable. - // - // make sure Broadcast() is only called every sendInterval, - // even if targetFrameRate isn't set in host mode (!) - // (done via AccurateInterval) - connection.Send(new TimeSnapshotMessage(), Channels.Unreliable); - - // broadcast world state to this connection - BroadcastToConnection(connection); - } - - // update connection to flush out batched messages - connection.Update(); - } - - // TODO this is way too slow because we iterate ALL spawned :/ - // TODO this is way too complicated :/ - // to understand what this tries to prevent, consider this example: - // monster has health=100 - // we change health=200, dirty bit is set - // player comes in range, gets full serialization spawn packet. - // next Broadcast(), player gets the health=200 change because dirty bit was set. - // - // this code clears all dirty bits if no players are around to prevent it. - // BUT there are two issues: - // 1. what if a playerB was around the whole time? - // 2. why don't we handle broadcast and spawn packets both HERE? - // handling spawn separately is why we need this complex magic - // - // see test: DirtyBitsAreClearedForSpawnedWithoutObservers() - // see test: SyncObjectChanges_DontGrowWithoutObservers() - // - // PAUL: we also do this to avoid ever growing SyncList .changes - //ClearSpawnedDirtyBits(); - // - // this was moved to NetworkIdentity.AddObserver! - // same result, but no more O(N) loop in here! - // TODO remove this comment after moving spawning into Broadcast()! - } - - // update ////////////////////////////////////////////////////////////// - // NetworkEarlyUpdate called before any Update/FixedUpdate - // (we add this to the UnityEngine in NetworkLoop) - internal static void NetworkEarlyUpdate() - { - // measure update time for profiling. - if (active) - { - earlyUpdateDuration.Begin(); - fullUpdateDuration.Begin(); - } - - // process all incoming messages first before updating the world - if (Transport.active != null) - Transport.active.ServerEarlyUpdate(); - - // step each connection's local time interpolation in early update. - foreach (NetworkConnectionToClient connection in connections.Values) - connection.UpdateTimeInterpolation(); - - if (active) earlyUpdateDuration.End(); - } - - internal static void NetworkLateUpdate() - { - if (active) - { - // measure update time for profiling. - lateUpdateDuration.Begin(); - - // only broadcast world if active - // broadcast every sendInterval. - // AccurateInterval to avoid update frequency inaccuracy issues: - // https://github.com/vis2k/Mirror/pull/3153 - // - // for example, host mode server doesn't set .targetFrameRate. - // Broadcast() would be called every tick. - // snapshots might be sent way too often, etc. - // - // during tests, we always call Broadcast() though. - // - // also important for syncInterval=0 components like - // NetworkTransform, so they can sync on same interval as time - // snapshots _but_ not every single tick. - if (!Application.isPlaying || + } + + // connections ///////////////////////////////////////////////////////// + /// Add a connection and setup callbacks. Returns true if not added yet. + public static bool AddConnection(NetworkConnectionToClient conn) + { + if (!connections.ContainsKey(conn.connectionId)) + { + // connection cannot be null here or conn.connectionId + // would throw NRE + connections[conn.connectionId] = conn; + return true; + } + // already a connection with this id + return false; + } + + /// Removes a connection by connectionId. Returns true if removed. + public static bool RemoveConnection(int connectionId) => + connections.Remove(connectionId); + + // called by LocalClient to add itself. don't call directly. + // TODO consider internal setter instead? + internal static void SetLocalConnection(LocalConnectionToClient conn) + { + if (localConnection != null) + { + Debug.LogError("Local Connection already exists"); + return; + } + + localConnection = conn; + } + + // removes local connection to client + internal static void RemoveLocalConnection() + { + if (localConnection != null) + { + localConnection.Disconnect(); + localConnection = null; + } + RemoveConnection(0); + } + + /// True if we have external connections (that are not host) + public static bool HasExternalConnections() + { + // any connections? + if (connections.Count > 0) + { + // only host connection? + if (connections.Count == 1 && localConnection != null) + return false; + + // otherwise we have real external connections + return true; + } + return false; + } + + // send //////////////////////////////////////////////////////////////// + /// Send a message to all clients, even those that haven't joined the world yet (non ready) + public static void SendToAll(T message, int channelId = Channels.Reliable, bool sendToReadyOnly = false) + where T : struct, NetworkMessage + { + if (!active) + { + Debug.LogWarning("Can not send using NetworkServer.SendToAll(T msg) because NetworkServer is not active"); + return; + } + + // Debug.Log($"Server.SendToAll {typeof(T)}"); + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // pack message only once + NetworkMessages.Pack(message, writer); + ArraySegment segment = writer.ToArraySegment(); + + // filter and then send to all internet connections at once + // -> makes code more complicated, but is HIGHLY worth it to + // avoid allocations, allow for multicast, etc. + int count = 0; + foreach (NetworkConnectionToClient conn in connections.Values) + { + if (sendToReadyOnly && !conn.isReady) + continue; + + count++; + conn.Send(segment, channelId); + } + + NetworkDiagnostics.OnSend(message, channelId, segment.Count, count); + } + } + + /// Send a message to all clients which have joined the world (are ready). + // TODO put rpcs into NetworkServer.Update WorldState packet, then finally remove SendToReady! + public static void SendToReady(T message, int channelId = Channels.Reliable) + where T : struct, NetworkMessage + { + if (!active) + { + Debug.LogWarning("Can not send using NetworkServer.SendToReady(T msg) because NetworkServer is not active"); + return; + } + + SendToAll(message, channelId, true); + } + + // this is like SendToReadyObservers - but it doesn't check the ready flag on the connection. + // this is used for ObjectDestroy messages. + static void SendToObservers(NetworkIdentity identity, T message, int channelId = Channels.Reliable) + where T : struct, NetworkMessage + { + // Debug.Log($"Server.SendToObservers {typeof(T)}"); + if (identity == null || identity.observers.Count == 0) + return; + + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // pack message into byte[] once + NetworkMessages.Pack(message, writer); + ArraySegment segment = writer.ToArraySegment(); + + foreach (NetworkConnectionToClient conn in identity.observers.Values) + { + conn.Send(segment, channelId); + } + + NetworkDiagnostics.OnSend(message, channelId, segment.Count, identity.observers.Count); + } + } + + /// Send a message to only clients which are ready with option to include the owner of the object identity + // TODO obsolete this later. it's not used anymore + public static void SendToReadyObservers(NetworkIdentity identity, T message, bool includeOwner = true, int channelId = Channels.Reliable) + where T : struct, NetworkMessage + { + // Debug.Log($"Server.SendToReady {typeof(T)}"); + if (identity == null || identity.observers.Count == 0) + return; + + using (NetworkWriterPooled writer = NetworkWriterPool.Get()) + { + // pack message only once + NetworkMessages.Pack(message, writer); + ArraySegment segment = writer.ToArraySegment(); + + int count = 0; + foreach (NetworkConnectionToClient conn in identity.observers.Values) + { + bool isOwner = conn == identity.connectionToClient; + if ((!isOwner || includeOwner) && conn.isReady) + { + count++; + conn.Send(segment, channelId); + } + } + + NetworkDiagnostics.OnSend(message, channelId, segment.Count, count); + } + } + + /// Send a message to only clients which are ready including the owner of the NetworkIdentity + // TODO obsolete this later. it's not used anymore + public static void SendToReadyObservers(NetworkIdentity identity, T message, int channelId) + where T : struct, NetworkMessage + { + SendToReadyObservers(identity, message, true, channelId); + } + + // transport events //////////////////////////////////////////////////// + // called by transport + static void OnTransportConnected(int connectionId) + { + // Debug.Log($"Server accepted client:{connectionId}"); + + // connectionId needs to be != 0 because 0 is reserved for local player + // note that some transports like kcp generate connectionId by + // hashing which can be < 0 as well, so we need to allow < 0! + if (connectionId == 0) + { + Debug.LogError($"Server.HandleConnect: invalid connectionId: {connectionId} . Needs to be != 0, because 0 is reserved for local player."); + Transport.active.ServerDisconnect(connectionId); + return; + } + + // connectionId not in use yet? + if (connections.ContainsKey(connectionId)) + { + Transport.active.ServerDisconnect(connectionId); + // Debug.Log($"Server connectionId {connectionId} already in use...kicked client"); + return; + } + + // are more connections allowed? if not, kick + // (it's easier to handle this in Mirror, so Transports can have + // less code and third party transport might not do that anyway) + // (this way we could also send a custom 'tooFull' message later, + // Transport can't do that) + if (connections.Count < maxConnections) + { + // add connection + NetworkConnectionToClient conn = new NetworkConnectionToClient(connectionId); + OnConnected(conn); + } + else + { + // kick + Transport.active.ServerDisconnect(connectionId); + // Debug.Log($"Server full, kicked client {connectionId}"); + } + } + + internal static void OnConnected(NetworkConnectionToClient conn) + { + // Debug.Log($"Server accepted client:{conn}"); + + // add connection and invoke connected event + AddConnection(conn); + OnConnectedEvent?.Invoke(conn); + } + + static bool UnpackAndInvoke(NetworkConnectionToClient connection, NetworkReader reader, int channelId) + { + if (NetworkMessages.UnpackId(reader, out ushort msgType)) + { + // try to invoke the handler for that message + if (handlers.TryGetValue(msgType, out NetworkMessageDelegate handler)) + { + handler.Invoke(connection, reader, channelId); + connection.lastMessageTime = Time.time; + return true; + } + else + { + // message in a batch are NOT length prefixed to save bandwidth. + // every message needs to be handled and read until the end. + // otherwise it would overlap into the next message. + // => need to warn and disconnect to avoid undefined behaviour. + // => WARNING, not error. can happen if attacker sends random data. + Debug.LogWarning($"Unknown message id: {msgType} for connection: {connection}. This can happen if no handler was registered for this message."); + // simply return false. caller is responsible for disconnecting. + //connection.Disconnect(); + return false; + } + } + else + { + // => WARNING, not error. can happen if attacker sends random data. + Debug.LogWarning($"Invalid message header for connection: {connection}."); + // simply return false. caller is responsible for disconnecting. + //connection.Disconnect(); + return false; + } + } + + // called by transport + internal static void OnTransportData(int connectionId, ArraySegment data, int channelId) + { + if (connections.TryGetValue(connectionId, out NetworkConnectionToClient connection)) + { + // client might batch multiple messages into one packet. + // feed it to the Unbatcher. + // NOTE: we don't need to associate a channelId because we + // always process all messages in the batch. + if (!connection.unbatcher.AddBatch(data)) + { + Debug.LogWarning($"NetworkServer: received Message was too short (messages should start with message id)"); + connection.Disconnect(); + return; + } + + // process all messages in the batch. + // only while NOT loading a scene. + // if we get a scene change message, then we need to stop + // processing. otherwise we might apply them to the old scene. + // => fixes https://github.com/vis2k/Mirror/issues/2651 + // + // NOTE: if scene starts loading, then the rest of the batch + // would only be processed when OnTransportData is called + // the next time. + // => consider moving processing to NetworkEarlyUpdate. + while (!isLoadingScene && + connection.unbatcher.GetNextMessage(out NetworkReader reader, out double remoteTimestamp)) + { + // enough to read at least header size? + if (reader.Remaining >= NetworkMessages.IdSize) + { + // make remoteTimeStamp available to the user + connection.remoteTimeStamp = remoteTimestamp; + + // handle message + if (!UnpackAndInvoke(connection, reader, channelId)) + { + // warn, disconnect and return if failed + // -> warning because attackers might send random data + // -> messages in a batch aren't length prefixed. + // failing to read one would cause undefined + // behaviour for every message afterwards. + // so we need to disconnect. + // -> return to avoid the below unbatches.count error. + // we already disconnected and handled it. + Debug.LogWarning($"NetworkServer: failed to unpack and invoke message. Disconnecting {connectionId}."); + connection.Disconnect(); + return; + } + } + // otherwise disconnect + else + { + // WARNING, not error. can happen if attacker sends random data. + Debug.LogWarning($"NetworkServer: received Message was too short (messages should start with message id). Disconnecting {connectionId}"); + connection.Disconnect(); + return; + } + } + + // if we weren't interrupted by a scene change, + // then all batched messages should have been processed now. + // otherwise batches would silently grow. + // we need to log an error to avoid debugging hell. + // + // EXAMPLE: https://github.com/vis2k/Mirror/issues/2882 + // -> UnpackAndInvoke silently returned because no handler for id + // -> Reader would never be read past the end + // -> Batch would never be retired because end is never reached + // + // NOTE: prefixing every message in a batch with a length would + // avoid ever not reading to the end. for extra bandwidth. + // + // IMPORTANT: always keep this check to detect memory leaks. + // this took half a day to debug last time. + if (!isLoadingScene && connection.unbatcher.BatchesCount > 0) + { + Debug.LogError($"Still had {connection.unbatcher.BatchesCount} batches remaining after processing, even though processing was not interrupted by a scene change. This should never happen, as it would cause ever growing batches.\nPossible reasons:\n* A message didn't deserialize as much as it serialized\n*There was no message handler for a message id, so the reader wasn't read until the end."); + } + } + else Debug.LogError($"HandleData Unknown connectionId:{connectionId}"); + } + + // called by transport + // IMPORTANT: often times when disconnecting, we call this from Mirror + // too because we want to remove the connection and handle + // the disconnect immediately. + // => which is fine as long as we guarantee it only runs once + // => which we do by removing the connection! + internal static void OnTransportDisconnected(int connectionId) + { + // Debug.Log($"Server disconnect client:{connectionId}"); + if (connections.TryGetValue(connectionId, out NetworkConnectionToClient conn)) + { + RemoveConnection(connectionId); + // Debug.Log($"Server lost client:{connectionId}"); + + // NetworkManager hooks into OnDisconnectedEvent to make + // DestroyPlayerForConnection(conn) optional, e.g. for PvP MMOs + // where players shouldn't be able to escape combat instantly. + if (OnDisconnectedEvent != null) + { + OnDisconnectedEvent.Invoke(conn); + } + // if nobody hooked into it, then simply call DestroyPlayerForConnection + else + { + DestroyPlayerForConnection(conn); + } + } + } + + // transport errors are forwarded to high level + static void OnTransportError(int connectionId, TransportError error, string reason) + { + // transport errors will happen. logging a warning is enough. + // make sure the user does not panic. + Debug.LogWarning($"Server Transport Error for connId={connectionId}: {error}: {reason}. This is fine."); + // try get connection. passes null otherwise. + connections.TryGetValue(connectionId, out NetworkConnectionToClient conn); + OnErrorEvent?.Invoke(conn, error, reason); + } + + /// Destroys all of the connection's owned objects on the server. + // This is used when a client disconnects, to remove the players for + // that client. This also destroys non-player objects that have client + // authority set for this connection. + public static void DestroyPlayerForConnection(NetworkConnectionToClient conn) + { + // destroy all objects owned by this connection, including the player object + conn.DestroyOwnedObjects(); + // remove connection from all of its observing entities observers + // fixes https://github.com/vis2k/Mirror/issues/2737 + // -> cleaning those up in NetworkConnection.Disconnect is NOT enough + // because voluntary disconnects from the other end don't call + // NetworkConnection.Disconnect() + conn.RemoveFromObservingsObservers(); + conn.identity = null; + } + + // message handlers //////////////////////////////////////////////////// + /// Register a handler for message type T. Most should require authentication. + // TODO obsolete this some day to always use the channelId version. + // all handlers in this version are wrapped with 1 extra action. + public static void RegisterHandler(Action handler, bool requireAuthentication = true) + where T : struct, NetworkMessage + { + ushort msgType = NetworkMessageId.Id; + if (handlers.ContainsKey(msgType)) + { + Debug.LogWarning($"NetworkServer.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); + } + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); + } + + /// Register a handler for message type T. Most should require authentication. + // This version passes channelId to the handler. + public static void RegisterHandler(Action handler, bool requireAuthentication = true) + where T : struct, NetworkMessage + { + ushort msgType = NetworkMessageId.Id; + if (handlers.ContainsKey(msgType)) + { + Debug.LogWarning($"NetworkServer.RegisterHandler replacing handler for {typeof(T).FullName}, id={msgType}. If replacement is intentional, use ReplaceHandler instead to avoid this warning."); + } + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); + } + + /// Replace a handler for message type T. Most should require authentication. + public static void ReplaceHandler(Action handler, bool requireAuthentication = true) + where T : struct, NetworkMessage + { + ReplaceHandler((_, value) => { handler(value); }, requireAuthentication); + } + + /// Replace a handler for message type T. Most should require authentication. + public static void ReplaceHandler(Action handler, bool requireAuthentication = true) + where T : struct, NetworkMessage + { + ushort msgType = NetworkMessageId.Id; + handlers[msgType] = NetworkMessages.WrapHandler(handler, requireAuthentication); + } + + /// Unregister a handler for a message type T. + public static void UnregisterHandler() + where T : struct, NetworkMessage + { + ushort msgType = NetworkMessageId.Id; + handlers.Remove(msgType); + } + + /// Clears all registered message handlers. + public static void ClearHandlers() => handlers.Clear(); + + internal static bool GetNetworkIdentity(GameObject go, out NetworkIdentity identity) + { + if (!go.TryGetComponent(out identity)) + { + Debug.LogError($"GameObject {go.name} doesn't have NetworkIdentity."); + return false; + } + return true; + } + + // disconnect ////////////////////////////////////////////////////////// + /// Disconnect all connections, including the local connection. + // synchronous: handles disconnect events and cleans up fully before returning! + public static void DisconnectAll() + { + // disconnect and remove all connections. + // we can not use foreach here because if + // conn.Disconnect -> Transport.ServerDisconnect calls + // OnDisconnect -> NetworkServer.OnDisconnect(connectionId) + // immediately then OnDisconnect would remove the connection while + // we are iterating here. + // see also: https://github.com/vis2k/Mirror/issues/2357 + // this whole process should be simplified some day. + // until then, let's copy .Values to avoid InvalidOperationException. + // note that this is only called when stopping the server, so the + // copy is no performance problem. + foreach (NetworkConnectionToClient conn in connections.Values.ToList()) + { + // disconnect via connection->transport + conn.Disconnect(); + + // we want this function to be synchronous: handle disconnect + // events and clean up fully before returning. + // -> OnTransportDisconnected can safely be called without + // waiting for the Transport's callback. + // -> it has checks to only run once. + + // call OnDisconnected unless local player in host mod + // TODO unnecessary check? + if (conn.connectionId != NetworkConnection.LocalConnectionId) + OnTransportDisconnected(conn.connectionId); + } + + // cleanup + connections.Clear(); + localConnection = null; + // this used to set active=false. + // however, then Shutdown can't properly destroy objects: + // https://github.com/MirrorNetworking/Mirror/issues/3344 + // "DisconnectAll" should only disconnect all, not set inactive. + // active = false; + } + + // add/remove/replace player /////////////////////////////////////////// + /// Called by server after AddPlayer message to add the player for the connection. + // When a player is added for a connection, the client for that + // connection is made ready automatically. The player object is + // automatically spawned, so you do not need to call NetworkServer.Spawn + // for that object. This function is used for "adding" a player, not for + // "replacing" the player on a connection. If there is already a player + // on this playerControllerId for this connection, this will fail. + public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId) + { + if (GetNetworkIdentity(player, out NetworkIdentity identity)) + { + identity.assetId = assetId; + } + return AddPlayerForConnection(conn, player); + } + + /// Called by server after AddPlayer message to add the player for the connection. + // When a player is added for a connection, the client for that + // connection is made ready automatically. The player object is + // automatically spawned, so you do not need to call NetworkServer.Spawn + // for that object. This function is used for "adding" a player, not for + // "replacing" the player on a connection. If there is already a player + // on this playerControllerId for this connection, this will fail. + public static bool AddPlayerForConnection(NetworkConnectionToClient conn, GameObject player) + { + if (!player.TryGetComponent(out NetworkIdentity identity)) + { + Debug.LogWarning($"AddPlayer: playerGameObject has no NetworkIdentity. Please add a NetworkIdentity to {player}"); + return false; + } + + // cannot have a player object in "Add" version + if (conn.identity != null) + { + Debug.Log("AddPlayer: player object already exists"); + return false; + } + + // make sure we have a controller before we call SetClientReady + // because the observers will be rebuilt only if we have a controller + conn.identity = identity; + + // Set the connection on the NetworkIdentity on the server, NetworkIdentity.SetLocalPlayer is not called on the server (it is on clients) + identity.SetClientOwner(conn); + + // special case, we are in host mode, set hasAuthority to true so that all overrides see it + if (conn is LocalConnectionToClient) + { + identity.isOwned = true; + NetworkClient.InternalAddPlayer(identity); + } + + // set ready if not set yet + SetClientReady(conn); + + // Debug.Log($"Adding new playerGameObject object netId: {identity.netId} asset ID: {identity.assetId}"); + + Respawn(identity); + return true; + } + + /// Replaces connection's player object. The old object is not destroyed. + // This does NOT change the ready state of the connection, so it can + // safely be used while changing scenes. + public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, bool keepAuthority = false) + { + if (!player.TryGetComponent(out NetworkIdentity identity)) + { + Debug.LogError($"ReplacePlayer: playerGameObject has no NetworkIdentity. Please add a NetworkIdentity to {player}"); + return false; + } + + if (identity.connectionToClient != null && identity.connectionToClient != conn) + { + Debug.LogError($"Cannot replace player for connection. New player is already owned by a different connection{player}"); + return false; + } + + //NOTE: there can be an existing player + //Debug.Log("NetworkServer ReplacePlayer"); + + NetworkIdentity previousPlayer = conn.identity; + + conn.identity = identity; + + // Set the connection on the NetworkIdentity on the server, NetworkIdentity.SetLocalPlayer is not called on the server (it is on clients) + identity.SetClientOwner(conn); + + // special case, we are in host mode, set hasAuthority to true so that all overrides see it + if (conn is LocalConnectionToClient) + { + identity.isOwned = true; + NetworkClient.InternalAddPlayer(identity); + } + + // add connection to observers AFTER the playerController was set. + // by definition, there is nothing to observe if there is no player + // controller. + // + // IMPORTANT: do this in AddPlayerForConnection & ReplacePlayerForConnection! + SpawnObserversForConnection(conn); + + //Debug.Log($"Replacing playerGameObject object netId:{player.GetComponent().netId} asset ID {player.GetComponent().assetId}"); + + Respawn(identity); + + if (keepAuthority) + { + // This needs to be sent to clear isLocalPlayer on + // client while keeping hasAuthority true + SendChangeOwnerMessage(previousPlayer, conn); + } + else + { + // This clears both isLocalPlayer and hasAuthority on client + previousPlayer.RemoveClientAuthority(); + } + + return true; + } + + /// Replaces connection's player object. The old object is not destroyed. + // This does NOT change the ready state of the connection, so it can + // safely be used while changing scenes. + public static bool ReplacePlayerForConnection(NetworkConnectionToClient conn, GameObject player, uint assetId, bool keepAuthority = false) + { + if (GetNetworkIdentity(player, out NetworkIdentity identity)) + { + identity.assetId = assetId; + } + return ReplacePlayerForConnection(conn, player, keepAuthority); + } + + /// Removes the player object from the connection + // destroyServerObject: Indicates whether the server object should be destroyed + public static void RemovePlayerForConnection(NetworkConnection conn, bool destroyServerObject) + { + if (conn.identity != null) + { + if (destroyServerObject) + Destroy(conn.identity.gameObject); + else + UnSpawn(conn.identity.gameObject); + + conn.identity = null; + } + //else Debug.Log($"Connection {conn} has no identity"); + } + + // ready /////////////////////////////////////////////////////////////// + /// Flags client connection as ready (=joined world). + // When a client has signaled that it is ready, this method tells the + // server that the client is ready to receive spawned objects and state + // synchronization updates. This is usually called in a handler for the + // SYSTEM_READY message. If there is not specific action a game needs to + // take for this message, relying on the default ready handler function + // is probably fine, so this call wont be needed. + public static void SetClientReady(NetworkConnectionToClient conn) + { + // Debug.Log($"SetClientReadyInternal for conn:{conn}"); + + // set ready + conn.isReady = true; + + // client is ready to start spawning objects + if (conn.identity != null) + SpawnObserversForConnection(conn); + } + + static void SpawnObserversForConnection(NetworkConnectionToClient conn) + { + //Debug.Log($"Spawning {spawned.Count} objects for conn {conn}"); + + if (!conn.isReady) + { + // client needs to finish initializing before we can spawn objects + // otherwise it would not find them. + return; + } + + // let connection know that we are about to start spawning... + conn.Send(new ObjectSpawnStartedMessage()); + + // add connection to each nearby NetworkIdentity's observers, which + // internally sends a spawn message for each one to the connection. + foreach (NetworkIdentity identity in spawned.Values) + { + // try with far away ones in ummorpg! + if (identity.gameObject.activeSelf) //TODO this is different + { + //Debug.Log($"Sending spawn message for current server objects name:{identity.name} netId:{identity.netId} sceneId:{identity.sceneId:X}"); + + // we need to support three cases: + // - legacy system (identity has .visibility) + // - new system (networkserver has .aoi) + // - default case: no .visibility and no .aoi means add all + // connections by default) + // + // ForceHidden/ForceShown overwrite all systems so check it + // first! + + // ForceShown: add no matter what + if (identity.visible == Visibility.ForceShown) + { + identity.AddObserver(conn); + } + // ForceHidden: don't show no matter what + else if (identity.visible == Visibility.ForceHidden) + { + // do nothing + } + // default: legacy system / new system / no system support + else if (identity.visible == Visibility.Default) + { + // aoi system + if (aoi != null) + { + // call OnCheckObserver + if (aoi.OnCheckObserver(identity, conn)) + identity.AddObserver(conn); + } + // no system: add all observers by default + else + { + identity.AddObserver(conn); + } + } + } + } + + // let connection know that we finished spawning, so it can call + // OnStartClient on each one (only after all were spawned, which + // is how Unity's Start() function works too) + conn.Send(new ObjectSpawnFinishedMessage()); + } + + /// Marks the client of the connection to be not-ready. + // Clients that are not ready do not receive spawned objects or state + // synchronization updates. They client can be made ready again by + // calling SetClientReady(). + public static void SetClientNotReady(NetworkConnectionToClient conn) + { + conn.isReady = false; + conn.RemoveFromObservingsObservers(); + conn.Send(new NotReadyMessage()); + } + + /// Marks all connected clients as no longer ready. + // All clients will no longer be sent state synchronization updates. The + // player's clients can call ClientManager.Ready() again to re-enter the + // ready state. This is useful when switching scenes. + public static void SetAllClientsNotReady() + { + foreach (NetworkConnectionToClient conn in connections.Values) + { + SetClientNotReady(conn); + } + } + + // show / hide for connection ////////////////////////////////////////// + internal static void ShowForConnection(NetworkIdentity identity, NetworkConnection conn) + { + if (conn.isReady) + SendSpawnMessage(identity, conn); + } + + internal static void HideForConnection(NetworkIdentity identity, NetworkConnection conn) + { + ObjectHideMessage msg = new ObjectHideMessage + { + netId = identity.netId + }; + conn.Send(msg); + } + + // spawning //////////////////////////////////////////////////////////// + internal static void SendSpawnMessage(NetworkIdentity identity, NetworkConnection conn) + { + if (identity.serverOnly) return; + + //Debug.Log($"Server SendSpawnMessage: name:{identity.name} sceneId:{identity.sceneId:X} netid:{identity.netId}"); + + // one writer for owner, one for observers + using (NetworkWriterPooled ownerWriter = NetworkWriterPool.Get(), observersWriter = NetworkWriterPool.Get()) + { + bool isOwner = identity.connectionToClient == conn; + ArraySegment payload = CreateSpawnMessagePayload(isOwner, identity, ownerWriter, observersWriter); + SpawnMessage message = new SpawnMessage + { + netId = identity.netId, + isLocalPlayer = conn.identity == identity, + isOwner = isOwner, + sceneId = identity.sceneId, + assetId = identity.assetId, + // use local values for VR support + position = identity.transform.localPosition, + rotation = identity.transform.localRotation, + scale = identity.transform.localScale, + payload = payload + }; + conn.Send(message); + } + } + + static ArraySegment CreateSpawnMessagePayload(bool isOwner, NetworkIdentity identity, NetworkWriterPooled ownerWriter, NetworkWriterPooled observersWriter) + { + // Only call SerializeAll if there are NetworkBehaviours + if (identity.NetworkBehaviours.Length == 0) + { + return default; + } + + // serialize all components with initialState = true + // (can be null if has none) + identity.SerializeServer(true, ownerWriter, observersWriter); + + // convert to ArraySegment to avoid reader allocations + // if nothing was written, .ToArraySegment returns an empty segment. + ArraySegment ownerSegment = ownerWriter.ToArraySegment(); + ArraySegment observersSegment = observersWriter.ToArraySegment(); + + // use owner segment if 'conn' owns this identity, otherwise + // use observers segment + ArraySegment payload = isOwner ? ownerSegment : observersSegment; + + return payload; + } + + internal static void SendChangeOwnerMessage(NetworkIdentity identity, NetworkConnectionToClient conn) + { + // Don't send if identity isn't spawned or only exists on server + if (identity.netId == 0 || identity.serverOnly) return; + + // Don't send if conn doesn't have the identity spawned yet + // May be excluded from the client by interest management + if (!conn.observing.Contains(identity)) return; + + //Debug.Log($"Server SendChangeOwnerMessage: name={identity.name} netid={identity.netId}"); + + conn.Send(new ChangeOwnerMessage + { + netId = identity.netId, + isOwner = identity.connectionToClient == conn, + isLocalPlayer = conn.identity == identity + }); + } + + // check NetworkIdentity parent before spawning it. + // - without parent, they are spawned + // - with parent, only if the parent is active in hierarchy + // + // note that active parents may have inactive parents of their own. + // we need to check .activeInHierarchy. + // + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3330 + // https://github.com/vis2k/Mirror/issues/2778 + static bool ValidParent(NetworkIdentity identity) => + identity.transform.parent == null || + identity.transform.parent.gameObject.activeInHierarchy; + + /// Spawns NetworkIdentities in the scene on the server. + // NetworkIdentity objects in a scene are disabled by default. Calling + // SpawnObjects() causes these scene objects to be enabled and spawned. + // It is like calling NetworkServer.Spawn() for each of them. + public static bool SpawnObjects() + { + // only if server active + if (!active) + return false; + + // find all NetworkIdentities in the scene. + // all of them are disabled because of NetworkScenePostProcess. + NetworkIdentity[] identities = Resources.FindObjectsOfTypeAll(); + + // first pass: activate all scene objects + foreach (NetworkIdentity identity in identities) + { + // only spawn scene objects which haven't been spawned yet. + // SpawnObjects may be called multiple times for additive scenes. + // https://github.com/MirrorNetworking/Mirror/issues/3318 + // + // note that we even activate objects under inactive parents. + // while they are not spawned, they do need to be activated + // in order to be spawned later. so here, we don't check parents. + // https://github.com/MirrorNetworking/Mirror/issues/3330 + if (Utils.IsSceneObject(identity) && identity.netId == 0) + { + // Debug.Log($"SpawnObjects sceneId:{identity.sceneId:X} name:{identity.gameObject.name}"); + identity.gameObject.SetActive(true); + } + } + + // second pass: spawn all scene objects + foreach (NetworkIdentity identity in identities) + { + // scene objects may be children of inactive parents. + // users would put them under disabled parents to 'deactivate' them. + // those should not be used by Mirror at all. + // fixes: https://github.com/MirrorNetworking/Mirror/issues/3330 + // https://github.com/vis2k/Mirror/issues/2778 + if (Utils.IsSceneObject(identity) && identity.netId == 0 && ValidParent(identity)) + { + // pass connection so that authority is not lost when server loads a scene + // https://github.com/vis2k/Mirror/pull/2987 + Spawn(identity.gameObject, identity.connectionToClient); + } + } + + return true; + } + + /// Spawns an object and also assigns Client Authority to the specified client. + // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + public static void Spawn(GameObject obj, GameObject ownerPlayer) + { + if (!ownerPlayer.TryGetComponent(out NetworkIdentity identity)) + { + Debug.LogError("Player object has no NetworkIdentity"); + return; + } + + if (identity.connectionToClient == null) + { + Debug.LogError("Player object is not a player."); + return; + } + + Spawn(obj, identity.connectionToClient); + } + + static void Respawn(NetworkIdentity identity) + { + if (identity.netId == 0) + { + // If the object has not been spawned, then do a full spawn and update observers + Spawn(identity.gameObject, identity.connectionToClient); + } + else + { + // otherwise just replace his data + SendSpawnMessage(identity, identity.connectionToClient); + } + } + + /// Spawn the given game object on all clients which are ready. + // This will cause a new object to be instantiated from the registered + // prefab, or from a custom spawn function. + public static void Spawn(GameObject obj, NetworkConnection ownerConnection = null) + { + SpawnObject(obj, ownerConnection); + } + + /// Spawns an object and also assigns Client Authority to the specified client. + // This is the same as calling NetworkIdentity.AssignClientAuthority on the spawned object. + public static void Spawn(GameObject obj, uint assetId, NetworkConnection ownerConnection = null) + { + if (GetNetworkIdentity(obj, out NetworkIdentity identity)) + { + identity.assetId = assetId; + } + SpawnObject(obj, ownerConnection); + } + + static void SpawnObject(GameObject obj, NetworkConnection ownerConnection) + { + // verify if we can spawn this + if (Utils.IsPrefab(obj)) + { + Debug.LogError($"GameObject {obj.name} is a prefab, it can't be spawned. Instantiate it first.", obj); + return; + } + + if (!active) + { + Debug.LogError($"SpawnObject for {obj}, NetworkServer is not active. Cannot spawn objects without an active server.", obj); + return; + } + + if (!obj.TryGetComponent(out NetworkIdentity identity)) + { + Debug.LogError($"SpawnObject {obj} has no NetworkIdentity. Please add a NetworkIdentity to {obj}", obj); + return; + } + + if (identity.SpawnedFromInstantiate) + { + // Using Instantiate on SceneObject is not allowed, so stop spawning here + // NetworkIdentity.Awake already logs error, no need to log a second error here + return; + } + + // Spawn should only be called once per netId. + // calling it twice would lead to undefined behaviour. + // https://github.com/MirrorNetworking/Mirror/pull/3205 + if (spawned.ContainsKey(identity.netId)) + { + Debug.LogWarning($"{identity} with netId={identity.netId} was already spawned.", identity.gameObject); + return; + } + + identity.connectionToClient = (NetworkConnectionToClient)ownerConnection; + + // special case to make sure hasAuthority is set + // on start server in host mode + if (ownerConnection is LocalConnectionToClient) + identity.isOwned = true; + + // only call OnStartServer if not spawned yet. + // check used to be in NetworkIdentity. may not be necessary anymore. + if (!identity.isServer && identity.netId == 0) + { + // configure NetworkIdentity + // this may be called in host mode, so we need to initialize + // isLocalPlayer/isClient flags too. + identity.isLocalPlayer = NetworkClient.localPlayer == identity; + identity.isClient = NetworkClient.active; + identity.isServer = true; + identity.netId = NetworkIdentity.GetNextNetworkId(); + + // add to spawned (after assigning netId) + spawned[identity.netId] = identity; + + // callback after all fields were set + identity.OnStartServer(); + } + + // Debug.Log($"SpawnObject instance ID {identity.netId} asset ID {identity.assetId}"); + + if (aoi) + { + // This calls user code which might throw exceptions + // We don't want this to leave us in bad state + try + { + aoi.OnSpawned(identity); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + RebuildObservers(identity, true); + } + + /// This takes an object that has been spawned and un-spawns it. + // The object will be removed from clients that it was spawned on, or + // the custom spawn handler function on the client will be called for + // the object. + // Unlike when calling NetworkServer.Destroy(), on the server the object + // will NOT be destroyed. This allows the server to re-use the object, + // even spawn it again later. + public static void UnSpawn(GameObject obj) => DestroyObject(obj, DestroyMode.Reset); + + // destroy ///////////////////////////////////////////////////////////// + // sometimes we want to GameObject.Destroy it. + // sometimes we want to just unspawn on clients and .Reset() it on server. + // => 'bool destroy' isn't obvious enough. it's really destroy OR reset! + enum DestroyMode { Destroy, Reset } + + /// Destroys this object and corresponding objects on all clients. + // In some cases it is useful to remove an object but not delete it on + // the server. For that, use NetworkServer.UnSpawn() instead of + // NetworkServer.Destroy(). + public static void Destroy(GameObject obj) => DestroyObject(obj, DestroyMode.Destroy); + + static void DestroyObject(GameObject obj, DestroyMode mode) + { + if (obj == null) + { + Debug.Log("NetworkServer DestroyObject is null"); + return; + } + + if (GetNetworkIdentity(obj, out NetworkIdentity identity)) + { + DestroyObject(identity, mode); + } + } + + static void DestroyObject(NetworkIdentity identity, DestroyMode mode) + { + // Debug.Log($"DestroyObject instance:{identity.netId}"); + + // only call OnRebuildObservers while active, + // not while shutting down + // (https://github.com/vis2k/Mirror/issues/2977) + if (active && aoi) + { + // This calls user code which might throw exceptions + // We don't want this to leave us in bad state + try + { + aoi.OnDestroyed(identity); + } + catch (Exception e) + { + Debug.LogException(e); + } + } + + // remove from NetworkServer (this) dictionary + spawned.Remove(identity.netId); + + identity.connectionToClient?.RemoveOwnedObject(identity); + + // send object destroy message to all observers, clear observers + SendToObservers(identity, new ObjectDestroyMessage + { + netId = identity.netId + }); + identity.ClearObservers(); + + // in host mode, call OnStopClient/OnStopLocalPlayer manually + if (NetworkClient.active && activeHost) + { + if (identity.isLocalPlayer) + identity.OnStopLocalPlayer(); + + identity.OnStopClient(); + // The object may have been spawned with host client ownership, + // e.g. a pet so we need to clear hasAuthority and call + // NotifyAuthority which invokes OnStopAuthority if hasAuthority. + identity.isOwned = false; + identity.NotifyAuthority(); + + // remove from NetworkClient dictionary + NetworkClient.connection.owned.Remove(identity); + NetworkClient.spawned.Remove(identity.netId); + } + + // we are on the server. call OnStopServer. + identity.OnStopServer(); + + // are we supposed to GameObject.Destroy() it completely? + if (mode == DestroyMode.Destroy) + { + identity.destroyCalled = true; + + // Destroy if application is running + if (Application.isPlaying) + { + UnityEngine.Object.Destroy(identity.gameObject); + } + // Destroy can't be used in Editor during tests. use DestroyImmediate. + else + { + GameObject.DestroyImmediate(identity.gameObject); + } + } + // otherwise simply .Reset() and set inactive again + else if (mode == DestroyMode.Reset) + { + identity.Reset(); + } + } + + // interest management ///////////////////////////////////////////////// + + // Helper function to add all server connections as observers. + // This is used if none of the components provides their own + // OnRebuildObservers function. + // rebuild observers default method (no AOI) - adds all connections + static void RebuildObserversDefault(NetworkIdentity identity, bool initialize) + { + // only add all connections when rebuilding the first time. + // second time we just keep them without rebuilding anything. + if (initialize) + { + // not force hidden? + if (identity.visible != Visibility.ForceHidden) + { + AddAllReadyServerConnectionsToObservers(identity); + } + } + } + + internal static void AddAllReadyServerConnectionsToObservers(NetworkIdentity identity) + { + // add all server connections + foreach (NetworkConnectionToClient conn in connections.Values) + { + // only if authenticated (don't send to people during logins) + if (conn.isReady) + identity.AddObserver(conn); + } + + // add local host connection (if any) + if (localConnection != null && localConnection.isReady) + { + identity.AddObserver(localConnection); + } + } + + // RebuildObservers does a local rebuild for the NetworkIdentity. + // This causes the set of players that can see this object to be rebuild. + // + // IMPORTANT: + // => global rebuild would be more simple, BUT + // => local rebuild is way faster for spawn/despawn because we can + // simply rebuild a select NetworkIdentity only + // => having both .observers and .observing is necessary for local + // rebuilds + // + // in other words, this is the perfect solution even though it's not + // completely simple (due to .observers & .observing) + // + // Mirror maintains .observing automatically in the background. best of + // both worlds without any worrying now! + public static void RebuildObservers(NetworkIdentity identity, bool initialize) + { + // if there is no interest management system, + // or if 'force shown' then add all connections + if (aoi == null || identity.visible == Visibility.ForceShown) + { + RebuildObserversDefault(identity, initialize); + } + // otherwise let interest management system rebuild + else + { + aoi.Rebuild(identity, initialize); + } + } + + + // broadcasting //////////////////////////////////////////////////////// + // helper function to get the right serialization for a connection + static NetworkWriter SerializeForConnection(NetworkIdentity identity, NetworkConnectionToClient connection) + { + // get serialization for this entity (cached) + // IMPORTANT: int tick avoids floating point inaccuracy over days/weeks + NetworkIdentitySerialization serialization = identity.GetServerSerializationAtTick(Time.frameCount); + + // is this entity owned by this connection? + bool owned = identity.connectionToClient == connection; + + // send serialized data + // owner writer if owned + if (owned) + { + // was it dirty / did we actually serialize anything? + if (serialization.ownerWriter.Position > 0) + return serialization.ownerWriter; + } + // observers writer if not owned + else + { + // was it dirty / did we actually serialize anything? + if (serialization.observersWriter.Position > 0) + return serialization.observersWriter; + } + + // nothing was serialized + return null; + } + + // helper function to broadcast the world to a connection + static void BroadcastToConnection(NetworkConnectionToClient connection) + { + // for each entity that this connection is seeing + foreach (NetworkIdentity identity in connection.observing) + { + // make sure it's not null or destroyed. + // (which can happen if someone uses + // GameObject.Destroy instead of + // NetworkServer.Destroy) + if (identity != null) + { + // get serialization for this entity viewed by this connection + // (if anything was serialized this time) + NetworkWriter serialization = SerializeForConnection(identity, connection); + if (serialization != null) + { + EntityStateMessage message = new EntityStateMessage + { + netId = identity.netId, + payload = serialization.ToArraySegment() + }; + connection.Send(message); + } + } + // spawned list should have no null entries because we + // always call Remove in OnObjectDestroy everywhere. + // if it does have null then someone used + // GameObject.Destroy instead of NetworkServer.Destroy. + else Debug.LogWarning($"Found 'null' entry in observing list for connectionId={connection.connectionId}. Please call NetworkServer.Destroy to destroy networked objects. Don't use GameObject.Destroy."); + } + } + + // NetworkLateUpdate called after any Update/FixedUpdate/LateUpdate + // (we add this to the UnityEngine in NetworkLoop) + // internal for tests + internal static readonly List connectionsCopy = + new List(); + + static void Broadcast() + { + // copy all connections into a helper collection so that + // OnTransportDisconnected can be called while iterating. + // -> OnTransportDisconnected removes from the collection + // -> which would throw 'can't modify while iterating' errors + // => see also: https://github.com/vis2k/Mirror/issues/2739 + // (copy nonalloc) + // TODO remove this when we move to 'lite' transports with only + // socket send/recv later. + connectionsCopy.Clear(); + connections.Values.CopyTo(connectionsCopy); + + // go through all connections + foreach (NetworkConnectionToClient connection in connectionsCopy) + { + // has this connection joined the world yet? + // for each READY connection: + // pull in UpdateVarsMessage for each entity it observes + if (connection.isReady) + { + // send time for snapshot interpolation every sendInterval. + // BroadcastToConnection() may not send if nothing is new. + // + // sent over unreliable. + // NetworkTime / Transform both use unreliable. + // + // make sure Broadcast() is only called every sendInterval, + // even if targetFrameRate isn't set in host mode (!) + // (done via AccurateInterval) + connection.Send(new TimeSnapshotMessage(), Channels.Unreliable); + + // broadcast world state to this connection + BroadcastToConnection(connection); + } + + // update connection to flush out batched messages + connection.Update(); + } + + // TODO this is way too slow because we iterate ALL spawned :/ + // TODO this is way too complicated :/ + // to understand what this tries to prevent, consider this example: + // monster has health=100 + // we change health=200, dirty bit is set + // player comes in range, gets full serialization spawn packet. + // next Broadcast(), player gets the health=200 change because dirty bit was set. + // + // this code clears all dirty bits if no players are around to prevent it. + // BUT there are two issues: + // 1. what if a playerB was around the whole time? + // 2. why don't we handle broadcast and spawn packets both HERE? + // handling spawn separately is why we need this complex magic + // + // see test: DirtyBitsAreClearedForSpawnedWithoutObservers() + // see test: SyncObjectChanges_DontGrowWithoutObservers() + // + // PAUL: we also do this to avoid ever growing SyncList .changes + //ClearSpawnedDirtyBits(); + // + // this was moved to NetworkIdentity.AddObserver! + // same result, but no more O(N) loop in here! + // TODO remove this comment after moving spawning into Broadcast()! + } + + // update ////////////////////////////////////////////////////////////// + // NetworkEarlyUpdate called before any Update/FixedUpdate + // (we add this to the UnityEngine in NetworkLoop) + internal static void NetworkEarlyUpdate() + { + // measure update time for profiling. + if (active) + { + earlyUpdateDuration.Begin(); + fullUpdateDuration.Begin(); + } + + // process all incoming messages first before updating the world + if (Transport.active != null) + Transport.active.ServerEarlyUpdate(); + + // step each connection's local time interpolation in early update. + foreach (NetworkConnectionToClient connection in connections.Values) + connection.UpdateTimeInterpolation(); + + if (active) earlyUpdateDuration.End(); + } + + internal static void NetworkLateUpdate() + { + if (active) + { + // measure update time for profiling. + lateUpdateDuration.Begin(); + + // only broadcast world if active + // broadcast every sendInterval. + // AccurateInterval to avoid update frequency inaccuracy issues: + // https://github.com/vis2k/Mirror/pull/3153 + // + // for example, host mode server doesn't set .targetFrameRate. + // Broadcast() would be called every tick. + // snapshots might be sent way too often, etc. + // + // during tests, we always call Broadcast() though. + // + // also important for syncInterval=0 components like + // NetworkTransform, so they can sync on same interval as time + // snapshots _but_ not every single tick. + if (!Application.isPlaying || #if !UNITY_2020_3_OR_NEWER // Unity 2019 doesn't have Time.timeAsDouble yet AccurateInterval.Elapsed(NetworkTime.localTime, sendInterval, ref lastSendTime)) #else - AccurateInterval.Elapsed(Time.timeAsDouble, sendInterval, ref lastSendTime)) + AccurateInterval.Elapsed(Time.timeAsDouble, sendInterval, ref lastSendTime)) #endif - { - Broadcast(); - } - } + { + Broadcast(); + } + } - // process all outgoing messages after updating the world - // (even if not active. still want to process disconnects etc.) - if (Transport.active != null) - Transport.active.ServerLateUpdate(); + // process all outgoing messages after updating the world + // (even if not active. still want to process disconnects etc.) + if (Transport.active != null) + Transport.active.ServerLateUpdate(); - // measure actual tick rate every second. - if (active) - { - ++actualTickRateCounter; + // measure actual tick rate every second. + if (active) + { + ++actualTickRateCounter; - // NetworkTime.localTime has defines for 2019 / 2020 compatibility - if (NetworkTime.localTime >= actualTickRateStart + 1) - { - // calculate avg by exact elapsed time. - // assuming 1s wouldn't be accurate, usually a few more ms passed. - float elapsed = (float)(NetworkTime.localTime - actualTickRateStart); - actualTickRate = Mathf.RoundToInt(actualTickRateCounter / elapsed); - actualTickRateStart = NetworkTime.localTime; - actualTickRateCounter = 0; - } + // NetworkTime.localTime has defines for 2019 / 2020 compatibility + if (NetworkTime.localTime >= actualTickRateStart + 1) + { + // calculate avg by exact elapsed time. + // assuming 1s wouldn't be accurate, usually a few more ms passed. + float elapsed = (float)(NetworkTime.localTime - actualTickRateStart); + actualTickRate = Mathf.RoundToInt(actualTickRateCounter / elapsed); + actualTickRateStart = NetworkTime.localTime; + actualTickRateCounter = 0; + } - // measure total update time. including transport. - // because in early update, transport update calls handlers. - lateUpdateDuration.End(); - fullUpdateDuration.End(); - } - } + // measure total update time. including transport. + // because in early update, transport update calls handlers. + lateUpdateDuration.End(); + fullUpdateDuration.End(); + } + } - // calls OnStartClient for all SERVER objects in host mode once. - // client doesn't get spawn messages for those, so need to call manually. - // Deprecated 2022-12-12 - [Obsolete("NetworkServer.ActivateHostScene was moved to HostMode.ActivateHostScene")] - public static void ActivateHostScene() => HostMode.ActivateHostScene(); - } + // calls OnStartClient for all SERVER objects in host mode once. + // client doesn't get spawn messages for those, so need to call manually. + // Deprecated 2022-12-12 + [Obsolete("NetworkServer.ActivateHostScene was moved to HostMode.ActivateHostScene")] + public static void ActivateHostScene() => HostMode.ActivateHostScene(); + } } diff --git a/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs index 558737588..e3eb74d62 100644 --- a/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs +++ b/Assets/Mirror/Core/SnapshotInterpolation/SnapshotInterpolation.cs @@ -14,351 +14,351 @@ namespace Mirror { - public static class SortedListExtensions - { - // removes the first 'amount' elements from the sorted list - public static void RemoveRange(this SortedList 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 SortedListExtensions + { + // removes the first 'amount' elements from the sorted list + public static void RemoveRange(this SortedList 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 catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots) - double catchupPositiveThreshold) // in % of sendInterval) - { - // 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 > catchupPositiveThreshold) - { - // localTimeline += 0.001; // too simple, this would ping pong - return 1 + catchupSpeed; // n% faster - } + 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 catchupNegativeThreshold, // in % of sendInteral (careful, we may run out of snapshots) + double catchupPositiveThreshold) // in % of sendInterval) + { + // 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 > catchupPositiveThreshold) + { + // 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 < catchupNegativeThreshold) - { - // localTimeline -= 0.001; // too simple, this would ping pong - return 1 - slowdownSpeed; // n% slower - } + // 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 < catchupNegativeThreshold) + { + // 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; - } + // 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; + // 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; + // 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; - // Console.WriteLine($"sendInterval={sendInterval:F3} jitter std={jitterStandardDeviation:F3} => that is ~{multiples:F1} x sendInterval + {dynamicAdjustmentTolerance} => dynamic bufferTimeMultiplier={safezone}"); - return safezone; - } + // add the tolerance + double safezone = multiples + dynamicAdjustmentTolerance; + // Console.WriteLine($"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. - // NetworkBehaviour local insertion without any time adjustments. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool InsertIfNotExists( - SortedList buffer, // snapshot buffer - T snapshot) // the newly received snapshot - where T : Snapshot - { - // 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. + // 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. + // NetworkBehaviour local insertion without any time adjustments. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool InsertIfNotExists( + SortedList buffer, // snapshot buffer + T snapshot) // the newly received snapshot + where T : Snapshot + { + // 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 + //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; - } + int before = buffer.Count; + buffer[snapshot.remoteTime] = snapshot; // overwrites if key exists + return buffer.Count > before; + } - // call this for every received snapshot. - // adds / inserts it to the list & initializes local time if needed. - public static void InsertAndAdjust( - SortedList buffer, // snapshot buffer - 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 - float clampMultiplier, // multiplier to check if time needs to be clamped - 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; + // call this for every received snapshot. + // adds / inserts it to the list & initializes local time if needed. + public static void InsertAndAdjust( + SortedList buffer, // snapshot buffer + 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 + float clampMultiplier, // multiplier to check if time needs to be clamped + 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, 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; + // 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, 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; + // 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); - } + // 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. + // 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; - - TimeLineOverride(latestRemoteTime, bufferTime, clampMultiplier, ref localTimeline); - - double timeDiff = latestRemoteTime - localTimeline; - if (buffer.Count > 1) - // 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); + // 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; - // next up, calculate how far we are currently away from bufferTime - double drift = driftEma.Value - bufferTime; + TimeLineOverride(latestRemoteTime, bufferTime, clampMultiplier, ref localTimeline); - // convert relative thresholds to absolute values based on sendInterval - double absoluteNegativeThreshold = sendInterval * catchupNegativeThreshold; - double absolutePositiveThreshold = sendInterval * catchupPositiveThreshold; + double timeDiff = latestRemoteTime - localTimeline; + if (buffer.Count > 1) + // 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); - // 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); + // next up, calculate how far we are currently away from bufferTime + double drift = driftEma.Value - bufferTime; - // debug logging - // Console.WriteLine($"sendInterval={sendInterval:F3} bufferTime={bufferTime:F3} drift={drift:F3} driftEma={driftEma.Value:F3} timescale={localTimescale:F3} deliveryIntervalEma={deliveryTimeEma.Value:F3}"); - } - } - - // If the time difference is more than X times of buffer, we will override time to be - // targetTime +- x times of buffer. - private static void TimeLineOverride(double latestRemoteTime, double bufferTime, float clampMultiplier, ref double localTimeline) - { - // If we want local timeline to be around bufferTime slower, - // Then over her we want to clamp localTimeline to be: - // target +- multiplierCheck * bufferTime. - double targetTime = latestRemoteTime - bufferTime; - - localTimeline = Math.Clamp(localTimeline, targetTime - clampMultiplier * bufferTime, targetTime + clampMultiplier * bufferTime); - } + // convert relative thresholds to absolute values based on sendInterval + double absoluteNegativeThreshold = sendInterval * catchupNegativeThreshold; + double absolutePositiveThreshold = sendInterval * catchupPositiveThreshold; - // 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( - SortedList 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; + // 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); - // 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; - } - } + // debug logging + // Console.WriteLine($"sendInterval={sendInterval:F3} bufferTime={bufferTime:F3} drift={drift:F3} driftEma={driftEma.Value:F3} timescale={localTimescale:F3} deliveryIntervalEma={deliveryTimeEma.Value:F3}"); + } + } - // didn't find two snapshots around local time. - // so pick either the first or last, depending on which is closer. + // If the time difference is more than X times of buffer, we will override time to be + // targetTime +- x times of buffer. + private static void TimeLineOverride(double latestRemoteTime, double bufferTime, float clampMultiplier, ref double localTimeline) + { + // If we want local timeline to be around bufferTime slower, + // Then over her we want to clamp localTimeline to be: + // target +- multiplierCheck * bufferTime. + double targetTime = latestRemoteTime - bufferTime; - // 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; - } - } + localTimeline = Math.Clamp(localTimeline, targetTime - clampMultiplier * bufferTime, targetTime + clampMultiplier * bufferTime); + } - // progress local timeline every update. - // - // ONLY CALL IF SNAPSHOTS.COUNT > 0! - // - // decoupled from Step 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 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( + SortedList 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, 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( - SortedList 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 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; + } + } - // sample snapshot buffer at local interpolation time - Sample(buffer, localTimeline, out int from, out int to, out t); + // didn't find two snapshots around local time. + // so pick either the first or last, depending on which is closer. - // save from/to - fromSnapshot = buffer.Values[from]; - toSnapshot = buffer.Values[to]; + // 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; + } + } - // 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); - } + // progress local timeline every update. + // + // ONLY CALL IF SNAPSHOTS.COUNT > 0! + // + // decoupled from Step 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; + } - // 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( - SortedList 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); - } - } + // 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( + SortedList 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( + SortedList 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); + } + } } diff --git a/Assets/Mirror/Examples/Snapshot Interpolation/ClientCube.cs b/Assets/Mirror/Examples/Snapshot Interpolation/ClientCube.cs index e54eb61f3..314b30a79 100644 --- a/Assets/Mirror/Examples/Snapshot Interpolation/ClientCube.cs +++ b/Assets/Mirror/Examples/Snapshot Interpolation/ClientCube.cs @@ -4,206 +4,206 @@ namespace Mirror.Examples.SnapshotInterpolationDemo { - public class ClientCube : MonoBehaviour - { - [Header("Components")] - public ServerCube server; - public Renderer render; + public class ClientCube : MonoBehaviour + { + [Header("Components")] + public ServerCube server; + public Renderer render; - [Header("Toggle")] - public bool interpolate = true; + [Header("Toggle")] + public bool interpolate = true; - // 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; - public double bufferTime => server.sendInterval * bufferTimeMultiplier; + // 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; + public double bufferTime => server.sendInterval * bufferTimeMultiplier; - // - public SortedList snapshots = new SortedList(); + // + public SortedList snapshots = new SortedList(); - // for smooth interpolation, we need to interpolate along server time. - // any other time (arrival on client, client local time, etc.) is not - // going to give smooth results. - double localTimeline; + // for smooth interpolation, we need to interpolate along server time. + // any other time (arrival on client, client local time, etc.) is not + // going to give smooth results. + double localTimeline; - // catchup / slowdown adjustments are applied to timescale, - // to be adjusted in every update instead of when receiving messages. - double localTimescale = 1; + // catchup / slowdown adjustments are applied to timescale, + // to be adjusted in every update instead of when receiving messages. + double localTimescale = 1; - [Tooltip("Mirror tries to maintain 2x send interval (= 1 / Send Rate) time behind server/client. If we are way out of sync by a multiple of this buffer, we simply clamp time to within this buffer.")] - public float bufferTimeMultiplierForClamping = 2; - - // 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("Mirror tries to maintain 2x send interval (= 1 / Send Rate) time behind server/client. If we are way out of sync by a multiple of this buffer, we simply clamp time to within this buffer.")] + public float bufferTimeMultiplierForClamping = 2; - [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; + // 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("Local timeline acceleration in % while catching up.")] - [Range(0, 1)] - public double catchupSpeed = 0.01f; // 1% + [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 slowdown in % while slowing down.")] - [Range(0, 1)] - public double slowdownSpeed = 0.01f; // 1% + [Tooltip("Local timeline acceleration in % while catching up.")] + [Range(0, 1)] + public double catchupSpeed = 0.01f; // 1% - [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 + [Tooltip("Local timeline slowdown in % while slowing down.")] + [Range(0, 1)] + public double slowdownSpeed = 0.01f; // 1% - // we use EMA to average the last second worth of snapshot time diffs. - // manually averaging the last second worth of values with a for loop - // would be the same, but a moving average is faster because we only - // ever add one value. - ExponentialMovingAverage driftEma; + [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; + // we use EMA to average the last second worth of snapshot time diffs. + // manually averaging the last second worth of values with a for loop + // would be the same, but a moving average is faster because we only + // ever add one value. + ExponentialMovingAverage driftEma; - [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) + // 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("Dynamic adjustment is computed over n-second exponential moving average standard deviation.")] - public int deliveryTimeEmaDuration = 2; // 1-2s recommended to capture average delivery time - ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter) + [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) - // debugging /////////////////////////////////////////////////////////// - [Header("Debug")] - public Color catchupColor = Color.green; // green traffic light = go fast - public Color slowdownColor = Color.red; // red traffic light = go slow - Color defaultColor; + [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 + ExponentialMovingAverage deliveryTimeEma; // average delivery time (standard deviation gives average jitter) - void Awake() - { - defaultColor = render.sharedMaterial.color; + // debugging /////////////////////////////////////////////////////////// + [Header("Debug")] + public Color catchupColor = Color.green; // green traffic light = go fast + public Color slowdownColor = Color.red; // red traffic light = go slow + Color defaultColor; - // initialize EMA with 'emaDuration' seconds worth of history. - // 1 second holds 'sendRate' worth of values. - // multiplied by emaDuration gives n-seconds. - driftEma = new ExponentialMovingAverage(server.sendRate * driftEmaDuration); - deliveryTimeEma = new ExponentialMovingAverage(server.sendRate * deliveryTimeEmaDuration); - } + void Awake() + { + defaultColor = render.sharedMaterial.color; - // add snapshot & initialize client interpolation time if needed - public void OnMessage(Snapshot3D snap) - { - // set local timestamp (= when it was received on our end) + // initialize EMA with 'emaDuration' seconds worth of history. + // 1 second holds 'sendRate' worth of values. + // multiplied by emaDuration gives n-seconds. + driftEma = new ExponentialMovingAverage(server.sendRate * driftEmaDuration); + deliveryTimeEma = new ExponentialMovingAverage(server.sendRate * deliveryTimeEmaDuration); + } + + // add snapshot & initialize client interpolation time if needed + public void OnMessage(Snapshot3D snap) + { + // set local timestamp (= when it was received on our end) #if !UNITY_2020_3_OR_NEWER snap.localTime = NetworkTime.localTime; #else - snap.localTime = Time.timeAsDouble; + snap.localTime = Time.timeAsDouble; #endif - // (optional) dynamic adjustment - if (dynamicAdjustment) - { - // set bufferTime on the fly. - // shows in inspector for easier debugging :) - bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment( - server.sendInterval, - deliveryTimeEma.StandardDeviation, - dynamicAdjustmentTolerance - ); - } + // (optional) dynamic adjustment + if (dynamicAdjustment) + { + // set bufferTime on the fly. + // shows in inspector for easier debugging :) + bufferTimeMultiplier = SnapshotInterpolation.DynamicAdjustment( + server.sendInterval, + deliveryTimeEma.StandardDeviation, + dynamicAdjustmentTolerance + ); + } - // insert into the buffer & initialize / adjust / catchup - SnapshotInterpolation.InsertAndAdjust( - snapshots, - snap, - ref localTimeline, - ref localTimescale, - server.sendInterval, - bufferTime, - bufferTimeMultiplierForClamping, - catchupSpeed, - slowdownSpeed, - ref driftEma, - catchupNegativeThreshold, - catchupPositiveThreshold, - ref deliveryTimeEma); - } + // insert into the buffer & initialize / adjust / catchup + SnapshotInterpolation.InsertAndAdjust( + snapshots, + snap, + ref localTimeline, + ref localTimescale, + server.sendInterval, + bufferTime, + bufferTimeMultiplierForClamping, + catchupSpeed, + slowdownSpeed, + ref driftEma, + catchupNegativeThreshold, + catchupPositiveThreshold, + ref deliveryTimeEma); + } - void Update() - { - // only while we have snapshots. - // timeline starts when the first snapshot arrives. - if (snapshots.Count > 0) - { - // snapshot interpolation - if (interpolate) - { - // step - SnapshotInterpolation.Step( - snapshots, - Time.unscaledDeltaTime, - ref localTimeline, - localTimescale, - out Snapshot3D fromSnapshot, - out Snapshot3D toSnapshot, - out double t); + void Update() + { + // only while we have snapshots. + // timeline starts when the first snapshot arrives. + if (snapshots.Count > 0) + { + // snapshot interpolation + if (interpolate) + { + // step + SnapshotInterpolation.Step( + snapshots, + Time.unscaledDeltaTime, + ref localTimeline, + localTimescale, + out Snapshot3D fromSnapshot, + out Snapshot3D toSnapshot, + out double t); - // interpolate & apply - Snapshot3D computed = Snapshot3D.Interpolate(fromSnapshot, toSnapshot, t); - transform.position = computed.position; - } - // apply raw - else - { - Snapshot3D snap = snapshots.Values[0]; - transform.position = snap.position; - snapshots.RemoveAt(0); - } - } + // interpolate & apply + Snapshot3D computed = Snapshot3D.Interpolate(fromSnapshot, toSnapshot, t); + transform.position = computed.position; + } + // apply raw + else + { + Snapshot3D snap = snapshots.Values[0]; + transform.position = snap.position; + snapshots.RemoveAt(0); + } + } - // color material while catching up / slowing down - if (localTimescale < 1) - render.material.color = slowdownColor; - else if (localTimescale > 1) - render.material.color = catchupColor; - else - render.material.color = defaultColor; - } + // color material while catching up / slowing down + if (localTimescale < 1) + render.material.color = slowdownColor; + else if (localTimescale > 1) + render.material.color = catchupColor; + else + render.material.color = defaultColor; + } - void OnGUI() - { - // display buffer size as number for easier debugging. - // catchup is displayed as color state in Update() already. - const int width = 30; // fit 3 digits - const int height = 20; - Vector2 screen = Camera.main.WorldToScreenPoint(transform.position); - string str = $"{snapshots.Count}"; - GUI.Label(new Rect(screen.x - width / 2, screen.y - height / 2, width, height), str); - } + void OnGUI() + { + // display buffer size as number for easier debugging. + // catchup is displayed as color state in Update() already. + const int width = 30; // fit 3 digits + const int height = 20; + Vector2 screen = Camera.main.WorldToScreenPoint(transform.position); + string str = $"{snapshots.Count}"; + GUI.Label(new Rect(screen.x - width / 2, screen.y - height / 2, width, height), str); + } - void OnValidate() - { - // thresholds need to be <0 and >0 - catchupNegativeThreshold = Math.Min(catchupNegativeThreshold, 0); - catchupPositiveThreshold = Math.Max(catchupPositiveThreshold, 0); - } - } + void OnValidate() + { + // thresholds need to be <0 and >0 + catchupNegativeThreshold = Math.Min(catchupNegativeThreshold, 0); + catchupPositiveThreshold = Math.Max(catchupPositiveThreshold, 0); + } + } } diff --git a/Assets/Mirror/Tests/Editor/SnapshotInterpolationTests.cs b/Assets/Mirror/Tests/Editor/SnapshotInterpolationTests.cs index b36bd1cb7..9db28a1bf 100644 --- a/Assets/Mirror/Tests/Editor/SnapshotInterpolationTests.cs +++ b/Assets/Mirror/Tests/Editor/SnapshotInterpolationTests.cs @@ -7,7 +7,7 @@ namespace Mirror.Tests struct SimpleSnapshot : Snapshot { public double remoteTime { get; set; } - public double localTime { get; set; } + public double localTime { get; set; } public double value; public SimpleSnapshot(double remoteTime, double localTime, double value) @@ -32,10 +32,10 @@ public class SnapshotInterpolationTests SortedList buffer; // some defaults - const double catchupSpeed = 0.02; - const double slowdownSpeed = 0.04; + const double catchupSpeed = 0.02; + const double slowdownSpeed = 0.04; const double negativeThresh = -0.10; - const double positiveThresh = 0.10; + const double positiveThresh = 0.10; [SetUp] public void SetUp() @@ -141,11 +141,11 @@ public void InsertIfNotExists() public void InsertTwice() { // defaults - ExponentialMovingAverage driftEma = default; + ExponentialMovingAverage driftEma = default; ExponentialMovingAverage deliveryIntervalEma = default; - SimpleSnapshot snap = default; + SimpleSnapshot snap = default; - double localTimeline = 0; + double localTimeline = 0; double localTimescale = 0; // insert twice @@ -160,10 +160,10 @@ public void InsertTwice() public void Insert_Sorts() { // defaults - ExponentialMovingAverage driftEma = default; + ExponentialMovingAverage driftEma = default; ExponentialMovingAverage deliveryIntervalEma = default; - double localTimeline = 0; + double localTimeline = 0; double localTimescale = 0; // example snaps @@ -184,10 +184,10 @@ public void Insert_Sorts() public void Insert_InitializesLocalTimeline() { // defaults - ExponentialMovingAverage driftEma = default; + ExponentialMovingAverage driftEma = default; ExponentialMovingAverage deliveryIntervalEma = default; - double localTimeline = 0; + double localTimeline = 0; double localTimescale = 0; // example snaps @@ -207,10 +207,10 @@ public void Insert_InitializesLocalTimeline() public void Insert_ComputesAverageDrift() { // defaults: drift ema with 3 values - ExponentialMovingAverage driftEma = new ExponentialMovingAverage(3); + ExponentialMovingAverage driftEma = new ExponentialMovingAverage(3); ExponentialMovingAverage deliveryIntervalEma = default; - double localTimeline = 0; + double localTimeline = 0; double localTimescale = 0; // example snaps @@ -233,10 +233,10 @@ public void Insert_ComputesAverageDrift() public void Insert_ComputesAverageDrift_Scrambled() { // defaults: drift ema with 3 values - ExponentialMovingAverage driftEma = new ExponentialMovingAverage(3); + ExponentialMovingAverage driftEma = new ExponentialMovingAverage(3); ExponentialMovingAverage deliveryIntervalEma = default; - double localTimeline = 0; + double localTimeline = 0; double localTimescale = 0; // example snaps @@ -264,10 +264,10 @@ public void Insert_ComputesAverageDeliveryInterval() // defaults: delivery ema with 2 values // because delivery time ema is always between 2 snaps. // so for 3 values, it's only computed twice. - ExponentialMovingAverage driftEma = new ExponentialMovingAverage(2); + ExponentialMovingAverage driftEma = new ExponentialMovingAverage(2); ExponentialMovingAverage deliveryIntervalEma = new ExponentialMovingAverage(2); - double localTimeline = 0; + double localTimeline = 0; double localTimescale = 0; // example snaps with local arrival times @@ -295,10 +295,10 @@ public void Insert_ComputesAverageDeliveryInterval_Scrambled() // defaults: delivery ema with 2 values // because delivery time ema is always between 2 snaps. // so for 3 values, it's only computed twice. - ExponentialMovingAverage driftEma = new ExponentialMovingAverage(2); + ExponentialMovingAverage driftEma = new ExponentialMovingAverage(2); ExponentialMovingAverage deliveryIntervalEma = new ExponentialMovingAverage(2); - double localTimeline = 0; + double localTimeline = 0; double localTimescale = 0; // example snaps with local arrival times @@ -324,10 +324,10 @@ public void Insert_ComputesAverageDeliveryInterval_Scrambled() public void Sample() { // defaults - ExponentialMovingAverage driftEma = default; + ExponentialMovingAverage driftEma = default; ExponentialMovingAverage deliveryIntervalEma = default; - double localTimeline = 0; + double localTimeline = 0; double localTimescale = 0; // example snaps @@ -361,10 +361,10 @@ public void Sample() public void Step() { // defaults - ExponentialMovingAverage driftEma = default; + ExponentialMovingAverage driftEma = default; ExponentialMovingAverage deliveryIntervalEma = default; - double localTimeline = 0; + double localTimeline = 0; double localTimescale = 0; // example snaps @@ -386,10 +386,10 @@ public void Step() public void Step_RemovesOld() { // defaults - ExponentialMovingAverage driftEma = default; + ExponentialMovingAverage driftEma = default; ExponentialMovingAverage deliveryIntervalEma = default; - double localTimeline = 0; + double localTimeline = 0; double localTimescale = 0; // example snaps