From 75a559fa5546bfac0545567e89b69c467a8fe6d7 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Sun, 19 Feb 2023 06:27:56 +1300 Subject: [PATCH] Update radio prefix parsing (#13777) --- Content.Client/Chat/Managers/ChatManager.cs | 8 +- Content.Client/Chat/Managers/IChatManager.cs | 2 +- .../Radio/EntitySystems/HeadsetSystem.cs | 7 + .../Systems/Chat/ChatUIController.cs | 136 ++++++------ .../Chat/Controls/ChannelSelectorButton.cs | 12 +- .../Controls/ChannelSelectorItemButton.cs | 10 +- .../Chat/Controls/ChannelSelectorPopup.cs | 61 +----- .../Systems/Chat/Widgets/ChatBox.xaml.cs | 43 +--- .../Chat/Systems/ChatSystem.Radio.cs | 112 ---------- Content.Server/Chat/Systems/ChatSystem.cs | 23 +- .../Radio/Components/HeadsetComponent.cs | 71 ------- .../IntrinsicRadioTransmitterComponent.cs | 4 +- .../Components/RadioMicrophoneComponent.cs | 3 +- .../Radio/Components/RadioSpeakerComponent.cs | 3 +- .../Radio/EntitySystems/HeadsetSystem.cs | 192 +++-------------- .../StationEvents/Events/SolarFlare.cs | 1 + Content.Server/Tools/ToolSystem.cs | 12 +- Content.Shared/Chat/SharedChatSystem.cs | 128 ++++++++++++ .../Inventory/InventorySystem.Relay.cs | 2 + .../EncryptionKeyComponent.cs | 9 +- .../EncryptionKeyHolderComponent.cs | 56 +++++ .../Radio/Components/HeadsetComponent.cs | 18 ++ .../Radio/EncryptionChannelsChangedEvent.cs | 13 ++ Content.Shared/Radio/EncryptionKeySystem.cs | 55 ----- .../EntitySystems/EncryptionKeySystem.cs | 196 ++++++++++++++++++ .../EntitySystems/SharedHeadsetSystem.cs | 38 ++++ .../Radio/GetDefaultRadioChannelEvent.cs | 15 ++ .../Systems/SharedToolSystem.MultipleTool.cs | 27 +++ .../en-US/chat/managers/chat-manager.ftl | 3 +- .../en-US/headset/headset-component.ftl | 2 +- .../Entities/Clothing/Ears/headsets.yml | 2 +- .../Entities/Clothing/Ears/headsets_alt.yml | 1 + 32 files changed, 659 insertions(+), 606 deletions(-) create mode 100644 Content.Client/Radio/EntitySystems/HeadsetSystem.cs delete mode 100644 Content.Server/Chat/Systems/ChatSystem.Radio.cs delete mode 100644 Content.Server/Radio/Components/HeadsetComponent.cs rename Content.Shared/Radio/{ => Components}/EncryptionKeyComponent.cs (76%) create mode 100644 Content.Shared/Radio/Components/EncryptionKeyHolderComponent.cs create mode 100644 Content.Shared/Radio/Components/HeadsetComponent.cs create mode 100644 Content.Shared/Radio/EncryptionChannelsChangedEvent.cs delete mode 100644 Content.Shared/Radio/EncryptionKeySystem.cs create mode 100644 Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs create mode 100644 Content.Shared/Radio/EntitySystems/SharedHeadsetSystem.cs create mode 100644 Content.Shared/Radio/GetDefaultRadioChannelEvent.cs diff --git a/Content.Client/Chat/Managers/ChatManager.cs b/Content.Client/Chat/Managers/ChatManager.cs index 9b5209d9a3..67b5f5202f 100644 --- a/Content.Client/Chat/Managers/ChatManager.cs +++ b/Content.Client/Chat/Managers/ChatManager.cs @@ -21,14 +21,14 @@ namespace Content.Client.Chat.Managers _sawmill.Level = LogLevel.Info; } - public void SendMessage(ReadOnlyMemory text, ChatSelectChannel channel) + public void SendMessage(string text, ChatSelectChannel channel) { var str = text.ToString(); switch (channel) { case ChatSelectChannel.Console: // run locally - _consoleHost.ExecuteCommand(text.ToString()); + _consoleHost.ExecuteCommand(text); break; case ChatSelectChannel.LOOC: @@ -57,10 +57,8 @@ namespace Content.Client.Chat.Managers _sawmill.Warning("Tried to speak on deadchat without being ghost or admin."); break; + // TODO sepearate radio and say into separate commands. case ChatSelectChannel.Radio: - _consoleHost.ExecuteCommand($"say \";{CommandParsing.Escape(str)}\""); - break; - case ChatSelectChannel.Local: _consoleHost.ExecuteCommand($"say \"{CommandParsing.Escape(str)}\""); break; diff --git a/Content.Client/Chat/Managers/IChatManager.cs b/Content.Client/Chat/Managers/IChatManager.cs index fc11745b37..6464ca1019 100644 --- a/Content.Client/Chat/Managers/IChatManager.cs +++ b/Content.Client/Chat/Managers/IChatManager.cs @@ -6,6 +6,6 @@ namespace Content.Client.Chat.Managers { void Initialize(); - public void SendMessage(ReadOnlyMemory text, ChatSelectChannel channel); + public void SendMessage(string text, ChatSelectChannel channel); } } diff --git a/Content.Client/Radio/EntitySystems/HeadsetSystem.cs b/Content.Client/Radio/EntitySystems/HeadsetSystem.cs new file mode 100644 index 0000000000..2810dd8b0a --- /dev/null +++ b/Content.Client/Radio/EntitySystems/HeadsetSystem.cs @@ -0,0 +1,7 @@ +using Content.Shared.Radio.EntitySystems; + +namespace Content.Client.Radio.EntitySystems; + +public sealed class HeadsetSystem : SharedHeadsetSystem +{ +} diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs index 8116946787..33f1de0547 100644 --- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs +++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs @@ -13,6 +13,7 @@ using Content.Shared.CCVar; using Content.Shared.Chat; using Content.Shared.Examine; using Content.Shared.Input; +using Content.Shared.Radio; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.Player; @@ -45,30 +46,21 @@ public sealed class ChatUIController : UIController [UISystemDependency] private readonly ExamineSystem? _examine = default; [UISystemDependency] private readonly GhostSystem? _ghost = default; [UISystemDependency] private readonly TypingIndicatorSystem? _typingIndicator = default; + [UISystemDependency] private readonly ChatSystem? _chatSys = default; private ISawmill _sawmill = default!; - public const char AliasLocal = '.'; - public const char AliasConsole = '/'; - public const char AliasDead = '\\'; - public const char AliasLOOC = '('; - public const char AliasOOC = '['; - public const char AliasEmotes = '@'; - public const char AliasAdmin = ']'; - public const char AliasRadio = ';'; - public const char AliasWhisper = ','; - public static readonly Dictionary PrefixToChannel = new() { - {AliasLocal, ChatSelectChannel.Local}, - {AliasWhisper, ChatSelectChannel.Whisper}, - {AliasConsole, ChatSelectChannel.Console}, - {AliasLOOC, ChatSelectChannel.LOOC}, - {AliasOOC, ChatSelectChannel.OOC}, - {AliasEmotes, ChatSelectChannel.Emotes}, - {AliasAdmin, ChatSelectChannel.Admin}, - {AliasRadio, ChatSelectChannel.Radio}, - {AliasDead, ChatSelectChannel.Dead} + {SharedChatSystem.LocalPrefix, ChatSelectChannel.Local}, + {SharedChatSystem.WhisperPrefix, ChatSelectChannel.Whisper}, + {SharedChatSystem.ConsolePrefix, ChatSelectChannel.Console}, + {SharedChatSystem.LOOCPrefix, ChatSelectChannel.LOOC}, + {SharedChatSystem.OOCPrefix, ChatSelectChannel.OOC}, + {SharedChatSystem.EmotesPrefix, ChatSelectChannel.Emotes}, + {SharedChatSystem.AdminPrefix, ChatSelectChannel.Admin}, + {SharedChatSystem.RadioCommonPrefix, ChatSelectChannel.Radio}, + {SharedChatSystem.DeadPrefix, ChatSelectChannel.Dead} }; public static readonly Dictionary ChannelPrefixes = @@ -369,11 +361,6 @@ public sealed class ChatUIController : UIController } } - public static string GetChannelSelectorName(ChatSelectChannel channelSelector) - { - return channelSelector.ToString(); - } - private void UpdateChannelPermissions() { CanSendChannels = default; @@ -592,50 +579,88 @@ public sealed class ChatUIController : UIController return channel; } - public (ChatSelectChannel channel, ReadOnlyMemory text) SplitInputContents(string inputText) + private bool TryGetRadioChannel(string text, out RadioChannelPrototype? radioChannel) { - var text = inputText.AsMemory().Trim(); - if (text.Length == 0) - return default; + radioChannel = null; + return _player.LocalPlayer?.ControlledEntity is EntityUid { Valid: true } uid + && _chatSys != null + && _chatSys.TryProccessRadioMessage(uid, text, out _, out radioChannel, quiet: true); + } - var prefixChar = text.Span[0]; - var channel = PrefixToChannel.GetValueOrDefault(prefixChar); + public void UpdateSelectedChannel(ChatBox box) + { + var (prefixChannel, _, radioChannel) = SplitInputContents(box.ChatInput.Input.Text); - if ((CanSendChannels & channel) != 0) - // Cut off prefix if it's valid and we can use the channel in question. - text = text[1..]; + if (prefixChannel == ChatSelectChannel.None) + box.ChatInput.ChannelSelector.UpdateChannelSelectButton(box.SelectedChannel, null); else - channel = 0; + box.ChatInput.ChannelSelector.UpdateChannelSelectButton(prefixChannel, radioChannel); + } - channel = MapLocalIfGhost(channel); + public (ChatSelectChannel chatChannel, string text, RadioChannelPrototype? radioChannel) SplitInputContents(string text) + { + text = text.Trim(); + if (text.Length == 0) + return (ChatSelectChannel.None, text, null); - // Trim from start again to cut out any whitespace between the prefix and message, if any. - return (channel, text.TrimStart()); + // We only cut off prefix only if it is not a radio or local channel, which both map to the same /say command + // because ???????? + + ChatSelectChannel chatChannel; + if (TryGetRadioChannel(text, out var radioChannel)) + chatChannel = ChatSelectChannel.Radio; + else + chatChannel = PrefixToChannel.GetValueOrDefault(text[0]); + + if ((CanSendChannels & chatChannel) == 0) + return (ChatSelectChannel.None, text, null); + + if (chatChannel == ChatSelectChannel.Radio) + return (chatChannel, text, radioChannel); + + if (chatChannel == ChatSelectChannel.Local) + { + if (_ghost?.IsGhost != true) + return (chatChannel, text, null); + else + chatChannel = ChatSelectChannel.Dead; + } + + return (chatChannel, text[1..].TrimStart(), null); } public void SendMessage(ChatBox box, ChatSelectChannel channel) { _typingIndicator?.ClientSubmittedChatText(); - if (!string.IsNullOrWhiteSpace(box.ChatInput.Input.Text)) + var text = box.ChatInput.Input.Text; + box.ChatInput.Input.Clear(); + box.ChatInput.Input.ReleaseKeyboardFocus(); + UpdateSelectedChannel(box); + + if (string.IsNullOrWhiteSpace(text)) + return; + + (var prefixChannel, text, var _) = SplitInputContents(text); + + // Check if message is longer than the character limit + if (text.Length > MaxMessageLength) { - var (prefixChannel, text) = SplitInputContents(box.ChatInput.Input.Text); - - // Check if message is longer than the character limit - if (text.Length > MaxMessageLength) - { - var locWarning = Loc.GetString("chat-manager-max-message-length", - ("maxMessageLength", MaxMessageLength)); - box.AddLine(locWarning, Color.Orange); - return; - } - - _manager.SendMessage(text, prefixChannel == 0 ? channel : prefixChannel); + var locWarning = Loc.GetString("chat-manager-max-message-length", + ("maxMessageLength", MaxMessageLength)); + box.AddLine(locWarning, Color.Orange); + return; } - box.ChatInput.Input.Clear(); - box.UpdateSelectedChannel(); - box.ChatInput.Input.ReleaseKeyboardFocus(); + if (prefixChannel != ChatSelectChannel.None) + channel = prefixChannel; + else if (channel == ChatSelectChannel.Radio) + { + // radio must have prefix as it goes through the say command. + text = $";{text}"; + } + + _manager.SendMessage(text, prefixChannel == 0 ? channel : prefixChannel); } private void OnChatMessage(MsgChatMessage message) => ProcessChatMessage(message.Message); @@ -687,11 +712,6 @@ public sealed class ChatUIController : UIController } } - public char GetPrefixFromChannel(ChatSelectChannel channel) - { - return ChannelPrefixes.GetValueOrDefault(channel); - } - public void RegisterChat(ChatBox chat) { _chats.Add(chat); diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs index daf0d95ec4..e594b04d9a 100644 --- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs +++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs @@ -1,4 +1,4 @@ -using Content.Shared.Chat; +using Content.Shared.Chat; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.Input; @@ -64,12 +64,10 @@ public sealed class ChannelSelectorButton : Button if (SelectedChannel == channel) return; SelectedChannel = channel; - UpdateChannelSelectButton(channel); - OnChannelSelect?.Invoke(channel); } - public string ChannelSelectorName(ChatSelectChannel channel) + public static string ChannelSelectorName(ChatSelectChannel channel) { return Loc.GetString($"hud-chatbox-select-channel-{channel}"); } @@ -87,10 +85,10 @@ public sealed class ChannelSelectorButton : Button }; } - public void UpdateChannelSelectButton(ChatSelectChannel channel) + public void UpdateChannelSelectButton(ChatSelectChannel channel, Shared.Radio.RadioChannelPrototype? radio) { - Text = ChannelSelectorName(channel); - Modulate = ChannelSelectColor(channel); + Text = radio != null ? Loc.GetString(radio.Name) : ChannelSelectorName(channel); + Modulate = radio?.Color ?? ChannelSelectColor(channel); } private void OnSelectorButtonToggled(ButtonToggledEventArgs args) diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorItemButton.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorItemButton.cs index fd25b2a619..bf37a412e8 100644 --- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorItemButton.cs +++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorItemButton.cs @@ -1,4 +1,4 @@ -using Content.Client.Stylesheets; +using Content.Client.Stylesheets; using Content.Shared.Chat; using Robust.Client.UserInterface.Controls; @@ -14,8 +14,12 @@ public sealed class ChannelSelectorItemButton : Button { Channel = selector; AddStyleClass(StyleNano.StyleClassChatChannelSelectorButton); - Text = ChatUIController.GetChannelSelectorName(selector); + + Text = ChannelSelectorButton.ChannelSelectorName(selector); + var prefix = ChatUIController.ChannelPrefixes[selector]; - if (prefix != default) Text = Loc.GetString("hud-chatbox-select-name-prefixed", ("name", Text), ("prefix", prefix)); + + if (prefix != default) + Text = Loc.GetString("hud-chatbox-select-name-prefixed", ("name", Text), ("prefix", prefix)); } } diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorPopup.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorPopup.cs index b17b9d4633..0852c10bb9 100644 --- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorPopup.cs +++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorPopup.cs @@ -1,4 +1,4 @@ -using Content.Shared.Chat; +using Content.Shared.Chat; using Robust.Client.UserInterface.Controls; using static Robust.Client.UserInterface.Controls.BaseButton; @@ -56,65 +56,6 @@ public sealed class ChannelSelectorPopup : Popup } } - /*public ChatSelectChannel NextChannel() - { - var nextChannel = ChatUIController.GetNextChannelSelector(_activeSelector); - var index = 0; - while (_selectorStates[(int)nextChannel].IsHidden && index <= _selectorStates.Count) - { - nextChannel = ChatUIController.GetNextChannelSelector(nextChannel); - index++; - } - _activeSelector = nextChannel; - return nextChannel; - } - - - private void SetupChannels(ChatUIController.ChannelSelectorSetup[] selectorData) - { - _channelSelectorHBox.DisposeAllChildren(); //cleanup old toggles - _selectorStates.Clear(); - foreach (var channelSelectorData in selectorData) - { - var newSelectorButton = new ChannelSelectorItemButton(channelSelectorData); - _selectorStates.Add(newSelectorButton); - if (!newSelectorButton.IsHidden) - { - _channelSelectorHBox.AddChild(newSelectorButton); - } - newSelectorButton.OnPressed += OnSelectorPressed; - } - } - - private void OnSelectorPressed(BaseButton.ButtonEventArgs args) - { - if (_selectorButton == null) return; - _selectorButton.SelectedChannel = ((ChannelSelectorItemButton) args.Button).Channel; - } - - public void HideChannels(params ChatChannel[] channels) - { - foreach (var channel in channels) - { - if (!ChatUIController.ChannelToSelector.TryGetValue(channel, out var selector)) continue; - var selectorbutton = _selectorStates[(int)selector]; - if (!selectorbutton.IsHidden) - { - _channelSelectorHBox.RemoveChild(selectorbutton); - if (_activeSelector != selector) continue; // do nothing - if (_channelSelectorHBox.Children.First() is ChannelSelectorItemButton button) - { - _activeSelector = button.Channel; - } - else - { - _activeSelector = ChatSelectChannel.None; - } - } - } - } - */ - private bool IsPreferredAvailable() { var preferred = _chatUIController.MapLocalIfGhost(_chatUIController.GetPreferredChannel()); diff --git a/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs b/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs index da81066e0b..cc5bd2bf67 100644 --- a/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs +++ b/Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs @@ -1,4 +1,4 @@ -using Content.Client.Chat; +using Content.Client.Chat; using Content.Client.Chat.TypingIndicator; using Content.Client.UserInterface.Systems.Chat.Controls; using Content.Shared.Chat; @@ -11,6 +11,7 @@ using Robust.Shared.Audio; using Robust.Shared.Input; using Robust.Shared.Player; using Robust.Shared.Utility; +using TerraFX.Interop.Windows; using static Robust.Client.UserInterface.Controls.LineEdit; namespace Content.Client.UserInterface.Systems.Chat.Widgets; @@ -68,7 +69,7 @@ public partial class ChatBox : UIWidget private void OnChannelSelect(ChatSelectChannel channel) { - UpdateSelectedChannel(); + _controller.UpdateSelectedChannel(this); } public void Repopulate() @@ -105,49 +106,13 @@ public partial class ChatBox : UIWidget Contents.AddMessage(formatted); } - public void UpdateSelectedChannel() - { - var (prefixChannel, _) = _controller.SplitInputContents(ChatInput.Input.Text); - var channel = prefixChannel == 0 ? SelectedChannel : prefixChannel; - - ChatInput.ChannelSelector.UpdateChannelSelectButton(channel); - } - public void Focus(ChatSelectChannel? channel = null) { var input = ChatInput.Input; var selectStart = Index.End; if (channel != null) - { - channel = _controller.MapLocalIfGhost(channel.Value); - - // Channel not selectable, just do NOTHING (not even focus). - if ((_controller.SelectableChannels & channel.Value) == 0) - return; - - var (_, text) = _controller.SplitInputContents(input.Text); - - var newPrefix = _controller.GetPrefixFromChannel(channel.Value); - DebugTools.Assert(newPrefix != default, "Focus channel must have prefix!"); - - if (channel == SelectedChannel) - { - // New selected channel is just the selected channel, - // just remove prefix (if any) and leave text unchanged. - - input.Text = text.ToString(); - selectStart = Index.Start; - } - else - { - // Change prefix to new focused channel prefix and leave text unchanged. - input.Text = string.Concat(newPrefix.ToString(), " ", text.Span); - selectStart = Index.FromStart(2); - } - ChatInput.ChannelSelector.Select(channel.Value); - } input.IgnoreNext = true; input.GrabKeyboardFocus(); @@ -205,7 +170,7 @@ public partial class ChatBox : UIWidget private void OnTextChanged(LineEditEventArgs args) { // Update channel select button to correct channel if we have a prefix. - UpdateSelectedChannel(); + _controller.UpdateSelectedChannel(this); // Warn typing indicator about change _controller.NotifyChatTextChange(); diff --git a/Content.Server/Chat/Systems/ChatSystem.Radio.cs b/Content.Server/Chat/Systems/ChatSystem.Radio.cs deleted file mode 100644 index 1a18ac4bb5..0000000000 --- a/Content.Server/Chat/Systems/ChatSystem.Radio.cs +++ /dev/null @@ -1,112 +0,0 @@ -using System.Linq; -using System.Text.RegularExpressions; -using Content.Server.Radio.Components; -using Content.Shared.Radio; -using Robust.Shared.Player; -using Robust.Shared.Prototypes; - -namespace Content.Server.Chat.Systems; - -public sealed partial class ChatSystem -{ - /// - /// Cache of the keycodes for faster lookup. - /// - private Dictionary _keyCodes = new(); - - private void InitializeRadio() - { - _prototypeManager.PrototypesReloaded += OnPrototypeReload; - CacheRadios(); - } - - private void OnPrototypeReload(PrototypesReloadedEventArgs obj) - { - CacheRadios(); - } - - private void CacheRadios() - { - _keyCodes.Clear(); - - foreach (var proto in _prototypeManager.EnumeratePrototypes()) - { - _keyCodes.Add(proto.KeyCode, proto); - } - - - } - - private void ShutdownRadio() - { - _prototypeManager.PrototypesReloaded -= OnPrototypeReload; - } - - private (string, RadioChannelPrototype?) GetRadioPrefix(EntityUid source, string message) - { - // TODO: Turn common into a true frequency and support multiple aliases. - var isRadioMessage = false; - RadioChannelPrototype? channel = null; - - // Check if have headset and grab headset UID for later - var hasHeadset = _inventory.TryGetSlotEntity(source, "ears", out var entityUid) & TryComp(entityUid, out var _headsetComponent); - - // First check if this is a message to the base radio frequency - if (message.StartsWith(';')) - { - // First Remove semicolon - channel = _prototypeManager.Index("Common"); - message = message[1..].TrimStart(); - isRadioMessage = true; - } - - - // Check now if the remaining message is a radio message - if ((message.StartsWith(':') || message.StartsWith('.')) && message.Length >= 2) - { - // Redirect to defaultChannel of headsetComp if it goes to "h" channel code after making sure defaultChannel exists - if (message[1] == 'h' - && _headsetComponent != null - && _headsetComponent.DefaultChannel != null - && _prototypeManager.TryIndex(_headsetComponent.DefaultChannel, out RadioChannelPrototype? protoDefaultChannel)) - { - // Set Channel to headset defaultChannel - channel = protoDefaultChannel; - } - else // otherwise it's a normal, targeted channel keycode - { - _keyCodes.TryGetValue(message[1], out channel); - } - - // Strip remaining message prefix. - message = message[2..].TrimStart(); - isRadioMessage = true; - } - - // If not a radio message at all - if (!isRadioMessage) return (message, null); - - // Special case for empty messages - if (message.Length <= 1) - return (string.Empty, null); - - // Check for headset before no-such-channel, otherwise you can get two PopupEntities if no headset and no channel - if (hasHeadset & channel == null ) - { - _popup.PopupEntity(Loc.GetString("chat-manager-no-such-channel"), source, source); - channel = null; - } - - // Re-capitalize message since we removed the prefix. - message = SanitizeMessageCapital(message); - - - - if (!hasHeadset && !HasComp(source)) - { - _popup.PopupEntity(Loc.GetString("chat-manager-no-headset-on-message"), source, source); - } - - return (message, channel); - } -} diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index a8f875bf4a..f180cf045a 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -65,7 +65,6 @@ public sealed partial class ChatSystem : SharedChatSystem public override void Initialize() { base.Initialize(); - InitializeRadio(); InitializeEmotes(); _configurationManager.OnValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged, true); _configurationManager.OnValueChanged(CCVars.DeadLoocEnabled, OnDeadLoocEnabledChanged, true); @@ -76,7 +75,6 @@ public sealed partial class ChatSystem : SharedChatSystem public override void Shutdown() { base.Shutdown(); - ShutdownRadio(); ShutdownEmotes(); _configurationManager.UnsubValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged); } @@ -140,6 +138,13 @@ public sealed partial class ChatSystem : SharedChatSystem if (!CanSendInGame(message, shell, player)) return; + if (desiredType == InGameICChatType.Speak && message.StartsWith(LocalPrefix)) + { + // prevent radios and remove prefix. + checkRadioPrefix = false; + message = message[1..]; + } + hideGlobalGhostChat |= hideChat; bool shouldCapitalize = (desiredType != InGameICChatType.Emote); bool shouldPunctuate = _configurationManager.GetCVar(CCVars.ChatPunctuation); @@ -159,10 +164,9 @@ public sealed partial class ChatSystem : SharedChatSystem // This message may have a radio prefix, and should then be whispered to the resolved radio channel if (checkRadioPrefix) { - var (radioMessage, channel) = GetRadioPrefix(source, message); - if (channel != null) + if (TryProccessRadioMessage(source, message, out var modMessage, out var channel)) { - SendEntityWhisper(source, radioMessage, hideChat, hideGlobalGhostChat, channel, nameOverride); + SendEntityWhisper(source, modMessage, hideChat, hideGlobalGhostChat, channel, nameOverride); return; } } @@ -544,15 +548,6 @@ public sealed partial class ChatSystem : SharedChatSystem .Select(p => p.ConnectedClient); } - private string SanitizeMessageCapital(string message) - { - if (string.IsNullOrEmpty(message)) - return message; - // Capitalize first letter - message = message[0].ToString().ToUpper() + message.Remove(0, 1); - return message; - } - private string SanitizeMessagePeriod(string message) { if (string.IsNullOrEmpty(message)) diff --git a/Content.Server/Radio/Components/HeadsetComponent.cs b/Content.Server/Radio/Components/HeadsetComponent.cs deleted file mode 100644 index fb9ee9a247..0000000000 --- a/Content.Server/Radio/Components/HeadsetComponent.cs +++ /dev/null @@ -1,71 +0,0 @@ -using Content.Server.Radio.EntitySystems; -using Content.Shared.Inventory; -using Content.Shared.Radio; -using Content.Shared.Tools; -using Robust.Shared.Audio; -using Robust.Shared.Containers; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Server.Radio.Components; -/// -/// This component relays radio messages to the parent entity's chat when equipped. -/// -[RegisterComponent] -[Access(typeof(HeadsetSystem))] -public sealed class HeadsetComponent : Component -{ - /// - /// This variable indicates locked state of encryption keys, allowing or prohibiting inserting and removing of encryption keys from headset. - /// true => User are able to remove encryption keys with tool mentioned in KeysExtractionMethod, and put encryption keys in headset. - /// false => encryption keys are locked in headset, they can't be properly removed or added. - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("isKeysUnlocked")] - public bool IsKeysUnlocked = true; - /// - /// Shows which tool a person should use to extract the encryption keys from the headset. - /// default "Screwing" - /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField("keysExtractionMethod", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string KeysExtractionMethod = "Screwing"; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("keySlots")] - public int KeySlots = 2; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("keyExtractionSound")] - public SoundSpecifier KeyExtractionSound = new SoundPathSpecifier("/Audio/Items/pistol_magout.ogg"); - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("keyInsertionSound")] - public SoundSpecifier KeyInsertionSound = new SoundPathSpecifier("/Audio/Items/pistol_magin.ogg"); - - [ViewVariables] - public Container KeyContainer = default!; - public const string KeyContainerName = "key_slots"; - - [ViewVariables] - public HashSet Channels = new(); - - // Maybe make the defaultChannel an actual channel type some day, and use that for parsing messages - // [DataField("defaultChannel", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))] - // public readonly HashSet defaultChannel = new(); - - /// - /// This variable defines what channel will be used with using ":h" (department channel prefix). - /// Headset read DefaultChannel of first encryption key installed. - /// Do not change this variable from headset or VV, better change encryption keys and UpdateDefaultChannel. - /// - [ViewVariables(VVAccess.ReadOnly)] - public string? DefaultChannel; - - [DataField("enabled")] - public bool Enabled = true; - - public bool IsEquipped = false; - - [DataField("requiredSlot")] - public SlotFlags RequiredSlot = SlotFlags.EARS; -} diff --git a/Content.Server/Radio/Components/IntrinsicRadioTransmitterComponent.cs b/Content.Server/Radio/Components/IntrinsicRadioTransmitterComponent.cs index 2da6b90f0b..eb28bac4ec 100644 --- a/Content.Server/Radio/Components/IntrinsicRadioTransmitterComponent.cs +++ b/Content.Server/Radio/Components/IntrinsicRadioTransmitterComponent.cs @@ -1,3 +1,5 @@ +using Content.Server.Chat.Systems; +using Content.Shared.Chat; using Content.Shared.Radio; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; @@ -11,5 +13,5 @@ namespace Content.Server.Radio.Components; public sealed class IntrinsicRadioTransmitterComponent : Component { [DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))] - public readonly HashSet Channels = new() { "Common" }; + public readonly HashSet Channels = new() { SharedChatSystem.CommonChannel }; } diff --git a/Content.Server/Radio/Components/RadioMicrophoneComponent.cs b/Content.Server/Radio/Components/RadioMicrophoneComponent.cs index 86c750762b..5ab2484a9d 100644 --- a/Content.Server/Radio/Components/RadioMicrophoneComponent.cs +++ b/Content.Server/Radio/Components/RadioMicrophoneComponent.cs @@ -1,4 +1,5 @@ using Content.Server.Radio.EntitySystems; +using Content.Shared.Chat; using Content.Shared.Radio; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; @@ -14,7 +15,7 @@ public sealed class RadioMicrophoneComponent : Component { [ViewVariables(VVAccess.ReadWrite)] [DataField("broadcastChannel", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string BroadcastChannel = "Common"; + public string BroadcastChannel = SharedChatSystem.CommonChannel; [ViewVariables, DataField("supportedChannels", customTypeSerializer: typeof(PrototypeIdListSerializer))] public List? SupportedChannels; diff --git a/Content.Server/Radio/Components/RadioSpeakerComponent.cs b/Content.Server/Radio/Components/RadioSpeakerComponent.cs index e83c0aa0fb..22a7578eea 100644 --- a/Content.Server/Radio/Components/RadioSpeakerComponent.cs +++ b/Content.Server/Radio/Components/RadioSpeakerComponent.cs @@ -1,4 +1,5 @@ using Content.Server.Radio.EntitySystems; +using Content.Shared.Chat; using Content.Shared.Radio; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; @@ -12,7 +13,7 @@ namespace Content.Server.Radio.Components; public sealed class RadioSpeakerComponent : Component { [DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))] - public HashSet Channels = new () { "Common" }; + public HashSet Channels = new () { SharedChatSystem.CommonChannel }; [DataField("enabled")] public bool Enabled; diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index 2a66f985d3..87ec6448ae 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -1,74 +1,71 @@ using Content.Server.Chat.Systems; -using Content.Server.Popups; -using Content.Server.Tools; -using Content.Shared.Tools.Components; -using Content.Shared.Examine; -using Content.Shared.Interaction; +using Content.Server.Radio.Components; using Content.Shared.Inventory.Events; using Content.Shared.Radio; -using Content.Server.Radio.Components; +using Content.Shared.Radio.Components; +using Content.Shared.Radio.EntitySystems; using Robust.Server.GameObjects; -using Robust.Shared.Containers; using Robust.Shared.Network; -using Robust.Shared.Prototypes; -using System.Linq; namespace Content.Server.Radio.EntitySystems; -public sealed class HeadsetSystem : EntitySystem +public sealed class HeadsetSystem : SharedHeadsetSystem { - [Dependency] private readonly IPrototypeManager _protoManager = default!; [Dependency] private readonly INetManager _netMan = default!; [Dependency] private readonly RadioSystem _radio = default!; - [Dependency] private readonly ToolSystem _toolSystem = default!; - [Dependency] private readonly PopupSystem _popupSystem = default!; - [Dependency] private readonly SharedContainerSystem _container = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnHeadsetReceive); - SubscribeLocalEvent(OnGotEquipped); - SubscribeLocalEvent(OnGotUnequipped); - SubscribeLocalEvent(OnSpeak); + SubscribeLocalEvent(OnKeysChanged); - SubscribeLocalEvent(OnStartup); - SubscribeLocalEvent(OnInteractUsing); - SubscribeLocalEvent(OnContainerInserted); + SubscribeLocalEvent(OnSpeak); + } + + private void OnKeysChanged(EntityUid uid, HeadsetComponent component, EncryptionChannelsChangedEvent args) + { + UpdateRadioChannels(uid, component, args.Component); + } + + private void UpdateRadioChannels(EntityUid uid, HeadsetComponent headset, EncryptionKeyHolderComponent? keyHolder = null) + { + if (!headset.Enabled) + return; + + if (!Resolve(uid, ref keyHolder)) + return; + + if (keyHolder.Channels.Count == 0) + RemComp(uid); + else + EnsureComp(uid).Channels = new(keyHolder.Channels); } private void OnSpeak(EntityUid uid, WearingHeadsetComponent component, EntitySpokeEvent args) { if (args.Channel != null - && TryComp(component.Headset, out HeadsetComponent? headset) - && headset.Channels.Contains(args.Channel.ID)) + && TryComp(component.Headset, out EncryptionKeyHolderComponent? keys) + && keys.Channels.Contains(args.Channel.ID)) { _radio.SendRadioMessage(uid, args.Message, args.Channel, component.Headset); args.Channel = null; // prevent duplicate messages from other listeners. } } - private void OnGotEquipped(EntityUid uid, HeadsetComponent component, GotEquippedEvent args) + protected override void OnGotEquipped(EntityUid uid, HeadsetComponent component, GotEquippedEvent args) { - component.IsEquipped = args.SlotFlags.HasFlag(component.RequiredSlot); - + base.OnGotEquipped(uid, component, args); if (component.IsEquipped && component.Enabled) { EnsureComp(args.Equipee).Headset = uid; - UpdateRadioChannelsInActiveRadio(uid, component, EnsureComp(uid)); + UpdateRadioChannels(uid, component); } } - private void UpdateRadioChannelsInActiveRadio(EntityUid uid, HeadsetComponent component, ActiveRadioComponent activeRadio) - { - activeRadio.Channels.Clear(); - activeRadio.Channels.UnionWith(component.Channels); - } - - private void OnGotUnequipped(EntityUid uid, HeadsetComponent component, GotUnequippedEvent args) + protected override void OnGotUnequipped(EntityUid uid, HeadsetComponent component, GotUnequippedEvent args) { + base.OnGotUnequipped(uid, component, args); component.IsEquipped = false; RemComp(uid); RemComp(args.Equipee); @@ -92,7 +89,7 @@ public sealed class HeadsetSystem : EntitySystem else if (component.IsEquipped) { EnsureComp(Transform(uid).ParentUid).Headset = uid; - EnsureComp(uid).Channels.UnionWith(component.Channels); + UpdateRadioChannels(uid, component); } } @@ -101,127 +98,4 @@ public sealed class HeadsetSystem : EntitySystem if (TryComp(Transform(uid).ParentUid, out ActorComponent? actor)) _netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.ConnectedClient); } - - private void OnExamined(EntityUid uid, HeadsetComponent component, ExaminedEvent args) - { - if (!args.IsInDetailsRange) - return; - if (component.KeyContainer.ContainedEntities.Count == 0) - { - args.PushMarkup(Loc.GetString("examine-headset-no-keys")); - return; - } - else if (component.Channels.Count > 0) - { - args.PushMarkup(Loc.GetString("examine-headset-channels-prefix")); - EncryptionKeySystem.GetChannelsExamine(component.Channels, args, _protoManager, "examine-headset-channel"); - args.PushMarkup(Loc.GetString("examine-headset-chat-prefix", ("prefix", ":h"))); - if (component.DefaultChannel != null) - { - var proto = _protoManager.Index(component.DefaultChannel); - args.PushMarkup(Loc.GetString("examine-headset-default-channel", ("channel", component.DefaultChannel), ("color", proto.Color))); - } - } - } - - private void OnStartup(EntityUid uid, HeadsetComponent component, ComponentStartup args) - { - component.KeyContainer = _container.EnsureContainer(uid, HeadsetComponent.KeyContainerName); - } - - private bool InstallKey(HeadsetComponent component, EntityUid key, EncryptionKeyComponent keyComponent) - { - return component.KeyContainer.Insert(key); - } - - private void UploadChannelsFromKey(HeadsetComponent component, EncryptionKeyComponent key) - { - foreach (var j in key.Channels) - component.Channels.Add(j); - } - - public void RecalculateChannels(HeadsetComponent component) - { - component.Channels.Clear(); - foreach (EntityUid i in component.KeyContainer.ContainedEntities) - { - if (TryComp(i, out var key)) - { - UploadChannelsFromKey(component, key); - } - } - } - - private void OnInteractUsing(EntityUid uid, HeadsetComponent component, InteractUsingEvent args) - { - if (!TryComp(uid, out var storage)) - return; - if(!component.IsKeysUnlocked) - { - _popupSystem.PopupEntity(Loc.GetString("headset-encryption-keys-are-locked"), uid, args.User); - return; - } - if (TryComp(args.Used, out var key)) - { - if (component.KeySlots > component.KeyContainer.ContainedEntities.Count) - { - if (InstallKey(component, args.Used, key)) - { - _popupSystem.PopupEntity(Loc.GetString("headset-encryption-key-successfully-installed"), uid, args.User); - _audio.PlayPvs(component.KeyInsertionSound, args.Target); - } - } - else - { - _popupSystem.PopupEntity(Loc.GetString("headset-encryption-key-slots-already-full"), uid, args.User); - } - } - if (TryComp(args.Used, out var tool)) - { - if (component.KeyContainer.ContainedEntities.Count > 0) - { - if (_toolSystem.UseTool( - args.Used, args.User, uid, - 0f, 0f, new String[] { component.KeysExtractionMethod }, - doAfterCompleteEvent: null, toolComponent: tool) - ) - { - var contained = component.KeyContainer.ContainedEntities.ToArray(); - foreach (var i in contained) - component.KeyContainer.Remove(i); - component.Channels.Clear(); - UpdateDefaultChannel(component); - _popupSystem.PopupEntity(Loc.GetString("headset-encryption-keys-all-extracted"), uid, args.User); - _audio.PlayPvs(component.KeyExtractionSound, args.Target); - } - } - else - { - _popupSystem.PopupEntity(Loc.GetString("headset-encryption-keys-no-keys"), uid, args.User); - } - } - } - - private void UpdateDefaultChannel(HeadsetComponent component) - { - if (component.KeyContainer.ContainedEntities.Count >= 1) - component.DefaultChannel = EnsureComp(component.KeyContainer.ContainedEntities[0])?.DefaultChannel; - else - component.DefaultChannel = null; - } - - private void OnContainerInserted(EntityUid uid, HeadsetComponent component, EntInsertedIntoContainerMessage args) - { - if (args.Container.ID != HeadsetComponent.KeyContainerName) - { - return; - } - if (TryComp(args.Entity, out var added)) - { - UpdateDefaultChannel(component); - UploadChannelsFromKey(component, added); - UpdateRadioChannelsInActiveRadio(uid, component, EnsureComp(uid)); - } - return; - } } diff --git a/Content.Server/StationEvents/Events/SolarFlare.cs b/Content.Server/StationEvents/Events/SolarFlare.cs index 2b4d6d182a..2ae4be595e 100644 --- a/Content.Server/StationEvents/Events/SolarFlare.cs +++ b/Content.Server/StationEvents/Events/SolarFlare.cs @@ -4,6 +4,7 @@ using Content.Server.Radio; using Robust.Shared.Random; using Content.Server.Light.EntitySystems; using Content.Server.Light.Components; +using Content.Shared.Radio.Components; namespace Content.Server.StationEvents.Events; diff --git a/Content.Server/Tools/ToolSystem.cs b/Content.Server/Tools/ToolSystem.cs index 3ffdc3680f..0368c7bc29 100644 --- a/Content.Server/Tools/ToolSystem.cs +++ b/Content.Server/Tools/ToolSystem.cs @@ -101,7 +101,7 @@ namespace Content.Server.Tools /// the and being broadcast /// to see whether using the tool succeeded or not. If the is zero, /// this simply returns whether using the tool succeeded or not. - public bool UseTool( + public override bool UseTool( EntityUid tool, EntityUid user, EntityUid? target, @@ -148,16 +148,6 @@ namespace Content.Server.Tools return ToolFinishUse(tool, user, fuel, toolComponent); } - // This is hilariously long. - /// - public bool UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel, - float doAfterDelay, string toolQualityNeeded, object doAfterCompleteEvent, object doAfterCancelledEvent, EntityUid? doAfterEventTarget = null, - Func? doAfterCheck = null, ToolComponent? toolComponent = null) - { - return UseTool(tool, user, target, fuel, doAfterDelay, new[] { toolQualityNeeded }, - doAfterCompleteEvent, doAfterCancelledEvent, doAfterEventTarget, doAfterCheck, toolComponent); - } - /// /// Async version of UseTool. /// diff --git a/Content.Shared/Chat/SharedChatSystem.cs b/Content.Shared/Chat/SharedChatSystem.cs index 0d046e12b8..040cb3af20 100644 --- a/Content.Shared/Chat/SharedChatSystem.cs +++ b/Content.Shared/Chat/SharedChatSystem.cs @@ -1,5 +1,133 @@ +using Content.Shared.Popups; +using Content.Shared.Radio; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; + namespace Content.Shared.Chat; public abstract class SharedChatSystem : EntitySystem { + public const char RadioCommonPrefix = ';'; + public const char RadioChannelPrefix = ':'; + public const char LocalPrefix = '.'; + public const char ConsolePrefix = '/'; + public const char DeadPrefix = '\\'; + public const char LOOCPrefix = '('; + public const char OOCPrefix = '['; + public const char EmotesPrefix = '@'; + public const char AdminPrefix = ']'; + public const char WhisperPrefix = ','; + + public const char DefaultChannelKey = 'h'; + public const string CommonChannel = "Common"; + public static string DefaultChannelPrefix = $"{RadioChannelPrefix}{DefaultChannelKey}"; + + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + /// + /// Cache of the keycodes for faster lookup. + /// + private Dictionary _keyCodes = new(); + + public override void Initialize() + { + base.Initialize(); + DebugTools.Assert(_prototypeManager.HasIndex(CommonChannel)); + _prototypeManager.PrototypesReloaded += OnPrototypeReload; + CacheRadios(); + } + + private void OnPrototypeReload(PrototypesReloadedEventArgs obj) + { + if (obj.ByType.ContainsKey(typeof(RadioChannelPrototype))) + CacheRadios(); + } + + private void CacheRadios() + { + _keyCodes.Clear(); + + foreach (var proto in _prototypeManager.EnumeratePrototypes()) + { + _keyCodes.Add(proto.KeyCode, proto); + } + } + + public override void Shutdown() + { + _prototypeManager.PrototypesReloaded -= OnPrototypeReload; + } + + /// + /// Attempts to resolve radio prefixes in chat messages (e.g., remove a leading ":e" and resolve the requested + /// channel. Returns true if a radio message was attempted, even if the channel is invalid. + /// + /// Source of the message + /// The message to be modified + /// The modified message + /// The channel that was requested, if any + /// Whether or not to generate an informative pop-up message. + /// + public bool TryProccessRadioMessage( + EntityUid source, + string input, + out string output, + out RadioChannelPrototype? channel, + bool quiet = false) + { + output = input.Trim(); + channel = null; + + if (input.Length == 0) + return false; + + if (input.StartsWith(RadioCommonPrefix)) + { + output = SanitizeMessageCapital(input[1..].TrimStart()); + channel = _prototypeManager.Index(CommonChannel); + return true; + } + + if (!input.StartsWith(RadioChannelPrefix)) + return false; + + if (input.Length < 2 || char.IsWhiteSpace(input[1])) + { + output = SanitizeMessageCapital(input[1..].TrimStart()); + if (!quiet) + _popup.PopupEntity(Loc.GetString("chat-manager-no-radio-key"), source, source); + return true; + } + + var channelKey = input[1]; + output = SanitizeMessageCapital(input[2..].TrimStart()); + + if (channelKey == DefaultChannelKey) + { + var ev = new GetDefaultRadioChannelEvent(); + RaiseLocalEvent(source, ev); + + if (ev.Channel != null) + _prototypeManager.TryIndex(ev.Channel, out channel); + return true; + } + + if (!_keyCodes.TryGetValue(channelKey, out channel) && !quiet) + { + var msg = Loc.GetString("chat-manager-no-such-channel", ("key", channelKey)); + _popup.PopupEntity(msg, source, source); + } + + return true; + } + + public string SanitizeMessageCapital(string message) + { + if (string.IsNullOrEmpty(message)) + return message; + // Capitalize first letter + message = char.ToUpper(message[0]) + message.Remove(0, 1); + return message; + } } diff --git a/Content.Shared/Inventory/InventorySystem.Relay.cs b/Content.Shared/Inventory/InventorySystem.Relay.cs index d336e0f1b0..6b2c4c4257 100644 --- a/Content.Shared/Inventory/InventorySystem.Relay.cs +++ b/Content.Shared/Inventory/InventorySystem.Relay.cs @@ -3,6 +3,7 @@ using Content.Shared.Electrocution; using Content.Shared.Explosion; using Content.Shared.IdentityManagement.Components; using Content.Shared.Movement.Systems; +using Content.Shared.Radio; using Content.Shared.Slippery; using Content.Shared.Strip.Components; using Content.Shared.Temperature; @@ -21,6 +22,7 @@ public partial class InventorySystem SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); + SubscribeLocalEvent(RelayInventoryEvent); } protected void RelayInventoryEvent(EntityUid uid, InventoryComponent component, T args) where T : EntityEventArgs, IInventoryRelayEvent diff --git a/Content.Shared/Radio/EncryptionKeyComponent.cs b/Content.Shared/Radio/Components/EncryptionKeyComponent.cs similarity index 76% rename from Content.Shared/Radio/EncryptionKeyComponent.cs rename to Content.Shared/Radio/Components/EncryptionKeyComponent.cs index 9fcd649a40..5a7a54eb55 100644 --- a/Content.Shared/Radio/EncryptionKeyComponent.cs +++ b/Content.Shared/Radio/Components/EncryptionKeyComponent.cs @@ -1,8 +1,9 @@ -using Content.Shared.Radio; +using Content.Shared.Chat; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; -namespace Content.Server.Radio.Components; +namespace Content.Shared.Radio.Components; + /// /// This component is currently used for providing access to channels for "HeadsetComponent"s. /// It should be used for intercoms and other radios in future. @@ -13,10 +14,8 @@ public sealed class EncryptionKeyComponent : Component [DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))] public HashSet Channels = new(); - /// - /// This variable defines what channel will be used with using ":h" (department channel prefix). - /// Headset read DefaultChannel of first encryption key installed. + /// This is the channel that will be used when using the default/department prefix (). /// [DataField("defaultChannel", customTypeSerializer: typeof(PrototypeIdSerializer))] public readonly string? DefaultChannel; diff --git a/Content.Shared/Radio/Components/EncryptionKeyHolderComponent.cs b/Content.Shared/Radio/Components/EncryptionKeyHolderComponent.cs new file mode 100644 index 0000000000..ca5d1f647a --- /dev/null +++ b/Content.Shared/Radio/Components/EncryptionKeyHolderComponent.cs @@ -0,0 +1,56 @@ +using Content.Shared.Chat; +using Content.Shared.Tools; +using Robust.Shared.Audio; +using Robust.Shared.Containers; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Radio.Components; + +/// +/// This component is by entities that can contain encryption keys +/// +[RegisterComponent] +public sealed class EncryptionKeyHolderComponent : Component +{ + /// + /// Whether or not encryption keys can be removed from the headset. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("keysUnlocked")] + public bool KeysUnlocked = true; + + /// + /// The tool required to extract the encryption keys from the headset. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("keysExtractionMethod", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string KeysExtractionMethod = "Screwing"; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("keySlots")] + public int KeySlots = 2; + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("keyExtractionSound")] + public SoundSpecifier KeyExtractionSound = new SoundPathSpecifier("/Audio/Items/pistol_magout.ogg"); + + [ViewVariables(VVAccess.ReadWrite)] + [DataField("keyInsertionSound")] + public SoundSpecifier KeyInsertionSound = new SoundPathSpecifier("/Audio/Items/pistol_magin.ogg"); + + [ViewVariables] + public Container KeyContainer = default!; + public const string KeyContainerName = "key_slots"; + + /// + /// Combined set of radio channels provided by all contained keys. + /// + [ViewVariables] + public HashSet Channels = new(); + + /// + /// This is the channel that will be used when using the default/department prefix (). + /// + [ViewVariables] + public string? DefaultChannel; +} diff --git a/Content.Shared/Radio/Components/HeadsetComponent.cs b/Content.Shared/Radio/Components/HeadsetComponent.cs new file mode 100644 index 0000000000..4d3b0fd4aa --- /dev/null +++ b/Content.Shared/Radio/Components/HeadsetComponent.cs @@ -0,0 +1,18 @@ +using Content.Shared.Inventory; + +namespace Content.Shared.Radio.Components; + +/// +/// This component relays radio messages to the parent entity's chat when equipped. +/// +[RegisterComponent] +public sealed class HeadsetComponent : Component +{ + [DataField("enabled")] + public bool Enabled = true; + + public bool IsEquipped = false; + + [DataField("requiredSlot")] + public SlotFlags RequiredSlot = SlotFlags.EARS; +} diff --git a/Content.Shared/Radio/EncryptionChannelsChangedEvent.cs b/Content.Shared/Radio/EncryptionChannelsChangedEvent.cs new file mode 100644 index 0000000000..3841248a41 --- /dev/null +++ b/Content.Shared/Radio/EncryptionChannelsChangedEvent.cs @@ -0,0 +1,13 @@ +using Content.Shared.Radio.Components; + +namespace Content.Shared.Radio; + +public sealed class EncryptionChannelsChangedEvent : EntityEventArgs +{ + public readonly EncryptionKeyHolderComponent Component; + + public EncryptionChannelsChangedEvent(EncryptionKeyHolderComponent component) + { + Component = component; + } +} diff --git a/Content.Shared/Radio/EncryptionKeySystem.cs b/Content.Shared/Radio/EncryptionKeySystem.cs deleted file mode 100644 index 37bd5c7670..0000000000 --- a/Content.Shared/Radio/EncryptionKeySystem.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Content.Server.Radio.Components; -using Content.Shared.Examine; -using Content.Shared.Radio; -using Robust.Shared.Prototypes; - -namespace Content.Server.Radio.EntitySystems; - -public sealed class EncryptionKeySystem : EntitySystem -{ - [Dependency] private readonly IPrototypeManager _protoManager = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnExamined); - } - - private void OnExamined(EntityUid uid, EncryptionKeyComponent component, ExaminedEvent args) - { - if (!args.IsInDetailsRange) - return; - if(component.Channels.Count > 0) - { - args.PushMarkup(Loc.GetString("examine-encryption-key-channels-prefix")); - EncryptionKeySystem.GetChannelsExamine(component.Channels, args, _protoManager, "examine-headset-channel"); - if (component.DefaultChannel != null) - { - var proto = _protoManager.Index(component.DefaultChannel); - args.PushMarkup(Loc.GetString("examine-encryption-key-default-channel", ("channel", component.DefaultChannel), ("color", proto.Color))); - } - } - } - - /// - /// A static method for formating list of radio channels for examine events. - /// - /// HashSet of channels in headset, encryptionkey or etc. - /// IPrototypeManager for getting prototypes of channels with their variables. - /// String that provide id of pattern in .ftl files to format channel with variables of it. - public static void GetChannelsExamine(HashSet channels, ExaminedEvent examineEvent, IPrototypeManager protoManager, string channelFTLPattern) - { - foreach (var id in channels) - { - var proto = protoManager.Index(id); - string keyCode = "" + proto.KeyCode; - if (id != "Common") - keyCode = ":" + keyCode; - examineEvent.PushMarkup(Loc.GetString(channelFTLPattern, - ("color", proto.Color), - ("key", keyCode), - ("id", proto.LocalizedName), - ("freq", proto.Frequency))); - } - } -} diff --git a/Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs b/Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs new file mode 100644 index 0000000000..23d4ee5091 --- /dev/null +++ b/Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs @@ -0,0 +1,196 @@ +using System.Linq; +using Content.Shared.Chat; +using Content.Shared.Examine; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction; +using Content.Shared.Popups; +using Content.Shared.Radio.Components; +using Content.Shared.Tools; +using Content.Shared.Tools.Components; +using Robust.Shared.Containers; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Shared.Radio.EntitySystems; + +/// +/// This system manages encryption keys & key holders for use with radio channels. +/// +public sealed class EncryptionKeySystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _protoManager = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedToolSystem _toolSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnKeyExamined); + SubscribeLocalEvent(OnHolderExamined); + + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnInteractUsing); + SubscribeLocalEvent(OnContainerModified); + SubscribeLocalEvent(OnContainerModified); + } + + public void UpdateChannels(EntityUid uid, EncryptionKeyHolderComponent component) + { + if (!component.Initialized) + return; + + component.Channels.Clear(); + component.DefaultChannel = null; + + foreach (var ent in component.KeyContainer.ContainedEntities) + { + if (TryComp(ent, out var key)) + { + component.Channels.UnionWith(key.Channels); + component.DefaultChannel ??= key.DefaultChannel; + } + } + + RaiseLocalEvent(uid, new EncryptionChannelsChangedEvent(component)); + } + + private void OnContainerModified(EntityUid uid, EncryptionKeyHolderComponent component, ContainerModifiedMessage args) + { + if (args.Container.ID == EncryptionKeyHolderComponent.KeyContainerName) + UpdateChannels(uid, component); + } + + private void OnInteractUsing(EntityUid uid, EncryptionKeyHolderComponent component, InteractUsingEvent args) + { + if (!TryComp(uid, out var storage)) + return; + + if (TryComp(args.Used, out var key)) + { + args.Handled = true; + + if (!component.KeysUnlocked) + { + if (_timing.IsFirstTimePredicted) + _popupSystem.PopupEntity(Loc.GetString("headset-encryption-keys-are-locked"), uid, Filter.Local(), false); + return; + } + + if (component.KeySlots <= component.KeyContainer.ContainedEntities.Count) + { + if (_timing.IsFirstTimePredicted) + _popupSystem.PopupEntity(Loc.GetString("headset-encryption-key-slots-already-full"), uid, Filter.Local(), false); + return; + } + + if (component.KeyContainer.Insert(args.Used)) + { + if (_timing.IsFirstTimePredicted) + _popupSystem.PopupEntity(Loc.GetString("headset-encryption-key-successfully-installed"), uid, Filter.Local(), false); + _audio.PlayPredicted(component.KeyInsertionSound, args.Target, args.User); + return; + } + } + + if (!TryComp(args.Used, out var tool) || !tool.Qualities.Contains(component.KeysExtractionMethod)) + return; + + args.Handled = true; + + if (component.KeyContainer.ContainedEntities.Count == 0) + { + if (_timing.IsFirstTimePredicted) + _popupSystem.PopupEntity(Loc.GetString("headset-encryption-keys-no-keys"), uid, Filter.Local(), false); + return; + } + + if (!_toolSystem.UseTool(args.Used, args.User, uid, 0f, 0f, component.KeysExtractionMethod, toolComponent: tool)) + return; + + var contained = component.KeyContainer.ContainedEntities.ToArray(); + _container.EmptyContainer(component.KeyContainer, entMan: EntityManager); + foreach (var ent in contained) + { + _hands.PickupOrDrop(args.User, ent); + } + + // if tool use ever gets predicted this needs changing. + _popupSystem.PopupEntity(Loc.GetString("headset-encryption-keys-all-extracted"), uid, args.User); + _audio.PlayPvs(component.KeyExtractionSound, args.Target); + } + + private void OnStartup(EntityUid uid, EncryptionKeyHolderComponent component, ComponentStartup args) + { + component.KeyContainer = _container.EnsureContainer(uid, EncryptionKeyHolderComponent.KeyContainerName); + UpdateChannels(uid, component); + } + + private void OnHolderExamined(EntityUid uid, EncryptionKeyHolderComponent component, ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + if (component.KeyContainer.ContainedEntities.Count == 0) + { + args.PushMarkup(Loc.GetString("examine-headset-no-keys")); + return; + } + + if (component.Channels.Count > 0) + { + args.PushMarkup(Loc.GetString("examine-headset-channels-prefix")); + AddChannelsExamine(component.Channels, component.DefaultChannel, args, _protoManager, "examine-headset-channel"); + } + } + + private void OnKeyExamined(EntityUid uid, EncryptionKeyComponent component, ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + if(component.Channels.Count > 0) + { + args.PushMarkup(Loc.GetString("examine-encryption-key-channels-prefix")); + AddChannelsExamine(component.Channels, component.DefaultChannel, args, _protoManager, "examine-headset-channel"); + } + } + + /// + /// A method for formating list of radio channels for examine events. + /// + /// HashSet of channels in headset, encryptionkey or etc. + /// IPrototypeManager for getting prototypes of channels with their variables. + /// String that provide id of pattern in .ftl files to format channel with variables of it. + public void AddChannelsExamine(HashSet channels, string? defaultChannel, ExaminedEvent examineEvent, IPrototypeManager protoManager, string channelFTLPattern) + { + RadioChannelPrototype? proto; + foreach (var id in channels) + { + proto = protoManager.Index(id); + + var key = id == SharedChatSystem.CommonChannel + ? SharedChatSystem.RadioCommonPrefix.ToString() + : $"{SharedChatSystem.RadioChannelPrefix}{proto.KeyCode}"; + + examineEvent.PushMarkup(Loc.GetString(channelFTLPattern, + ("color", proto.Color), + ("key", key), + ("id", proto.LocalizedName), + ("freq", proto.Frequency))); + } + + if (defaultChannel != null && _protoManager.TryIndex(defaultChannel, out proto)) + { + var msg = Loc.GetString("examine-default-channel", + ("prefix", SharedChatSystem.DefaultChannelPrefix), + ("channel", defaultChannel), + ("color", proto.Color)); + examineEvent.PushMarkup(msg); + } + } +} diff --git a/Content.Shared/Radio/EntitySystems/SharedHeadsetSystem.cs b/Content.Shared/Radio/EntitySystems/SharedHeadsetSystem.cs new file mode 100644 index 0000000000..8083c8cd3a --- /dev/null +++ b/Content.Shared/Radio/EntitySystems/SharedHeadsetSystem.cs @@ -0,0 +1,38 @@ +using Content.Shared.Inventory; +using Content.Shared.Inventory.Events; +using Content.Shared.Radio.Components; + +namespace Content.Shared.Radio.EntitySystems; + +public abstract class SharedHeadsetSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent>(OnGetDefault); + SubscribeLocalEvent(OnGotEquipped); + SubscribeLocalEvent(OnGotUnequipped); + } + + private void OnGetDefault(EntityUid uid, HeadsetComponent component, InventoryRelayedEvent args) + { + if (!component.Enabled || !component.IsEquipped) + { + // don't provide default channels from pocket slots. + return; + } + + if (TryComp(uid, out EncryptionKeyHolderComponent? keyHolder)) + args.Args.Channel ??= keyHolder.DefaultChannel; + } + + protected virtual void OnGotEquipped(EntityUid uid, HeadsetComponent component, GotEquippedEvent args) + { + component.IsEquipped = args.SlotFlags.HasFlag(component.RequiredSlot); + } + + protected virtual void OnGotUnequipped(EntityUid uid, HeadsetComponent component, GotUnequippedEvent args) + { + component.IsEquipped = false; + } +} diff --git a/Content.Shared/Radio/GetDefaultRadioChannelEvent.cs b/Content.Shared/Radio/GetDefaultRadioChannelEvent.cs new file mode 100644 index 0000000000..a8a48d6d9e --- /dev/null +++ b/Content.Shared/Radio/GetDefaultRadioChannelEvent.cs @@ -0,0 +1,15 @@ +using Content.Shared.Chat; +using Content.Shared.Inventory; + +namespace Content.Shared.Radio; + +public sealed class GetDefaultRadioChannelEvent : EntityEventArgs, IInventoryRelayEvent +{ + /// + /// Id of the default that will get addressed when using the + /// department/default channel prefix. See . + /// + public string? Channel; + + public SlotFlags TargetSlots => ~SlotFlags.POCKET; +} diff --git a/Content.Shared/Tools/Systems/SharedToolSystem.MultipleTool.cs b/Content.Shared/Tools/Systems/SharedToolSystem.MultipleTool.cs index 2bfe09ecbd..9cce7337ec 100644 --- a/Content.Shared/Tools/Systems/SharedToolSystem.MultipleTool.cs +++ b/Content.Shared/Tools/Systems/SharedToolSystem.MultipleTool.cs @@ -1,4 +1,5 @@ using System.Linq; +using System.Threading; using Content.Shared.Interaction; using Content.Shared.Tools.Components; using Robust.Shared.GameStates; @@ -19,6 +20,32 @@ public abstract class SharedToolSystem : EntitySystem SubscribeLocalEvent(OnMultipleToolHandleState); } + public bool UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel, + float doAfterDelay, string toolQualityNeeded, object? doAfterCompleteEvent = null, object? doAfterCancelledEvent = null, EntityUid? doAfterEventTarget = null, + Func? doAfterCheck = null, ToolComponent? toolComponent = null) + { + return UseTool(tool, user, target, fuel, doAfterDelay, new[] { toolQualityNeeded }, + doAfterCompleteEvent, doAfterCancelledEvent, doAfterEventTarget, doAfterCheck, toolComponent); + } + + public virtual bool UseTool( + EntityUid tool, + EntityUid user, + EntityUid? target, + float fuel, + float doAfterDelay, + IEnumerable toolQualitiesNeeded, + object? doAfterCompleteEvent = null, + object? doAfterCancelledEvent = null, + EntityUid? doAfterEventTarget = null, + Func? doAfterCheck = null, + ToolComponent? toolComponent = null, + CancellationToken? cancelToken = null) + { + // predicted tools when. + return false; + } + private void OnMultipleToolHandleState(EntityUid uid, MultipleToolComponent component, ref ComponentHandleState args) { if (args.Current is not MultipleToolComponentState state) diff --git a/Resources/Locale/en-US/chat/managers/chat-manager.ftl b/Resources/Locale/en-US/chat/managers/chat-manager.ftl index 7c818bcc65..c189cea10f 100644 --- a/Resources/Locale/en-US/chat/managers/chat-manager.ftl +++ b/Resources/Locale/en-US/chat/managers/chat-manager.ftl @@ -11,7 +11,8 @@ chat-manager-admin-ooc-chat-enabled-message = Admin OOC chat has been enabled. chat-manager-admin-ooc-chat-disabled-message = Admin OOC chat has been disabled. chat-manager-max-message-length-exceeded-message = Your message exceeded {$limit} character limit chat-manager-no-headset-on-message = You don't have a headset on! -chat-manager-no-such-channel = There is no such channel! +chat-manager-no-radio-key = No radio key specified! +chat-manager-no-such-channel = There is no channel with key '{$key}'! chat-manager-whisper-headset-on-message = You can't whisper on the radio! chat-manager-server-wrap-message = SERVER: {$message} chat-manager-sender-announcement-wrap-message = {$sender} Announcement: diff --git a/Resources/Locale/en-US/headset/headset-component.ftl b/Resources/Locale/en-US/headset/headset-component.ftl index e36015094d..e4706ecad2 100644 --- a/Resources/Locale/en-US/headset/headset-component.ftl +++ b/Resources/Locale/en-US/headset/headset-component.ftl @@ -14,7 +14,7 @@ examine-radio-frequency = It's set to broadcast over the {$frequency} frequency. examine-headset-channels-prefix = A small screen on the headset displays the following available frequencies: examine-headset-channel = [color={$color}]{$key} for {$id} ({$freq})[/color] examine-headset-no-keys = It seems broken. There are no encryption keys in it. -examine-headset-chat-prefix = Use this {$prefix} for your department's frequency. +examine-default-channel = Use {$prefix} for the default channel ([color={$color}]{$channel}[/color]). examine-headset-default-channel = It indicates that the default channel of this headset is [color={$color}]{$channel}[/color]. examine-encryption-key-default-channel = The default channel is [color={$color}]{$channel}[/color]. diff --git a/Resources/Prototypes/Entities/Clothing/Ears/headsets.yml b/Resources/Prototypes/Entities/Clothing/Ears/headsets.yml index 8dc10add35..402de92138 100644 --- a/Resources/Prototypes/Entities/Clothing/Ears/headsets.yml +++ b/Resources/Prototypes/Entities/Clothing/Ears/headsets.yml @@ -13,7 +13,7 @@ key_slots: - EncryptionKeyCommon - type: Headset - keysExtractionMethod: Screwing + - type: EncryptionKeyHolder - type: Sprite state: icon - type: Clothing diff --git a/Resources/Prototypes/Entities/Clothing/Ears/headsets_alt.yml b/Resources/Prototypes/Entities/Clothing/Ears/headsets_alt.yml index 85770cab21..be5327b0a4 100644 --- a/Resources/Prototypes/Entities/Clothing/Ears/headsets_alt.yml +++ b/Resources/Prototypes/Entities/Clothing/Ears/headsets_alt.yml @@ -115,6 +115,7 @@ description: An updated, modular syndicate intercom that fits over the head and takes encryption keys (there are 4 slots for them). components: - type: Headset + - type: EncryptionKeyHolder keySlots: 4 - type: ContainerFill containers: