using System;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using UnityEngine;
namespace Unity.NetCode.Samples.PlayerList
{
///
/// Manages the component and RPCs,
/// which allows clients to view the names and connection statuses of other clients.
///
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
public partial struct ServerPlayerListSystem : ISystem
{
EntityArchetype m_InvalidUsernameRpcArchetype;
EntityArchetype m_RpcArchetype;
EntityQuery m_PlayerListQuery;
ComponentLookup m_PlayerListEntryFromEntity;
ComponentLookup m_NetworkIdFromEntity;
EntityQuery m_ClientRegisterUsernameRpcQuery;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
m_PlayerListQuery = state.GetEntityQuery(ComponentType.ReadOnly());
var archetypeTypes = new NativeArray(2, Allocator.Temp);
archetypeTypes[0] = ComponentType.ReadOnly();
archetypeTypes[1] = ComponentType.ReadOnly();
m_RpcArchetype = state.EntityManager.CreateArchetype(archetypeTypes);
archetypeTypes[0] = ComponentType.ReadOnly();
m_InvalidUsernameRpcArchetype = state.EntityManager.CreateArchetype(archetypeTypes);
archetypeTypes.Dispose();
m_PlayerListEntryFromEntity = state.GetComponentLookup(true);
m_NetworkIdFromEntity = state.GetComponentLookup(true);
m_ClientRegisterUsernameRpcQuery = state.GetEntityQuery(ComponentType.ReadOnly());
state.RequireForUpdate();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var rpcArchetype = m_RpcArchetype;
var invalidUsernameRpcArchetype = m_InvalidUsernameRpcArchetype;
var ecb = SystemAPI.GetSingleton().CreateCommandBuffer(state.WorldUnmanaged);
var netDbg = SystemAPI.GetSingleton();
var connectionEventsForTick = SystemAPI.GetSingleton().ConnectionEventsForTick;
if (connectionEventsForTick.Length > 0)
{
state.Dependency = new NotifyPlayersOfDisconnectsJob
{
netDbg = netDbg,
ecb = ecb,
rpcArchetype = rpcArchetype,
connectionEventsForTick = connectionEventsForTick,
playerListEntryLookup = SystemAPI.GetComponentLookup(),
}.Schedule(state.Dependency);
}
if (!m_ClientRegisterUsernameRpcQuery.IsEmptyIgnoreFilter)
{
// We only add new players IF they send us a username.
// This ensures that they will always have a valid username from the start.
m_PlayerListEntryFromEntity.Update(ref state);
m_NetworkIdFromEntity.Update(ref state);
var playerListEntries = m_PlayerListQuery.ToComponentDataListAsync(state.WorldUpdateAllocator, state.Dependency, out var gatherPlayerListsHandle);
state.Dependency = new HandleNewJoinersJob
{
ecb = ecb,
netDbg = netDbg,
rpcArchetype = rpcArchetype,
invalidUsernameRpcArchetype = invalidUsernameRpcArchetype,
playerListEntries = m_PlayerListEntryFromEntity,
networkIds = m_NetworkIdFromEntity,
existingPlayerListEntries = playerListEntries,
}.Schedule(gatherPlayerListsHandle);
}
}
[BurstCompile]
public partial struct HandleNewJoinersJob : IJobEntity
{
public EntityCommandBuffer ecb;
public EntityArchetype rpcArchetype;
public EntityArchetype invalidUsernameRpcArchetype;
public NetDebug netDbg;
[ReadOnly]
public ComponentLookup playerListEntries;
[ReadOnly]
public ComponentLookup networkIds;
[ReadOnly]
public NativeList existingPlayerListEntries;
public void Execute(Entity rpcEntity, ref PlayerListEntry.ClientRegisterUsernameRpc rpc, in ReceiveRpcCommandRequest req)
{
if (!networkIds.TryGetComponent(req.SourceConnection, out var networkId))
{
netDbg.DebugLog("Server received a ClientRegisterUsernameRpc from a client who has since disconnected. Ignoring!");
return;
}
// Auto-patch here rather than kicking the player, as players don't pick their default names.
var originalUsername = rpc.Value;
rpc.Value = UsernameSanitizer.SanitizeUsername(rpc.Value, networkId.Value, out var usernameWasSanitized);
if (usernameWasSanitized)
netDbg.LogError($"Server received a PlayerListEntry.ClientRegisterUsernameRpc with an invalid username '{originalUsername}', sanitized to '{rpc.Value}'!");
// Note that a ClientRegisterUsernameRpc can mean either:
if (!playerListEntries.TryGetComponent(req.SourceConnection, out var entry))
{
// A NEW JOINER:
entry.State = new PlayerListEntry.ChangedRpc
{
ChangeType = PlayerListEntry.ChangedRpc.UpdateType.NewJoiner,
Reason = default,
NetworkId = networkId.Value,
Username = rpc
};
ecb.AddComponent(req.SourceConnection, entry);
NotifyJoinerOfAllExistingPlayers(ref existingPlayerListEntries, ref ecb, in rpcArchetype, in req.SourceConnection);
}
else
{
// AN EXISTING PLAYER with a new username:
if (entry.State.Username.Value == rpc.Value)
{
netDbg.LogWarning($"Server received a PlayerListEntry.ChangedRpc from existing player {entry.State.NetworkId} but username '{rpc.Value}' is identical to cached value. Ignoring.");
ecb.DestroyEntity(rpcEntity);
return;
}
netDbg.DebugLog($"Server received a PlayerListEntry.ChangedRpc from an already connected player {entry.State.NetworkId}! Broadcasting the rename ('{entry.State.Username.Value}' >>> '{rpc.Value}').");
entry.State.ChangeType = PlayerListEntry.ChangedRpc.UpdateType.UsernameChange;
entry.State.Username = rpc;
// Update the Servers cached entry.
ecb.SetComponent(req.SourceConnection, entry);
}
// Broadcast notify of username by re-purposing the RPC.
// We only need to broadcast if it's pertinent to other clients:
// Otherwise we just send back to sender.
ecb.RemoveComponent(rpcEntity);
ecb.RemoveComponent(rpcEntity);
ecb.AddComponent(rpcEntity, entry.State);
ecb.AddComponent(rpcEntity);
// Notify the sender that their original became this new, sanitized input, so that they can accept the servers value.
if (usernameWasSanitized)
{
var clientInvalidUsernameRpc = ecb.CreateEntity(invalidUsernameRpcArchetype);
ecb.AddComponent(clientInvalidUsernameRpc, new PlayerListEntry.InvalidUsernameResponseRpc
{
RequestedUsername = originalUsername
});
ecb.SetComponent(clientInvalidUsernameRpc, new SendRpcCommandRequest
{
TargetConnection = req.SourceConnection
});
}
}
}
[BurstCompile]
public partial struct NotifyPlayersOfDisconnectsJob : IJob
{
public NetDebug netDbg;
public EntityCommandBuffer ecb;
public EntityArchetype rpcArchetype;
public NativeArray.ReadOnly connectionEventsForTick;
public ComponentLookup playerListEntryLookup;
public void Execute()
{
foreach (var evt in connectionEventsForTick)
{
if (evt.State == ConnectionState.State.Disconnected)
{
var entryRef = playerListEntryLookup.GetRefRWOptional(evt.ConnectionEntity);
// Ignore if it has not had a PlayerListEntry added to it.
if (!entryRef.IsValid) continue;
ref var entry = ref entryRef.ValueRW;
netDbg.DebugLog($"Server: Established player {evt.ConnectionId} disconnected with reason {evt.DisconnectReason}! Notifying other players.");
entry.State.Reason = evt.DisconnectReason;
entry.State.ChangeType = PlayerListEntry.ChangedRpc.UpdateType.PlayerDisconnect;
// Broadcast notify of state:
var rpcEntity = ecb.CreateEntity(rpcArchetype);
ecb.SetComponent(rpcEntity, entry.State);
// Cleanup the cleanup component (which will also trigger entity deletion).
ecb.RemoveComponent(evt.ConnectionEntity);
}
}
}
}
static void NotifyJoinerOfAllExistingPlayers(ref NativeList existingPlayers, ref EntityCommandBuffer ecb, in EntityArchetype newJoinerArchetype, in Entity targetConnection)
{
for (var i = 0; i < existingPlayers.Length; i++)
{
var notifyOthersRpc = ecb.CreateEntity(newJoinerArchetype);
var changedRpc = existingPlayers[i].State;
changedRpc.ChangeType = PlayerListEntry.ChangedRpc.UpdateType.ExistingPlayer;
ecb.SetComponent(notifyOthersRpc, changedRpc);
ecb.SetComponent(notifyOthersRpc, new SendRpcCommandRequest
{
TargetConnection = targetConnection
});
}
}
}
}