diff --git a/Content.Client/Chat/CollectiveMindSystem.cs b/Content.Client/Chat/CollectiveMindSystem.cs new file mode 100644 index 000000000000..f1d89e06af34 --- /dev/null +++ b/Content.Client/Chat/CollectiveMindSystem.cs @@ -0,0 +1,31 @@ +using Content.Client.Chat.Managers; +using Content.Shared.CollectiveMind; +using Robust.Client.Player; + +namespace Content.Client.Chat +{ + public sealed class CollectiveMindSystem : EntitySystem + { + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(OnRemove); + } + + public bool IsCollectiveMind => CompOrNull(_playerManager.LocalPlayer?.ControlledEntity) != null; + + private void OnInit(EntityUid uid, CollectiveMindComponent component, ComponentInit args) + { + _chatManager.UpdatePermissions(); + } + + private void OnRemove(EntityUid uid, CollectiveMindComponent component, ComponentRemove args) + { + _chatManager.UpdatePermissions(); + } + } +} diff --git a/Content.Client/Chat/Managers/ChatManager.cs b/Content.Client/Chat/Managers/ChatManager.cs index 68707e021c5c..d8cd73b7923e 100644 --- a/Content.Client/Chat/Managers/ChatManager.cs +++ b/Content.Client/Chat/Managers/ChatManager.cs @@ -14,6 +14,7 @@ internal sealed class ChatManager : IChatManager [Dependency] private readonly IEntitySystemManager _systems = default!; private ISawmill _sawmill = default!; + public event Action? PermissionsUpdated; public void Initialize() { @@ -77,8 +78,17 @@ public void SendMessage(string text, ChatSelectChannel channel) _consoleHost.ExecuteCommand($"whisper \"{CommandParsing.Escape(str)}\""); break; + case ChatSelectChannel.CollectiveMind: + _consoleHost.ExecuteCommand($"cmsay \"{CommandParsing.Escape(str)}\""); + break; + default: throw new ArgumentOutOfRangeException(nameof(channel), channel, null); } } + + public void UpdatePermissions() + { + PermissionsUpdated?.Invoke(); + } } diff --git a/Content.Client/Chat/Managers/IChatManager.cs b/Content.Client/Chat/Managers/IChatManager.cs index 62a97c6bd82b..1abcf2926755 100644 --- a/Content.Client/Chat/Managers/IChatManager.cs +++ b/Content.Client/Chat/Managers/IChatManager.cs @@ -4,6 +4,14 @@ namespace Content.Client.Chat.Managers { public interface IChatManager : ISharedChatManager { + void Initialize(); + + /// + /// Will refresh perms. + /// + event Action PermissionsUpdated; + public void SendMessage(string text, ChatSelectChannel channel); + public void UpdatePermissions(); } } diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs index f97a908b292d..7f9ecebdcdf7 100644 --- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs +++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs @@ -61,6 +61,7 @@ public sealed class ChatUIController : UIController [UISystemDependency] private readonly ExamineSystem? _examine = default; [UISystemDependency] private readonly GhostSystem? _ghost = default; + [UISystemDependency] private readonly CollectiveMindSystem? _collectiveMind = default!; [UISystemDependency] private readonly TypingIndicatorSystem? _typingIndicator = default; [UISystemDependency] private readonly ChatSystem? _chatSys = default; [UISystemDependency] private readonly TransformSystem? _transform = default; @@ -85,7 +86,8 @@ public sealed class ChatUIController : UIController {SharedChatSystem.EmotesAltPrefix, ChatSelectChannel.Emotes}, {SharedChatSystem.AdminPrefix, ChatSelectChannel.Admin}, {SharedChatSystem.RadioCommonPrefix, ChatSelectChannel.Radio}, - {SharedChatSystem.DeadPrefix, ChatSelectChannel.Dead} + {SharedChatSystem.DeadPrefix, ChatSelectChannel.Dead}, + {SharedChatSystem.CollectiveMindPrefix, ChatSelectChannel.CollectiveMind} }; public static readonly Dictionary ChannelPrefixes = new() @@ -557,9 +559,17 @@ private void UpdateChannelPermissions() FilterableChannels |= ChatChannel.Admin; FilterableChannels |= ChatChannel.AdminAlert; FilterableChannels |= ChatChannel.AdminChat; + FilterableChannels |= ChatChannel.CollectiveMind; CanSendChannels |= ChatSelectChannel.Admin; } + // collective mind + if (_collectiveMind != null && _collectiveMind.IsCollectiveMind) + { + FilterableChannels |= ChatChannel.CollectiveMind; + CanSendChannels |= ChatSelectChannel.CollectiveMind; + } + SelectableChannels = CanSendChannels; // Necessary so that we always have a channel to fall back to. diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs index df4f56cb27ce..a6f2677c066d 100644 --- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs +++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs @@ -16,6 +16,7 @@ public sealed partial class ChannelFilterPopup : Popup ChatChannel.Whisper, ChatChannel.Emotes, ChatChannel.Radio, + ChatChannel.CollectiveMind, ChatChannel.Notifications, ChatChannel.LOOC, ChatChannel.OOC, diff --git a/Content.Server/Chat/Commands/CollectiveMindCommand.cs b/Content.Server/Chat/Commands/CollectiveMindCommand.cs new file mode 100644 index 000000000000..fc7af9531f95 --- /dev/null +++ b/Content.Server/Chat/Commands/CollectiveMindCommand.cs @@ -0,0 +1,43 @@ +using Content.Server.Chat.Systems; +using Content.Shared.Administration; +using Robust.Shared.Player; +using Robust.Shared.Console; +using Robust.Shared.Enums; + +namespace Content.Server.Chat.Commands +{ + [AnyCommand] + internal sealed class CollectiveMindCommand : IConsoleCommand + { + public string Command => "cmsay"; + public string Description => "Send chat messages to the collective mind."; + public string Help => "cmsay "; + + public void Execute(IConsoleShell shell, string argStr, string[] args) + { + if (shell.Player is not ICommonSession player) + { + shell.WriteError("This command cannot be run from the server."); + return; + } + + if (player.Status != SessionStatus.InGame) + return; + + if (player.AttachedEntity is not {} playerEntity) + { + shell.WriteError("You don't have an entity!"); + return; + } + + if (args.Length < 1) + return; + + var message = string.Join(" ", args).Trim(); + if (string.IsNullOrEmpty(message)) + return; + + EntitySystem.Get().TrySendInGameICMessage(playerEntity, message, InGameICChatType.CollectiveMind, ChatTransmitRange.Normal); + } + } +} diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index e81ad7978851..e87f82b4b1cf 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -1,3 +1,4 @@ +using System.Collections.Immutable; using System.Globalization; using System.Linq; using System.Text; @@ -15,6 +16,7 @@ using Content.Shared.Administration; using Content.Shared.CCVar; using Content.Shared.Chat; +using Content.Shared.CollectiveMind; using Content.Shared.Database; using Content.Shared.Examine; using Content.Shared.Ghost; @@ -256,6 +258,9 @@ public void TrySendInGameICMessage( case InGameICChatType.Emote: SendEntityEmote(source, message, range, nameOverride, hideLog: hideLog, ignoreActionBlocker: ignoreActionBlocker); break; + case InGameICChatType.CollectiveMind: + SendCollectiveMindChat(source, message, false); + break; } } @@ -428,6 +433,55 @@ public void DispatchStationAnnouncement( #region Private API + public void SendCollectiveMindChat(EntityUid source, string message, bool hideChat) + { + if (!TryComp(source, out var sourseCollectiveMindComp) || !_prototypeManager.TryIndex(sourseCollectiveMindComp.Channel, out var radioChannelProto)) + return; + + var clients = Filter.Empty(); + var mindQuery = EntityQueryEnumerator(); + while (mindQuery.MoveNext(out var uid, out var collectMindComp, out var actorComp)) + { + if (collectMindComp.Channel == sourseCollectiveMindComp.Channel) + { + clients.AddPlayer(actorComp.PlayerSession); + } + } + + var admins = _adminManager.ActiveAdmins.Select(p => p.Channel); + string messageWrap; + string adminMessageWrap; + + messageWrap = Loc.GetString("chat-manager-send-collective-mind-chat-wrap-message", + ("message", message), + ("channel", sourseCollectiveMindComp.Channel)); + + adminMessageWrap = Loc.GetString("chat-manager-send-collective-mind-chat-wrap-message-admin", + ("source", source), + ("message", message), + ("channel", sourseCollectiveMindComp.Channel)); + + _adminLogger.Add(LogType.Chat, LogImpact.Low, $"CollectiveMind chat from {ToPrettyString(source):Player}: {message}"); + + _chatManager.ChatMessageToManyFiltered(clients, + ChatChannel.CollectiveMind, + message, + messageWrap, + source, + hideChat, + true, + radioChannelProto.Color); + + _chatManager.ChatMessageToMany(ChatChannel.CollectiveMind, + message, + adminMessageWrap, + source, + hideChat, + true, + admins, + radioChannelProto.Color); + } + private void SendEntitySpeak( EntityUid source, string originalMessage, @@ -989,7 +1043,8 @@ public enum InGameICChatType : byte { Speak, Emote, - Whisper + Whisper, + CollectiveMind } /// diff --git a/Content.Shared/Chat/ChatChannel.cs b/Content.Shared/Chat/ChatChannel.cs index e8715a6ecb04..578f4c9fb746 100644 --- a/Content.Shared/Chat/ChatChannel.cs +++ b/Content.Shared/Chat/ChatChannel.cs @@ -79,16 +79,21 @@ public enum ChatChannel : ushort /// Admin chat /// AdminChat = 1 << 13, + + /// + /// Collective mind channel for entities who have comp. + /// + CollectiveMind = 1 << 14, /// /// Unspecified. /// - Unspecified = 1 << 14, + Unspecified = 1 << 15, /// /// Channels considered to be IC. /// - IC = Local | Whisper | Radio | Dead | Emotes | Damage | Visual | Notifications, + IC = Local | Whisper | Radio | Dead | Emotes | Damage | Visual | CollectiveMind | Notifications, AdminRelated = Admin | AdminAlert | AdminChat, } diff --git a/Content.Shared/Chat/ChatSelectChannel.cs b/Content.Shared/Chat/ChatSelectChannel.cs index c18bb9b8ee31..81d23e0d61b3 100644 --- a/Content.Shared/Chat/ChatSelectChannel.cs +++ b/Content.Shared/Chat/ChatSelectChannel.cs @@ -41,6 +41,11 @@ public enum ChatSelectChannel : ushort /// Emotes = ChatChannel.Emotes, + /// + /// CollectiveMind + /// + CollectiveMind = ChatChannel.CollectiveMind, + /// /// Deadchat /// diff --git a/Content.Shared/Chat/SharedChatSystem.cs b/Content.Shared/Chat/SharedChatSystem.cs index e5f3d4699747..dc2400ea0803 100644 --- a/Content.Shared/Chat/SharedChatSystem.cs +++ b/Content.Shared/Chat/SharedChatSystem.cs @@ -22,6 +22,8 @@ public abstract class SharedChatSystem : EntitySystem public const char EmotesAltPrefix = '*'; public const char AdminPrefix = ']'; public const char WhisperPrefix = ','; + public const char CollectiveMindPrefix = '+'; + public const char DefaultChannelKey = 'h'; [ValidatePrototypeId] diff --git a/Content.Shared/CollectiveMind/CollectiveMindComponent.cs b/Content.Shared/CollectiveMind/CollectiveMindComponent.cs new file mode 100644 index 000000000000..c068838595d4 --- /dev/null +++ b/Content.Shared/CollectiveMind/CollectiveMindComponent.cs @@ -0,0 +1,13 @@ +using Content.Shared.Radio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.CollectiveMind +{ + [RegisterComponent, NetworkedComponent] + public sealed partial class CollectiveMindComponent : Component + { + [DataField("channel", required: true)] + public ProtoId Channel; + } +} diff --git a/Resources/Locale/en-US/_Starlight/abductor/abductor-ui.ftl b/Resources/Locale/en-US/_Starlight/abductor/abductor-ui.ftl index 3c4de8260894..0c385e657ec1 100644 --- a/Resources/Locale/en-US/_Starlight/abductor/abductor-ui.ftl +++ b/Resources/Locale/en-US/_Starlight/abductor/abductor-ui.ftl @@ -27,4 +27,4 @@ objective-condition-abduct-description = (use the Gizmo on a subdued victim, the abductor-role-greeting = I am a professional combat scientist of a high-tech race. My task is to abduct humans, conduct experiments on them, and return them intact for the purity of the experiment. It is not in my interest to destroy the station, kill, or assist the crew. -roles-antag-abductor-objective = Find the nuke disk and blow up the station. +roles-antag-abductor-objective = Kidnap station crew and perform your experiments on them! diff --git a/Resources/Locale/en-US/chat/managers/chat-manager.ftl b/Resources/Locale/en-US/chat/managers/chat-manager.ftl index 315a793dec27..baf1371407bd 100644 --- a/Resources/Locale/en-US/chat/managers/chat-manager.ftl +++ b/Resources/Locale/en-US/chat/managers/chat-manager.ftl @@ -48,6 +48,10 @@ chat-manager-send-hook-ooc-wrap-message = OOC: [bold](D){$senderName}:[/bold] {$ chat-manager-dead-channel-name = DEAD chat-manager-admin-channel-name = ADMIN +chat-manager-send-collective-mind-chat-wrap-message = {$channel} collective mind: {$message} +chat-manager-send-collective-mind-chat-wrap-message-admin = {$source} ({$channel} collective mind): {$message} +chat-manager-collective-mind-channel-name = collective mind + chat-manager-rate-limited = You are sending messages too quickly! chat-manager-rate-limit-admin-announcement = Player { $player } breached chat rate limits. Watch them if this is a regular occurence. diff --git a/Resources/Locale/en-US/chat/ui/chat-box.ftl b/Resources/Locale/en-US/chat/ui/chat-box.ftl index 720f0d15ab45..f112a50a4296 100644 --- a/Resources/Locale/en-US/chat/ui/chat-box.ftl +++ b/Resources/Locale/en-US/chat/ui/chat-box.ftl @@ -13,6 +13,7 @@ hud-chatbox-select-channel-Whisper = Whisper hud-chatbox-select-channel-LOOC = LOOC hud-chatbox-select-channel-OOC = OOC hud-chatbox-select-channel-Damage = Damage +hud-chatbox-select-channel-CollectiveMind = Collective Mind hud-chatbox-select-channel-Visual = Actions hud-chatbox-select-channel-Radio = Radio @@ -28,6 +29,7 @@ hud-chatbox-channel-OOC = OOC hud-chatbox-channel-Radio = Radio hud-chatbox-channel-Notifications = Notifications hud-chatbox-channel-Server = Server +hud-chatbox-channel-CollectiveMind = Collective Mind hud-chatbox-channel-Visual = Actions hud-chatbox-channel-Damage = Damage hud-chatbox-channel-Unspecified = Unspecified diff --git a/Resources/Prototypes/Entities/Mobs/Species/diona.yml b/Resources/Prototypes/Entities/Mobs/Species/diona.yml index fdb7ac1e954f..3e6d1b4b5e5a 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/diona.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/diona.yml @@ -5,6 +5,8 @@ id: BaseMobDiona abstract: true components: + - type: CollectiveMind + channel: Dionas - type: HumanoidAppearance species: Diona - type: Hunger diff --git a/Resources/Prototypes/radio_channels.yml b/Resources/Prototypes/radio_channels.yml index 9cb8bf4daad1..8f48bbfd6a47 100644 --- a/Resources/Prototypes/radio_channels.yml +++ b/Resources/Prototypes/radio_channels.yml @@ -95,3 +95,10 @@ color: "#f6ce64" # long range since otherwise it'd defeat the point of a handheld radio independent of telecomms longRange: true + +- type: radioChannel + id: Dionas + name: chat-radio-dionas + keycode: 'd' + color: "#025c0f" + longRange: true