#if UNITY_EDITOR || DEVELOPMENT_BUILD #define DEVELOPMENT #endif using System; using FishNet.CodeGenerating; using FishNet.Connection; using FishNet.Documenting; using FishNet.Managing; using FishNet.Managing.Transporting; using FishNet.Object.Delegating; using FishNet.Serializing; using FishNet.Transporting; using GameKit.Dependencies.Utilities; using System.Collections.Generic; using System.Text; using FishNet.Serializing.Helping; using UnityEngine; namespace FishNet.Object { public abstract partial class NetworkBehaviour : MonoBehaviour { #region Types. private struct BufferedRpc { /// /// Writer containing the full RPC. /// public PooledWriter Writer; /// /// Which order to send the data in relation to other packets. /// public DataOrderType OrderType; public BufferedRpc(PooledWriter writer, DataOrderType orderType) { Writer = writer; OrderType = orderType; } } #endregion #region Private. /// /// Registered ServerRpc methods. /// private readonly Dictionary _serverRpcDelegates = new(); /// /// Registered ObserversRpc methods. /// private readonly Dictionary _observersRpcDelegates = new(); /// /// Registered TargetRpc methods. /// private readonly Dictionary _targetRpcDelegates = new(); /// /// Number of total RPC methods for scripts in the same inheritance tree for this instance. /// private uint _rpcMethodCount; /// /// Size of every rpcHash for this networkBehaviour. /// private byte _rpcHashSize = 1; /// /// RPCs buffered for new clients. /// private readonly Dictionary _bufferedRpcs = new(); /// /// Connections to exclude from RPCs, such as ExcludeOwner or ExcludeServer. /// private readonly HashSet _networkConnectionCache = new(); #endregion #region Const. /// /// This is an estimated value of what the maximum possible size of a RPC could be. /// Realistically this value is much smaller but this value is used as a buffer. /// private const int MAXIMUM_RPC_HEADER_SIZE = 10; #if DEVELOPMENT /// /// Bytes used to write length for validating Rpc length. /// private const int VALIDATE_RPC_LENGTH_BYTES = 4; #endif #endregion /// /// Called when buffered RPCs should be sent. /// internal void SendBufferedRpcs(NetworkConnection conn) { TransportManager tm = _networkObjectCache.NetworkManager.TransportManager; foreach (BufferedRpc bRpc in _bufferedRpcs.Values) tm.SendToClient((byte)Channel.Reliable, bRpc.Writer.GetArraySegment(), conn, true, bRpc.OrderType); } /// /// Registers a RPC method. /// /// /// [APIExclude] [MakePublic] internal void RegisterServerRpc(uint hash, ServerRpcDelegate del) { if (_serverRpcDelegates.TryAdd(hash, del)) IncreaseRpcMethodCount(); else NetworkManager.LogError($"ServerRpc key {hash} has already been added for {GetType().FullName} on {gameObject.name}"); } /// /// Registers a RPC method. /// /// /// [APIExclude] [MakePublic] internal void RegisterObserversRpc(uint hash, ClientRpcDelegate del) { if (_observersRpcDelegates.TryAdd(hash, del)) IncreaseRpcMethodCount(); else NetworkManager.LogError($"ObserversRpc key {hash} has already been added for {GetType().FullName} on {gameObject.name}"); } /// /// Registers a RPC method. /// /// /// [APIExclude] [MakePublic] internal void RegisterTargetRpc(uint hash, ClientRpcDelegate del) { if (_targetRpcDelegates.TryAdd(hash, del)) IncreaseRpcMethodCount(); else NetworkManager.LogError($"TargetRpc key {hash} has already been added for {GetType().FullName} on {gameObject.name}"); } /// /// Increases rpcMethodCount and rpcHashSize. /// private void IncreaseRpcMethodCount() { _rpcMethodCount++; if (_rpcMethodCount <= byte.MaxValue) _rpcHashSize = 1; else _rpcHashSize = 2; } /// /// Clears all buffered RPCs for this NetworkBehaviour. /// public void ClearBuffedRpcs() { foreach (BufferedRpc bRpc in _bufferedRpcs.Values) bRpc.Writer.Store(); _bufferedRpcs.Clear(); } /// /// Reads a RPC hash. /// /// /// private uint ReadRpcHash(PooledReader reader) { if (_rpcHashSize == 1) return reader.ReadUInt8Unpacked(); else return reader.ReadUInt16(); } /// /// Called when a ServerRpc is received. /// internal void ReadServerRpc(bool fromRpcLink, uint methodHash, PooledReader reader, NetworkConnection sendingClient, Channel channel) { if (!fromRpcLink) methodHash = ReadRpcHash(reader); if (sendingClient == null) { _networkObjectCache.NetworkManager.LogError($"NetworkConnection is null. ServerRpc {methodHash} on object {gameObject.name} [id {ObjectId}] will not complete. Remainder of packet may become corrupt."); return; } if (_serverRpcDelegates.TryGetValueIL2CPP(methodHash, out ServerRpcDelegate data)) data.Invoke(reader, channel, sendingClient); else _networkObjectCache.NetworkManager.LogError($"ServerRpc not found for hash {methodHash} on object {gameObject.name} [id {ObjectId}]. Remainder of packet may become corrupt."); } /// /// Called when an ObserversRpc is received. /// internal void ReadObserversRpc(bool fromRpcLink, uint methodHash, PooledReader reader, Channel channel) { if (!fromRpcLink) methodHash = ReadRpcHash(reader); if (_observersRpcDelegates.TryGetValueIL2CPP(methodHash, out ClientRpcDelegate del)) del.Invoke(reader, channel); else _networkObjectCache.NetworkManager.LogError($"ObserversRpc not found for hash {methodHash} on object {gameObject.name} [id {ObjectId}] . Remainder of packet may become corrupt."); } /// /// Called when an TargetRpc is received. /// internal void ReadTargetRpc(bool fromRpcLink, uint methodHash, PooledReader reader, Channel channel) { if (!fromRpcLink) methodHash = ReadRpcHash(reader); if (_targetRpcDelegates.TryGetValueIL2CPP(methodHash, out ClientRpcDelegate del)) del.Invoke(reader, channel); else _networkObjectCache.NetworkManager.LogError($"TargetRpc not found for hash {methodHash} on object {gameObject.name} [id {ObjectId}] . Remainder of packet may become corrupt."); } /// /// Sends a RPC to server. /// /// /// /// [MakePublic] internal void SendServerRpc(uint hash, PooledWriter methodWriter, Channel channel, DataOrderType orderType) { if (!IsSpawnedWithWarning()) return; _transportManagerCache.CheckSetReliableChannel(methodWriter.Length + MAXIMUM_RPC_HEADER_SIZE, ref channel); PooledWriter writer = CreateRpc(hash, methodWriter, PacketId.ServerRpc, channel); _networkObjectCache.NetworkManager.TransportManager.SendToServer((byte)channel, writer.GetArraySegment(), true, orderType); writer.StoreLength(); } /// /// Sends a RPC to observers. /// /// /// /// [APIExclude] [MakePublic] internal void SendObserversRpc(uint hash, PooledWriter methodWriter, Channel channel, DataOrderType orderType, bool bufferLast, bool excludeServer, bool excludeOwner) { if (!IsSpawnedWithWarning()) return; _transportManagerCache.CheckSetReliableChannel(methodWriter.Length + MAXIMUM_RPC_HEADER_SIZE, ref channel); PooledWriter writer = lCreateRpc(channel); SetNetworkConnectionCache(excludeServer, excludeOwner); _networkObjectCache.NetworkManager.TransportManager.SendToClients((byte)channel, writer.GetArraySegment(), _networkObjectCache.Observers, _networkConnectionCache, true, orderType); /* If buffered then dispose of any already buffered * writers and replace with new one. Writers should * automatically dispose when references are lost * anyway but better safe than sorry. */ if (bufferLast) { if (_bufferedRpcs.TryGetValueIL2CPP(hash, out BufferedRpc result)) result.Writer.StoreLength(); /* If sent on unreliable the RPC has to be rebuilt for * reliable headers since buffered RPCs always send reliably * to new connections. */ if (channel == Channel.Unreliable) { writer.StoreLength(); writer = lCreateRpc(Channel.Reliable); } _bufferedRpcs[hash] = new(writer, orderType); } //If not buffered then dispose immediately. else { writer.StoreLength(); } PooledWriter lCreateRpc(Channel c) { #if DEVELOPMENT if (!NetworkManager.DebugManager.DisableObserversRpcLinks && _rpcLinks.TryGetValueIL2CPP(hash, out RpcLinkType link)) #else if (_rpcLinks.TryGetValueIL2CPP(hash, out RpcLinkType link)) #endif writer = CreateLinkedRpc(link, methodWriter, c); else writer = CreateRpc(hash, methodWriter, PacketId.ObserversRpc, c); return writer; } } /// /// Sends a RPC to target. /// [MakePublic] internal void SendTargetRpc(uint hash, PooledWriter methodWriter, Channel channel, DataOrderType orderType, NetworkConnection target, bool excludeServer, bool validateTarget = true) { if (!IsSpawnedWithWarning()) return; _transportManagerCache.CheckSetReliableChannel(methodWriter.Length + MAXIMUM_RPC_HEADER_SIZE, ref channel); if (validateTarget) { if (target == null) { _networkObjectCache.NetworkManager.LogWarning($"Action cannot be completed as no Target is specified."); return; } else { //If target is not an observer. if (!_networkObjectCache.Observers.Contains(target)) { _networkObjectCache.NetworkManager.LogWarning($"Action cannot be completed as Target is not an observer for object {gameObject.name} [id {ObjectId}]."); return; } } } //Excluding server. if (excludeServer && target.IsLocalClient) return; PooledWriter writer; #if DEVELOPMENT if (!NetworkManager.DebugManager.DisableTargetRpcLinks && _rpcLinks.TryGetValueIL2CPP(hash, out RpcLinkType link)) #else if (_rpcLinks.TryGetValueIL2CPP(hash, out RpcLinkType link)) #endif writer = CreateLinkedRpc(link, methodWriter, channel); else writer = CreateRpc(hash, methodWriter, PacketId.TargetRpc, channel); _networkObjectCache.NetworkManager.TransportManager.SendToClient((byte)channel, writer.GetArraySegment(), target, true, orderType); writer.Store(); } /// /// Adds excluded connections to ExcludedRpcConnections. /// private void SetNetworkConnectionCache(bool addClientHost, bool addOwner) { _networkConnectionCache.Clear(); if (addClientHost && IsClientStarted) _networkConnectionCache.Add(LocalConnection); if (addOwner && Owner.IsValid) _networkConnectionCache.Add(Owner); } /// /// Returns if spawned and throws a warning if not. /// /// private bool IsSpawnedWithWarning() { bool result = this.IsSpawned; if (!result) _networkObjectCache.NetworkManager.LogWarning($"Action cannot be completed as object {gameObject.name} [Id {ObjectId}] is not spawned."); return result; } /// /// Writes a full RPC and returns the writer. /// private PooledWriter CreateRpc(uint hash, PooledWriter methodWriter, PacketId packetId, Channel channel) { int rpcHeaderBufferLength = GetEstimatedRpcHeaderLength(); int methodWriterLength = methodWriter.Length; //Writer containing full packet. PooledWriter writer = WriterPool.Retrieve(rpcHeaderBufferLength + methodWriterLength); writer.WritePacketIdUnpacked(packetId); #if DEVELOPMENT int written = WriteDebugForValidateRpc(writer, packetId, hash); #endif writer.WriteNetworkBehaviour(this); //Only write length if reliable. if (channel == Channel.Reliable) writer.WriteInt32(methodWriterLength + _rpcHashSize); //Hash and data. WriteRpcHash(hash, writer); writer.WriteArraySegment(methodWriter.GetArraySegment()); #if DEVELOPMENT WriteDebugLengthForValidateRpc(writer, written); #endif return writer; } #if DEVELOPMENT /// /// Gets the method name for a Rpc using packetId and Rpc hash. /// private string GetRpcMethodName(PacketId packetId, uint hash) { try { if (packetId == PacketId.ObserversRpc) return _observersRpcDelegates[hash].Method.Name; else if (packetId == PacketId.TargetRpc) return _targetRpcDelegates[hash].Method.Name; else if (packetId == PacketId.ServerRpc) return _serverRpcDelegates[hash].Method.Name; else if (packetId == PacketId.Replicate) return _replicateRpcDelegates[hash].Method.Name; else if (packetId == PacketId.Reconcile) return _reconcileRpcDelegates[hash].Method.Name; else _networkObjectCache.NetworkManager.LogError($"Unhandled packetId of {packetId} for hash {hash}."); } //This should not ever happen. catch { _networkObjectCache.NetworkManager.LogError($"Rpc method name not found for packetId {packetId}, hash {hash}."); } return "Error"; } #endif /// /// Writes rpcHash to writer. /// /// /// private void WriteRpcHash(uint hash, PooledWriter writer) { if (_rpcHashSize == 1) writer.WriteUInt8Unpacked((byte)hash); else writer.WriteUInt16((byte)hash); } #if DEVELOPMENT private int WriteDebugForValidateRpc(Writer writer, PacketId packetId, uint hash) { if (!_networkObjectCache.NetworkManager.DebugManager.ValidateRpcLengths) return -1; writer.Skip(VALIDATE_RPC_LENGTH_BYTES); int positionStart = writer.Position; string txt = $"NetworkObject Details: {_networkObjectCache.ToString()}. NetworkBehaviour Details: Name [{GetType().Name}]. Rpc Details: Name [{GetRpcMethodName(packetId, hash)}] PacketId [{packetId}] Hash [{hash}]"; writer.WriteString(txt); return positionStart; } private void WriteDebugLengthForValidateRpc(Writer writer, int positionStart) { if (!_networkObjectCache.NetworkManager.DebugManager.ValidateRpcLengths) return; //Write length. int writtenLength = (writer.Position - positionStart); writer.InsertInt32Unpacked(writtenLength, positionStart - VALIDATE_RPC_LENGTH_BYTES); } /// /// Parses written data used to validate a Rpc packet. /// internal static void ReadDebugForValidatedRpc(NetworkManager manager, PooledReader reader, out int readerRemainingAfterLength, out string rpcInformation, out uint expectedReadAmount) { rpcInformation = null; expectedReadAmount = 0; readerRemainingAfterLength = 0; if (!manager.DebugManager.ValidateRpcLengths) return; expectedReadAmount = (uint)reader.ReadInt32Unpacked(); readerRemainingAfterLength = reader.Remaining; rpcInformation = reader.ReadStringAllocated(); } /// /// Prints an error if an Rpc packet did not validate correctly. /// /// True if an error occurred. internal static bool TryPrintDebugForValidatedRpc(bool fromRpcLink, NetworkManager manager, PooledReader reader, int startReaderRemaining, string rpcInformation, uint expectedReadAmount, Channel channel) { if (!manager.DebugManager.ValidateRpcLengths) return false; int readAmount = (startReaderRemaining - reader.Remaining); if (readAmount != expectedReadAmount) { string src = (fromRpcLink) ? "RpcLink" : "Rpc"; string msg = $"A {src} read an incorrect amount of data on channel {channel}. Read length was {readAmount}, expected length is {expectedReadAmount}. {rpcInformation}." + $" {manager.PacketIdHistory.GetReceivedPacketIds(packetsFromServer: (reader.Source == Reader.DataSource.Server))}."; manager.LogError(msg); return true; } return false; } #endif } }