using FishNet.Broadcast; using FishNet.Broadcast.Helping; using FishNet.Connection; using FishNet.Managing.Logging; using FishNet.Managing.Utility; using FishNet.Object; using FishNet.Serializing; using FishNet.Serializing.Helping; using FishNet.Transporting; using FishNet.Utility.Extension; using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using UnityEngine; namespace FishNet.Managing.Server { public sealed partial class ServerManager : MonoBehaviour { #region Private. /// /// Delegate to read received broadcasts. /// /// /// private delegate void ClientBroadcastDelegate(NetworkConnection connection, PooledReader reader); /// /// Delegates for each key. /// private readonly Dictionary> _broadcastHandlers = new Dictionary>(); /// /// Delegate targets for each key. /// private Dictionary> _handlerTargets = new Dictionary>(); /// /// Connections which can be broadcasted to after having excluded removed. /// private HashSet _connectionsWithoutExclusions = new HashSet(); #endregion /// /// Registers a method to call when a Broadcast arrives. /// /// Type of broadcast being registered. /// Method to call. /// True if the client must be authenticated for the method to call. public void RegisterBroadcast(Action handler, bool requireAuthentication = true) where T : struct, IBroadcast { ushort key = BroadcastHelper.GetKey(); /* Create delegate and add for * handler method. */ HashSet handlers; if (!_broadcastHandlers.TryGetValueIL2CPP(key, out handlers)) { handlers = new HashSet(); _broadcastHandlers.Add(key, handlers); } ClientBroadcastDelegate del = CreateBroadcastDelegate(handler, requireAuthentication); handlers.Add(del); /* Add hashcode of target for handler. * This is so we can unregister the target later. */ int handlerHashCode = handler.GetHashCode(); HashSet<(int, ClientBroadcastDelegate)> targetHashCodes; if (!_handlerTargets.TryGetValueIL2CPP(key, out targetHashCodes)) { targetHashCodes = new HashSet<(int, ClientBroadcastDelegate)>(); _handlerTargets.Add(key, targetHashCodes); } targetHashCodes.Add((handlerHashCode, del)); } /// /// Unregisters a method call from a Broadcast type. /// /// Type of broadcast being unregistered. /// Method to unregister. public void UnregisterBroadcast(Action handler) where T : struct, IBroadcast { ushort key = BroadcastHelper.GetKey(); /* If key is found for T then look for * the appropriate handler to remove. */ if (_broadcastHandlers.TryGetValueIL2CPP(key, out HashSet handlers)) { HashSet<(int, ClientBroadcastDelegate)> targetHashCodes; if (_handlerTargets.TryGetValueIL2CPP(key, out targetHashCodes)) { int handlerHashCode = handler.GetHashCode(); ClientBroadcastDelegate result = null; foreach ((int targetHashCode, ClientBroadcastDelegate del) in targetHashCodes) { if (targetHashCode == handlerHashCode) { result = del; targetHashCodes.Remove((targetHashCode, del)); break; } } //If no more in targetHashCodes then remove from handlerTarget. if (targetHashCodes.Count == 0) _handlerTargets.Remove(key); if (result != null) handlers.Remove(result); } //If no more in handlers then remove broadcastHandlers. if (handlers.Count == 0) _broadcastHandlers.Remove(key); } } /// /// Creates a ClientBroadcastDelegate. /// /// /// /// /// private ClientBroadcastDelegate CreateBroadcastDelegate(Action handler, bool requireAuthentication) { void LogicContainer(NetworkConnection connection, PooledReader reader) { //If requires authentication and client isn't authenticated. if (requireAuthentication && !connection.Authenticated) { connection.Kick(KickReason.ExploitAttempt, LoggingType.Common, $"ConnectionId {connection.ClientId} sent broadcast {typeof(T).Name} which requires authentication, but client was not authenticated. Client has been disconnected."); return; } T broadcast = reader.Read(); handler?.Invoke(connection, broadcast); } return LogicContainer; } /// /// Parses a received broadcast. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ParseBroadcast(PooledReader reader, NetworkConnection conn, Channel channel) { ushort key = reader.ReadUInt16(); int dataLength = Packets.GetPacketLength((ushort)PacketId.Broadcast, reader, channel); //Try to invoke the handler for that message if (_broadcastHandlers.TryGetValueIL2CPP(key, out HashSet handlers)) { int readerStartPosition = reader.Position; /* //muchlater resetting the position could be better by instead reading once and passing in * the object to invoke with. */ bool rebuildHandlers = false; //True if data is read at least once. Otherwise it's length will have to be purged. bool dataRead = false; foreach (ClientBroadcastDelegate handler in handlers) { if (handler.Target == null) { NetworkManager.LogWarning($"A Broadcast handler target is null. This can occur when a script is destroyed but does not unregister from a Broadcast."); rebuildHandlers = true; } else { reader.Position = readerStartPosition; handler.Invoke(conn, reader); dataRead = true; } } //If rebuilding handlers... if (rebuildHandlers) { List dels = handlers.ToList(); handlers.Clear(); for (int i = 0; i < dels.Count; i++) { if (dels[i].Target != null) handlers.Add(dels[i]); } } //Make sure data was read as well. if (!dataRead) reader.Skip(dataLength); } else { reader.Skip(dataLength); } } /// /// Sends a broadcast to a connection. /// /// Type of broadcast to send. /// Connection to send to. /// Broadcast data being sent; for example: an instance of your broadcast type. /// True if the client must be authenticated for this broadcast to send. /// Channel to send on. public void Broadcast(NetworkConnection connection, T message, bool requireAuthenticated = true, Channel channel = Channel.Reliable) where T : struct, IBroadcast { if (!Started) { NetworkManager.LogWarning($"Cannot send broadcast to client because server is not active."); return; } if (requireAuthenticated && !connection.Authenticated) { NetworkManager.LogWarning($"Cannot send broadcast to client because they are not authenticated."); return; } using (PooledWriter writer = WriterPool.GetWriter()) { Broadcasts.WriteBroadcast(writer, message, channel); ArraySegment segment = writer.GetArraySegment(); NetworkManager.TransportManager.SendToClient((byte)channel, segment, connection); } } /// /// Sends a broadcast to connections. /// /// Type of broadcast to send. /// Connections to send to. /// Broadcast data being sent; for example: an instance of your broadcast type. /// True if the clients must be authenticated for this broadcast to send. /// Channel to send on. public void Broadcast(HashSet connections, T message, bool requireAuthenticated = true, Channel channel = Channel.Reliable) where T : struct, IBroadcast { if (!Started) { NetworkManager.LogWarning($"Cannot send broadcast to client because server is not active."); return; } bool failedAuthentication = false; using (PooledWriter writer = WriterPool.GetWriter()) { Broadcasts.WriteBroadcast(writer, message, channel); ArraySegment segment = writer.GetArraySegment(); foreach (NetworkConnection conn in connections) { if (requireAuthenticated && !conn.Authenticated) failedAuthentication = true; else NetworkManager.TransportManager.SendToClient((byte)channel, segment, conn); } } if (failedAuthentication) { NetworkManager.LogWarning($"One or more broadcast did not send to a client because they were not authenticated."); return; } } /// /// Sends a broadcast to connections except excluded. /// /// Type of broadcast to send. /// Connections to send to. /// Connection to exclude. /// Broadcast data being sent; for example: an instance of your broadcast type. /// True if the clients must be authenticated for this broadcast to send. /// Channel to send on. public void BroadcastExcept(HashSet connections, NetworkConnection excludedConnection, T message, bool requireAuthenticated = true, Channel channel = Channel.Reliable) where T : struct, IBroadcast { if (!Started) { NetworkManager.LogWarning($"Cannot send broadcast to client because server is not active."); return; } //Fast exit if no exclusions. if (excludedConnection == null || !excludedConnection.IsValid) { Broadcast(connections, message, requireAuthenticated, channel); return; } connections.Remove(excludedConnection); Broadcast(connections, message, requireAuthenticated, channel); } /// /// Sends a broadcast to connections except excluded. /// /// Type of broadcast to send. /// Connections to send to. /// Connections to exclude. /// Broadcast data being sent; for example: an instance of your broadcast type. /// True if the clients must be authenticated for this broadcast to send. /// Channel to send on. public void BroadcastExcept(HashSet connections, HashSet excludedConnections, T message, bool requireAuthenticated = true, Channel channel = Channel.Reliable) where T : struct, IBroadcast { if (!Started) { NetworkManager.LogWarning($"Cannot send broadcast to client because server is not active."); return; } //Fast exit if no exclusions. if (excludedConnections == null || excludedConnections.Count == 0) { Broadcast(connections, message, requireAuthenticated, channel); return; } /* I'm not sure if the hashset API such as intersect generates * GC or not but I'm betting doing remove locally is faster, or * just as fast. */ foreach (NetworkConnection ec in excludedConnections) connections.Remove(ec); Broadcast(connections, message, requireAuthenticated, channel); } /// /// Sends a broadcast to all connections except excluded. /// /// Type of broadcast to send. /// Connection to exclude. /// Broadcast data being sent; for example: an instance of your broadcast type. /// True if the clients must be authenticated for this broadcast to send. /// Channel to send on. public void BroadcastExcept(NetworkConnection excludedConnection, T message, bool requireAuthenticated = true, Channel channel = Channel.Reliable) where T : struct, IBroadcast { if (!Started) { NetworkManager.LogWarning($"Cannot send broadcast to client because server is not active."); return; } //Fast exit if there are no excluded. if (excludedConnection == null || !excludedConnection.IsValid) { Broadcast(message, requireAuthenticated, channel); return; } _connectionsWithoutExclusions.Clear(); /* It will be faster to fill the entire list then * remove vs checking if each connection is contained within excluded. */ foreach (NetworkConnection c in Clients.Values) _connectionsWithoutExclusions.Add(c); //Remove _connectionsWithoutExclusions.Remove(excludedConnection); Broadcast(_connectionsWithoutExclusions, message, requireAuthenticated, channel); } /// /// Sends a broadcast to all connections except excluded. /// /// Type of broadcast to send. /// Connections to send to. /// Broadcast data being sent; for example: an instance of your broadcast type. /// True if the clients must be authenticated for this broadcast to send. /// Channel to send on. public void BroadcastExcept(HashSet excludedConnections, T message, bool requireAuthenticated = true, Channel channel = Channel.Reliable) where T : struct, IBroadcast { if (!Started) { NetworkManager.LogWarning($"Cannot send broadcast to client because server is not active."); return; } //Fast exit if there are no excluded. if (excludedConnections == null || excludedConnections.Count == 0) { Broadcast(message, requireAuthenticated, channel); return; } _connectionsWithoutExclusions.Clear(); /* It will be faster to fill the entire list then * remove vs checking if each connection is contained within excluded. */ foreach (NetworkConnection c in Clients.Values) _connectionsWithoutExclusions.Add(c); //Remove foreach (NetworkConnection c in excludedConnections) _connectionsWithoutExclusions.Remove(c); Broadcast(_connectionsWithoutExclusions, message, requireAuthenticated, channel); } /// /// Sends a broadcast to observers. /// /// Type of broadcast to send. /// NetworkObject to use Observers from. /// Broadcast data being sent; for example: an instance of your broadcast type. /// True if the clients must be authenticated for this broadcast to send. /// Channel to send on. [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Broadcast(NetworkObject networkObject, T message, bool requireAuthenticated = true, Channel channel = Channel.Reliable) where T : struct, IBroadcast { if (networkObject == null) { NetworkManager.LogWarning($"Cannot send broadcast because networkObject is null."); return; } Broadcast(networkObject.Observers, message, requireAuthenticated, channel); } /// /// Sends a broadcast to all clients. /// /// Type of broadcast to send. /// Broadcast data being sent; for example: an instance of your broadcast type. /// True if the clients must be authenticated for this broadcast to send. /// Channel to send on. public void Broadcast(T message, bool requireAuthenticated = true, Channel channel = Channel.Reliable) where T : struct, IBroadcast { if (!Started) { NetworkManager.LogWarning($"Cannot send broadcast to client because server is not active."); return; } bool failedAuthentication = false; using (PooledWriter writer = WriterPool.GetWriter()) { Broadcasts.WriteBroadcast(writer, message, channel); ArraySegment segment = writer.GetArraySegment(); foreach (NetworkConnection conn in Clients.Values) { // if (requireAuthenticated && !conn.Authenticated) failedAuthentication = true; else NetworkManager.TransportManager.SendToClient((byte)channel, segment, conn); } } if (failedAuthentication) { NetworkManager.LogWarning($"One or more broadcast did not send to a client because they were not authenticated."); return; } } } }