using System;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Entities;
using UnityEngine;
namespace Unity.NetCode.Samples.PlayerList
{
///
/// Receives RPC's, notifying this client of the PRESENCE of other clients.
///
[BurstCompile]
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)]
public partial struct ClientPlayerListSystem : ISystem, ISystemStartStop
{
EntityArchetype m_UsernameRpcArchetype;
EntityQuery m_InvalidUsernameResponseRpc;
EntityQuery m_DesiredUsernameChangedQuery;
public void OnCreate(ref SystemState state)
{
OnCreateBurstCompatible(ref state);
ref var desiredUsernameStore = ref SystemAPI.GetSingletonRW().ValueRW;
if (desiredUsernameStore.Value.IsEmpty) desiredUsernameStore.Value = UsernameSanitizer.GetDefaultUsername(state.World);
m_InvalidUsernameResponseRpc = state.GetEntityQuery(ComponentType.ReadOnly());
m_DesiredUsernameChangedQuery = state.GetEntityQuery(ComponentType.ReadWrite());
m_DesiredUsernameChangedQuery.AddChangedVersionFilter(ComponentType.ReadWrite());
}
[BurstCompile]
public void OnStartRunning(ref SystemState state)
{
var desiredUsername = SystemAPI.GetSingletonRW().ValueRW;
var netDebug = SystemAPI.GetSingleton();
var localPlayerNetworkId = SystemAPI.GetSingleton().Value;
var players = SystemAPI.GetSingletonBuffer();
ref var entry = ref GetOrCreateEntry(players, localPlayerNetworkId);
netDebug.DebugLog($"Client {localPlayerNetworkId} has connected, so sending their DesiredUsername '{desiredUsername.Value}' to the server!");
SendUsernameRpc(ref state, ref desiredUsername, ref entry, "SetUsernameRpc");
}
[BurstCompile]
public void OnStopRunning(ref SystemState state)
{
// The implication is that we disconnected.
SystemAPI.GetSingletonBuffer().Clear();
}
[BurstCompile]
void OnCreateBurstCompatible(ref SystemState state)
{
if (!SystemAPI.HasSingleton())
{
state.EntityManager.CreateSingleton();
}
state.RequireForUpdate();
state.RequireForUpdate();
var componentTypes = new NativeArray(2, Allocator.Temp);
componentTypes[0] = ComponentType.ReadWrite();
componentTypes[1] = ComponentType.ReadWrite();
m_UsernameRpcArchetype = state.EntityManager.CreateArchetype(componentTypes);
componentTypes.Dispose();
state.EntityManager.CreateSingletonBuffer();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var netDebug = SystemAPI.GetSingleton();
var localPlayerNetworkId = SystemAPI.GetSingleton().Value;
var players = SystemAPI.GetSingletonBuffer();
var ecb = SystemAPI.GetSingleton().CreateCommandBuffer(state.WorldUnmanaged);
// Handle username RPC:
if(!m_DesiredUsernameChangedQuery.IsEmpty)
{
var desiredUsername = SystemAPI.GetSingletonRW().ValueRW;
ref var localPlayerEntry = ref GetOrCreateEntry(players, localPlayerNetworkId);
if (localPlayerEntry.State.Username.Value != desiredUsername.Value)
{
netDebug.DebugLog($"Client {localPlayerNetworkId} has changed their DesiredUsername '{desiredUsername.Value}', so notifying server of change.");
SendUsernameRpc(ref state, ref desiredUsername, ref localPlayerEntry, "NotifyUsernameChangedRpc");
}
}
new HandleReceivedStateChangedRpcJob
{
ecb = ecb,
players = players,
netDebug = netDebug,
localPlayerNetworkId = localPlayerNetworkId
}.Schedule();
// Handle invalid username responses:
if(!m_InvalidUsernameResponseRpc.IsEmptyIgnoreFilter)
{
using var rpcs = m_InvalidUsernameResponseRpc.ToComponentDataArray(Allocator.Temp);
foreach (var rpc in rpcs)
{
ref var entry = ref GetOrCreateEntry(players, localPlayerNetworkId);
var desiredUsernameStore = SystemAPI.GetSingletonRW().ValueRW;
// Note that if the user has already changed their username AGAIN, this invalid response should be ignored (as we've already sent another Username Change Request RPC).
if (desiredUsernameStore.Value == rpc.RequestedUsername)
{
desiredUsernameStore.Value = entry.State.Username.Value;
netDebug.LogError($"Local player received InvalidUsernameResponseRpc for '{rpc.RequestedUsername}'. Using '{entry.State.Username.Value}'!");
}
else netDebug.LogError($"Local player received InvalidUsernameResponseRpc for '{rpc.RequestedUsername}', but user attempting '{desiredUsernameStore.Value}'!");
}
state.EntityManager.DestroyEntity(m_InvalidUsernameResponseRpc);
}
}
void SendUsernameRpc(ref SystemState state, ref DesiredUsername desiredUsername, ref PlayerListEntry localPlayerEntry, in FixedString64Bytes rpcName)
{
localPlayerEntry.State.Username.Value = desiredUsername.Value = UsernameSanitizer.SanitizeUsername(desiredUsername.Value, localPlayerEntry.State.NetworkId, out _);
var rpcEntity = state.EntityManager.CreateEntity(m_UsernameRpcArchetype);
state.EntityManager.SetName(rpcEntity, rpcName);
state.EntityManager.SetComponentData(rpcEntity, new PlayerListEntry.ClientRegisterUsernameRpc
{
Value = desiredUsername.Value
});
}
[BurstCompile]
[WithAll(typeof(ReceiveRpcCommandRequest))]
public partial struct HandleReceivedStateChangedRpcJob : IJobEntity
{
public EntityCommandBuffer ecb;
public DynamicBuffer players;
public NetDebug netDebug;
public int localPlayerNetworkId;
public void Execute(Entity rpcEntity, in PlayerListEntry.ChangedRpc rpc)
{
ecb.DestroyEntity(rpcEntity);
ref var entry = ref GetOrCreateEntry(players, rpc.NetworkId);
netDebug.DebugLog($"Client {localPlayerNetworkId} received PlayerListEntry.StateChangedRpc: {PlayerListDebugUtils.ToFixedString(entry.State)} >>> {PlayerListDebugUtils.ToFixedString(rpc)}!");
entry.State = rpc;
}
}
///
/// Because we store entries in a list, fetching an entry involves:
/// 1. Ensuring array capacity.
/// 2. Returning a ref of the entry.
/// Note that a default entry is valid.
///
static unsafe ref PlayerListEntry GetOrCreateEntry(DynamicBuffer players, int networkId)
{
var delta = networkId - players.Length;
if (delta > 0)
players.AddRange(new NativeArray(delta, Allocator.Temp));
return ref UnsafeUtility.ArrayElementAsRef(players.GetUnsafePtr(), networkId - 1);
}
}
}