using System; using System.Collections.Generic; using System.Text.RegularExpressions; using Unity.Collections; using Unity.Entities; using Unity.Jobs; using UnityEngine; using Unity.NetCode; using Unity.Sample.Core; [UpdateInGroup(typeof(ServerSimulationSystemGroup))] [AlwaysSynchronizeSystem] public class ChatSystemServer : JobComponentSystem { // TODO : The integration is annoying because we don't have a proper permanent place for player // info (world is destroyed for each level). We should try to make this smoother public void ResetChatTime() { m_StartTime = Game.Clock.ElapsedMilliseconds; } char[] _msgBuf = new char[256]; public void SendChatAnnouncement(string message) { var c = Mathf.Min(256, message.Length); message.CopyTo(0, _msgBuf, 0, c); SendChatAnnouncement(new CharBufView(_msgBuf, c)); } char[] _buf = new char[256]; public void SendChatAnnouncement(CharBufView message) { var time = (Game.Clock.ElapsedMilliseconds - m_StartTime) / 1000; var minutes = (int)time / 60; var seconds = (int)time % 60; var formatted_length = StringFormatter.Write(ref _buf, 0, "[{0}:{1:00}] {2}", minutes, seconds, message); unsafe { fixed (char* ptr = message.buf) { GameDebug.Assert(message.length <= NativeString512.MaxLength); var msg = new NativeString512(); msg.CopyFrom(ptr, (ushort)message.length); var connectionEntities = m_ConnectionQuery.ToEntityArray(Allocator.TempJob); for (int i = 0; i < connectionEntities.Length; ++i) { var connectionEntity = connectionEntities[i]; m_RpcChatQueue.Schedule(EntityManager.GetBuffer(connectionEntity), new RpcChatMessage { Message = msg }); } connectionEntities.Dispose(); } } } protected override void OnCreate() { m_ClientsQuery = EntityManager.CreateEntityQuery(ComponentType.ReadWrite(), ComponentType.ReadWrite()); m_ConnectionQuery = GetEntityQuery(ComponentType.ReadOnly()); m_RpcChatQueue = World.GetOrCreateSystem().GetRpcQueue(); } protected override JobHandle OnUpdate(JobHandle inputDeps) { var connectionEntity = m_ConnectionQuery.ToEntityArray(Allocator.TempJob); // The chat system receive message handling has to happen on the main thread (so can't be jobified atm) var PostUpdateCommands = new EntityCommandBuffer(Allocator.TempJob); Entities .WithNativeDisableContainerSafetyRestriction(PostUpdateCommands) .WithoutBurst() // uses strings .ForEach((Entity entity, ref IncomingChatMessageComponent state) => { for (int i = 0; i < connectionEntity.Length; ++i) { if (connectionEntity[i] == state.Connection) { ReceiveMessage(connectionEntity[i], state.Message.ToString()); } } PostUpdateCommands.RemoveComponent(entity, typeof(IncomingChatMessageComponent)); }).Run(); connectionEntity.Dispose(); PostUpdateCommands.Playback(EntityManager); PostUpdateCommands.Dispose(); return default; } private void ReceiveMessage(Entity from, string message) { ChatMessageType type; Entity target; var time = (Game.Clock.ElapsedMilliseconds - m_StartTime) / 1000; var minutes = time / 60; var seconds = time % 60; var text = ParseMessage(from, message, out type, out target); var fromPlayer = EntityManager.GetComponentData(from).playerEntity; var fromState = EntityManager.GetComponentData(fromPlayer); var fromName = fromState.playerName.ToString(); if (type == ChatMessageType.Whisper) { if (target != Entity.Null) { var targetPlayer = EntityManager.GetComponentData(target).playerEntity; var targetState = EntityManager.GetComponentData(targetPlayer); var targetName = targetState.playerName.ToString(); var fromLine = string.Format("[{0}:{1:00}] [From {2}] {3}", minutes, seconds, fromName, text); SendChatMessage(EntityManager.GetComponentData(target).Value, fromLine); var toLine = string.Format("[{0}:{1:00}] [To {2}] {3}", minutes, seconds, targetName, text); SendChatMessage(EntityManager.GetComponentData(from).Value, toLine); } else SendChatMessage(EntityManager.GetComponentData(from).Value, string.Format(" Player not found")); } else if (type == ChatMessageType.All || type == ChatMessageType.Team) { var marker = type == ChatMessageType.All ? "[All] " : ""; var friendly = string.Format("[{0}:{1:00}] {2}{3} {4}", minutes, seconds, marker, fromName, text); var hostile = string.Format("[{0}:{1:00}] {2}{3} {4}", minutes, seconds, marker, fromName, text); var fromTeamIndex = fromPlayer != Entity.Null ? fromState.teamIndex : -1; var clients = m_ClientsQuery.ToEntityArray(Allocator.TempJob); for (int i = 0; i < clients.Length; ++i) { var playerEntity = EntityManager.GetComponentData(clients[i]) .playerEntity; var netId = EntityManager.GetComponentData(clients[i]).Value; var toState = EntityManager.GetComponentData(playerEntity); var targetTeamIndex = playerEntity != Entity.Null ? toState.teamIndex : -1; if (fromTeamIndex == targetTeamIndex) SendChatMessage(netId, friendly); else if (type == ChatMessageType.All) SendChatMessage(netId, hostile); } clients.Dispose(); } } string ParseMessage(Entity from, string message, out ChatMessageType type, out Entity target) { type = ChatMessageType.All; target = Entity.Null; var match = m_CommandRegex.Match(message); if (match.Success) { var command = match.Groups[1].Value.ToLower(); var actualMessage = match.Groups[2].Value; switch (command) { case "t": case "team": type = ChatMessageType.Team; return match.Groups[2].Value; case "w": case "whisper": var match2 = m_TargetRegex.Match(actualMessage); if (match2.Success) { type = ChatMessageType.Whisper; // try to find client var name = !String.IsNullOrEmpty(match2.Groups[1].Value) ? match2.Groups[1].Value : match2.Groups[2].Value; var clients = m_ClientsQuery.ToEntityArray(Allocator.TempJob); for (int i = 0; i < clients.Length; ++i) { var targetPlayer = EntityManager.GetComponentData(clients[i]).playerEntity; var targetState = EntityManager.GetComponentData(targetPlayer); var targetName = targetState.playerName.ToString(); if (targetName.Equals(name, StringComparison.InvariantCultureIgnoreCase)) { target = clients[i]; m_ReplyTracker[target] = from; return match2.Groups[3].Value; } } clients.Dispose(); } return actualMessage; case "r": case "reply": if (m_ReplyTracker.TryGetValue(from, out target)) { type = ChatMessageType.Whisper; m_ReplyTracker[target] = from; } return actualMessage; case "a": case "all": default: return actualMessage; } } return message; } public void SendChatMessage(int clientId, string message) { var connectionEntities = m_ConnectionQuery.ToEntityArray(Allocator.TempJob); for (int i = 0; i < connectionEntities.Length; ++i) { var connectionEntity = connectionEntities[i]; if (EntityManager.GetComponentData(connectionEntity).Value == clientId) m_RpcChatQueue.Schedule(EntityManager.GetBuffer(connectionEntity), new RpcChatMessage { Message = new NativeString512(message) }); } connectionEntities.Dispose(); } long m_StartTime; Regex m_CommandRegex = new Regex(@"^/(\w+)\s+(.*)"); // e.g. "/all hey" Regex m_TargetRegex = new Regex(@"^(?:""(.*)""|([^\s]*))\s*(.+)"); // e.g. "some user" hey there Dictionary m_ReplyTracker = new Dictionary(); EntityQuery m_ClientsQuery; private EntityQuery m_ConnectionQuery; private RpcQueue m_RpcChatQueue; }