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;
}
}
}
}