diff --git a/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs b/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs index 4c312763..4fe3f7f4 100644 --- a/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs +++ b/Assets/FishNet/Runtime/Generated/Component/NetworkAnimator/NetworkAnimator.cs @@ -8,6 +8,7 @@ using FishNet.Managing.Server; using FishNet.Object; using FishNet.Serializing; +using FishNet.Transporting; using FishNet.Utility; using FishNet.Utility.Performance; using GameKit.Dependencies.Utilities; @@ -334,6 +335,25 @@ public bool ClientAuthoritative [Tooltip("True to synchronize server results back to owner. Typically used when you are changing animations on the server and are relying on the server response to update the clients animations.")] [SerializeField] private bool _sendToOwner; + /// + /// Channel for continuous parameter updates (floats, layer weights, speed). State changes and triggers always send reliably regardless of this setting. + /// + [Tooltip("Channel for continuous parameter updates (floats, layer weights, speed). " + + "State changes and triggers always send reliably regardless of this setting. " + + "Default Reliable preserves legacy behavior. Set to Unreliable to relieve " + + "reliable-channel pressure with many concurrent NetworkAnimators. May be " + + "changed at runtime — e.g. set to Reliable when close to a player or for " + + "high-importance entities, Unreliable otherwise.")] + [SerializeField] + private Channel _continuousChannel = Channel.Reliable; + /// + /// Channel for continuous parameter updates (floats, layer weights, speed). May be changed at runtime. + /// + public Channel ContinuousChannel + { + get => _continuousChannel; + set => _continuousChannel = value; + } #endregion #region Private. @@ -438,6 +458,16 @@ private bool _canSmoothFloats /// private PooledWriter _writer = new(); /// + /// Events writer for the split-channel path (triggers, STATE, CROSSFADE — always reliable). + /// Only used when ContinuousChannel != Reliable. + /// + private PooledWriter _eventsWriter = new(); + /// + /// Values writer for the split-channel path (bool/float/int params, LAYER_WEIGHT, SPEED — channel configurable). + /// Only used when ContinuousChannel != Reliable. + /// + private PooledWriter _valuesWriter = new(); + /// /// Holds client authoritative updates received to send to other clients. /// private ClientAuthoritativeUpdate _clientAuthoritativeUpdates; @@ -758,8 +788,23 @@ private void CheckSendToServer() * because there's no way the sent bytes are * ever going to come close to the mtu * when sending a single update. */ - if (AnimatorUpdated(out ArraySegment updatedBytes, _forceAllOnTimed)) - ServerAnimatorUpdated(updatedBytes); + if (_continuousChannel == Channel.Reliable) + { + /* LEGACY PATH — bit-for-bit identical to upstream. */ + if (AnimatorUpdated(out ArraySegment updatedBytes, _forceAllOnTimed)) + ServerAnimatorUpdated(updatedBytes); + } + else + { + /* SPLIT PATH — opt-in. Events always reliable; values use _continuousChannel. */ + if (AnimatorUpdatedSplit(out ArraySegment events, out ArraySegment values, _forceAllOnTimed)) + { + if (events.Count > 0) + ServerAnimatorEvents(events); + if (values.Count > 0) + ServerAnimatorValues(values, _continuousChannel); + } + } _forceAllOnTimed = false; } @@ -852,13 +897,28 @@ private void CheckSendToClients() //Sending from server, send what's changed. else { - if (AnimatorUpdated(out ArraySegment updatedBytes, _forceAllOnTimed)) - SendSegment(updatedBytes); + if (_continuousChannel == Channel.Reliable) + { + /* LEGACY PATH — bit-for-bit identical to upstream. */ + if (AnimatorUpdated(out ArraySegment updatedBytes, _forceAllOnTimed)) + SendSegment(updatedBytes); + } + else + { + /* SPLIT PATH — opt-in. Events always reliable; values use _continuousChannel. */ + if (AnimatorUpdatedSplit(out ArraySegment events, out ArraySegment values, _forceAllOnTimed)) + { + if (events.Count > 0) + SendEventsSegment(events); + if (values.Count > 0) + SendValuesSegment(values); + } + } _forceAllOnTimed = false; } - //Sends segment to clients + //Sends segment to clients (legacy single-RPC path; also used for client-auth relay). void SendSegment(ArraySegment data) { foreach (NetworkConnection nc in Observers) @@ -869,6 +929,28 @@ void SendSegment(ArraySegment data) TargetAnimatorUpdated(nc, data); } } + + //Sends events segment to clients (split path, always reliable). + void SendEventsSegment(ArraySegment data) + { + foreach (NetworkConnection nc in Observers) + { + if (!_sendToOwner && nc == Owner) + continue; + TargetAnimatorEvents(nc, data); + } + } + + //Sends values segment to clients (split path, channel from _continuousChannel). + void SendValuesSegment(ArraySegment data) + { + foreach (NetworkConnection nc in Observers) + { + if (!_sendToOwner && nc == Owner) + continue; + TargetAnimatorValues(nc, data, _continuousChannel); + } + } } } @@ -1075,6 +1157,150 @@ private bool AnimatorUpdated(out ArraySegment updatedBytes, bool forceAll } } + /// + /// Same as AnimatorUpdated, but writes events (triggers/STATE/CROSSFADE) and values + /// (bool/float/int params/LAYER_WEIGHT/SPEED) into separate streams. Used by the split-channel + /// path when ContinuousChannel != Reliable. Returns true if either stream has data. + /// + private bool AnimatorUpdatedSplit(out ArraySegment eventsBytes, out ArraySegment valuesBytes, bool forceAll = false) + { + eventsBytes = default; + valuesBytes = default; + if (_layerWeights == null) + return false; + + _eventsWriter.Clear(); + _valuesWriter.Clear(); + + /* Continuous parameters (bool/float/int) — values stream. */ + for (byte parameterIndex = 0; parameterIndex < _parameterDetails.Count; parameterIndex++) + { + ParameterDetail pd = _parameterDetails[parameterIndex]; + if (pd.ControllerParameter.type == AnimatorControllerParameterType.Bool) + { + bool next = _animator.GetBool(pd.Hash); + if (forceAll || _bools[pd.TypeIndex] != next) + { + _valuesWriter.WriteUInt8Unpacked(parameterIndex); + _valuesWriter.WriteBoolean(next); + _bools[pd.TypeIndex] = next; + } + } + else if (pd.ControllerParameter.type == AnimatorControllerParameterType.Float) + { + float next = _animator.GetFloat(pd.Hash); + if (forceAll || _floats[pd.TypeIndex] != next) + { + _valuesWriter.WriteUInt8Unpacked(parameterIndex); + _valuesWriter.WriteSingle(next); + _floats[pd.TypeIndex] = next; + } + } + else if (pd.ControllerParameter.type == AnimatorControllerParameterType.Int) + { + int next = _animator.GetInteger(pd.Hash); + if (forceAll || _ints[pd.TypeIndex] != next) + { + _valuesWriter.WriteUInt8Unpacked(parameterIndex); + _valuesWriter.WriteInt32(next); + _ints[pd.TypeIndex] = next; + } + } + } + + /* Triggers — events stream (one-shots, must arrive). */ + for (int i = 0; i < _triggerUpdates.Count; i++) + { + _eventsWriter.WriteUInt8Unpacked(_triggerUpdates[i].ParameterIndex); + _eventsWriter.WriteBoolean(_triggerUpdates[i].Setting); + } + _triggerUpdates.Clear(); + + /* States — events stream (must arrive). */ + if (forceAll) + { + for (int i = 0; i < _animator.layerCount; i++) + _unsynchronizedLayerStates[i] = new(Time.frameCount); + } + + if (_unsynchronizedLayerStates.Count > 0) + { + int frameCount = Time.frameCount; + List sentLayers = CollectionCaches.RetrieveList(); + foreach (KeyValuePair item in _unsynchronizedLayerStates) + { + if (frameCount == item.Value.FrameCount) + continue; + + sentLayers.Add(item.Key); + int layerIndex = item.Key; + StateChange sc = item.Value; + if (!sc.IsCrossfade) + { + if (ReturnCurrentLayerState(out int stateHash, out float normalizedTime, layerIndex)) + { + _eventsWriter.WriteUInt8Unpacked(STATE); + _eventsWriter.WriteUInt8Unpacked((byte)layerIndex); + _eventsWriter.WriteInt32Unpacked(stateHash); + _eventsWriter.WriteSingle(normalizedTime); + } + } + else + { + _eventsWriter.WriteUInt8Unpacked(CROSSFADE); + _eventsWriter.WriteUInt8Unpacked((byte)layerIndex); + _eventsWriter.WriteInt32(sc.Hash); + _eventsWriter.WriteBoolean(sc.FixedTime); + _eventsWriter.WriteSingle(sc.DurationTime); + _eventsWriter.WriteSingle(sc.OffsetTime); + _eventsWriter.WriteSingle(sc.NormalizedTransitionTime); + } + } + + if (sentLayers.Count > 0) + { + for (int i = 0; i < sentLayers.Count; i++) + _unsynchronizedLayerStates.Remove(sentLayers[i]); + CollectionCaches.Store(sentLayers); + } + } + + /* Layer weights — values stream. */ + for (int layerIndex = 0; layerIndex < _layerWeights.Length; layerIndex++) + { + float next = _animator.GetLayerWeight(layerIndex); + if (forceAll || _layerWeights[layerIndex] != next) + { + _valuesWriter.WriteUInt8Unpacked(LAYER_WEIGHT); + _valuesWriter.WriteUInt8Unpacked((byte)layerIndex); + _valuesWriter.WriteSingle(next); + _layerWeights[layerIndex] = next; + } + } + + /* Speed — values stream. */ + float speedNext = _animator.speed; + if (forceAll || _speed != speedNext) + { + _valuesWriter.WriteUInt8Unpacked(SPEED); + _valuesWriter.WriteSingle(speedNext); + _speed = speedNext; + } + + bool any = false; + if (_eventsWriter.Position > 0) + { + eventsBytes = _eventsWriter.GetArraySegment(); + any = true; + } + if (_valuesWriter.Position > 0) + { + valuesBytes = _valuesWriter.GetArraySegment(); + any = true; + } + return any; + } + /// /// Applies changed parameters to the animator. /// @@ -1543,6 +1769,90 @@ private void ServerAnimatorUpdated(ArraySegment data) ApplyParametersUpdated(ref data); _clientAuthoritativeUpdates.AddToBuffer(ref data); } + + /// + /// Called on clients to receive the events stream (triggers, STATE, CROSSFADE) — always reliable. + /// Used by the split-channel path when ContinuousChannel != Reliable. + /// + [TargetRpc(ValidateTarget = false)] + private void TargetAnimatorEvents(NetworkConnection connection, ArraySegment data) + { + ReceiveTargetAnimatorPayload(connection, data); + } + + /// + /// Called on clients to receive the values stream (continuous parameters, LAYER_WEIGHT, SPEED). + /// Channel is configurable per call via the trailing arg; defaults to Reliable. + /// Used by the split-channel path when ContinuousChannel != Reliable. + /// + [TargetRpc(ValidateTarget = false)] + private void TargetAnimatorValues(NetworkConnection connection, ArraySegment data, Channel channel = Channel.Reliable) + { + ReceiveTargetAnimatorPayload(connection, data); + } + + /// + /// Shared receive logic for the split-channel target RPCs. + /// Mirrors TargetAnimatorUpdated. + /// + private void ReceiveTargetAnimatorPayload(NetworkConnection connection, ArraySegment data) + { + if (!_canSynchronizeAnimator) + return; + + if (IsServerInitialized && connection.IsLocalClient) + return; + + bool clientAuth = ClientAuthoritative; + bool isOwner = IsOwner; + if (clientAuth && isOwner) + return; + if (!clientAuth && !_sendToOwner && isOwner) + return; + + ReceivedServerData rd = new(data); + _fromServerBuffer.Enqueue(rd); + + if (_startTick == 0) + _startTick = TimeManager.LocalTick + _interpolation; + } + + /// + /// Called on server to receive the events stream from a client-authoritative client — always reliable. + /// + [ServerRpc] + private void ServerAnimatorEvents(ArraySegment data) + { + ReceiveServerAnimatorPayload(data); + } + + /// + /// Called on server to receive the values stream from a client-authoritative client. + /// Channel is configurable per call; defaults to Reliable. + /// + [ServerRpc] + private void ServerAnimatorValues(ArraySegment data, Channel channel = Channel.Reliable) + { + ReceiveServerAnimatorPayload(data); + } + + /// + /// Shared receive logic for the split-channel server RPCs. + /// Mirrors ServerAnimatorUpdated. + /// + private void ReceiveServerAnimatorPayload(ArraySegment data) + { + if (!_canSynchronizeAnimator) + return; + if (!ClientAuthoritative) + { + Owner.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"Connection Id {Owner.ClientId} has been kicked for trying to update this object without client authority."); + return; + } + + ApplyParametersUpdated(ref data); + _clientAuthoritativeUpdates.AddToBuffer(ref data); + } #endregion #region Editor.