]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Update radio prefix parsing (#13777)
authorLeon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Sat, 18 Feb 2023 17:27:56 +0000 (06:27 +1300)
committerGitHub <noreply@github.com>
Sat, 18 Feb 2023 17:27:56 +0000 (04:27 +1100)
32 files changed:
Content.Client/Chat/Managers/ChatManager.cs
Content.Client/Chat/Managers/IChatManager.cs
Content.Client/Radio/EntitySystems/HeadsetSystem.cs [new file with mode: 0644]
Content.Client/UserInterface/Systems/Chat/ChatUIController.cs
Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs
Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorItemButton.cs
Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorPopup.cs
Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml.cs
Content.Server/Chat/Systems/ChatSystem.Radio.cs [deleted file]
Content.Server/Chat/Systems/ChatSystem.cs
Content.Server/Radio/Components/HeadsetComponent.cs [deleted file]
Content.Server/Radio/Components/IntrinsicRadioTransmitterComponent.cs
Content.Server/Radio/Components/RadioMicrophoneComponent.cs
Content.Server/Radio/Components/RadioSpeakerComponent.cs
Content.Server/Radio/EntitySystems/HeadsetSystem.cs
Content.Server/StationEvents/Events/SolarFlare.cs
Content.Server/Tools/ToolSystem.cs
Content.Shared/Chat/SharedChatSystem.cs
Content.Shared/Inventory/InventorySystem.Relay.cs
Content.Shared/Radio/Components/EncryptionKeyComponent.cs [moved from Content.Shared/Radio/EncryptionKeyComponent.cs with 76% similarity]
Content.Shared/Radio/Components/EncryptionKeyHolderComponent.cs [new file with mode: 0644]
Content.Shared/Radio/Components/HeadsetComponent.cs [new file with mode: 0644]
Content.Shared/Radio/EncryptionChannelsChangedEvent.cs [new file with mode: 0644]
Content.Shared/Radio/EncryptionKeySystem.cs [deleted file]
Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs [new file with mode: 0644]
Content.Shared/Radio/EntitySystems/SharedHeadsetSystem.cs [new file with mode: 0644]
Content.Shared/Radio/GetDefaultRadioChannelEvent.cs [new file with mode: 0644]
Content.Shared/Tools/Systems/SharedToolSystem.MultipleTool.cs
Resources/Locale/en-US/chat/managers/chat-manager.ftl
Resources/Locale/en-US/headset/headset-component.ftl
Resources/Prototypes/Entities/Clothing/Ears/headsets.yml
Resources/Prototypes/Entities/Clothing/Ears/headsets_alt.yml

index 9b5209d9a3b2cdfea226a7085228407c558f4ffd..67b5f5202f9fb80cfdfd9dd7e2ac0936791ab7b4 100644 (file)
@@ -21,14 +21,14 @@ namespace Content.Client.Chat.Managers
             _sawmill.Level = LogLevel.Info;
         }
 
-        public void SendMessage(ReadOnlyMemory<char> 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;
index fc11745b37aa822aa2c2c28c003de5aa79233347..6464ca1019615c1a8b8258bef854a1e112407fac 100644 (file)
@@ -6,6 +6,6 @@ namespace Content.Client.Chat.Managers
     {
         void Initialize();
 
-        public void SendMessage(ReadOnlyMemory<char> 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 (file)
index 0000000..2810dd8
--- /dev/null
@@ -0,0 +1,7 @@
+using Content.Shared.Radio.EntitySystems;
+
+namespace Content.Client.Radio.EntitySystems;
+
+public sealed class HeadsetSystem : SharedHeadsetSystem
+{
+}
index 81169467874b35e2e03988dad3743904bb037f1d..33f1de054707fc4360592dba642cf66b99cda274 100644 (file)
@@ -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<char, ChatSelectChannel> 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<ChatSelectChannel, char> 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<char> text) SplitInputContents(string inputText)
+    private bool TryGetRadioChannel(string text, out RadioChannelPrototype? radioChannel)
     {
-        var text = inputText.AsMemory().Trim();
+        radioChannel = null;
+        return _player.LocalPlayer?.ControlledEntity is EntityUid { Valid: true } uid
+           && _chatSys != null
+           && _chatSys.TryProccessRadioMessage(uid, text, out _, out radioChannel, quiet: true);
+    }
+
+    public void UpdateSelectedChannel(ChatBox box)
+    {
+        var (prefixChannel, _, radioChannel) = SplitInputContents(box.ChatInput.Input.Text);
+
+        if (prefixChannel == ChatSelectChannel.None)
+            box.ChatInput.ChannelSelector.UpdateChannelSelectButton(box.SelectedChannel, null);
+        else
+            box.ChatInput.ChannelSelector.UpdateChannelSelectButton(prefixChannel, radioChannel);
+    }
+
+    public (ChatSelectChannel chatChannel, string text, RadioChannelPrototype? radioChannel) SplitInputContents(string text)
+    {
+        text = text.Trim();
         if (text.Length == 0)
-            return default;
+            return (ChatSelectChannel.None, text, null);
 
-        var prefixChar = text.Span[0];
-        var channel = PrefixToChannel.GetValueOrDefault(prefixChar);
+        // We only cut off prefix only if it is not a radio or local channel, which both map to the same /say command
+        // because ????????
 
-        if ((CanSendChannels & channel) != 0)
-            // Cut off prefix if it's valid and we can use the channel in question.
-            text = text[1..];
+        ChatSelectChannel chatChannel;
+        if (TryGetRadioChannel(text, out var radioChannel))
+            chatChannel = ChatSelectChannel.Radio;
         else
-            channel = 0;
+            chatChannel = PrefixToChannel.GetValueOrDefault(text[0]);
+
+        if ((CanSendChannels & chatChannel) == 0)
+            return (ChatSelectChannel.None, text, null);
 
-        channel = MapLocalIfGhost(channel);
+        if (chatChannel == ChatSelectChannel.Radio)
+            return (chatChannel, text, radioChannel);
 
-        // Trim from start again to cut out any whitespace between the prefix and message, if any.
-        return (channel, text.TrimStart());
+        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 (prefixChannel, text) = SplitInputContents(box.ChatInput.Input.Text);
+        var text = box.ChatInput.Input.Text;
+        box.ChatInput.Input.Clear();
+        box.ChatInput.Input.ReleaseKeyboardFocus();
+        UpdateSelectedChannel(box);
 
-            // 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;
-            }
+        if (string.IsNullOrWhiteSpace(text))
+            return;
+
+        (var prefixChannel, text, var _) = SplitInputContents(text);
 
-            _manager.SendMessage(text, prefixChannel == 0 ? channel : prefixChannel);
+        // 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;
         }
 
-        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);
index daf0d95ec4d90ae56bace7aa89ad034f66d57ced..e594b04d9a0470a3f7272b12836c14f272b30812 100644 (file)
@@ -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)
index fd25b2a619eb4567b22426ffb71c4d92b5160e09..bf37a412e8823a545a1abaf753105f1930916abf 100644 (file)
@@ -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));
     }
 }
index b17b9d4633bcc323f393100b7669c6fa6c98de03..0852c10bb917c9d6bfa566066c4bb9845150012b 100644 (file)
@@ -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());
index da81066e0b754b9cfb04a54c3ba76a9baf04e2b7..cc5bd2bf6725645c741cda4422f204a985443239 100644 (file)
@@ -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 (file)
index 1a18ac4..0000000
+++ /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
-{
-    /// <summary>
-    /// Cache of the keycodes for faster lookup.
-    /// </summary>
-    private Dictionary<char, RadioChannelPrototype> _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<RadioChannelPrototype>())
-        {
-            _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<HeadsetComponent>(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<RadioChannelPrototype>("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<IntrinsicRadioTransmitterComponent>(source))
-        {
-            _popup.PopupEntity(Loc.GetString("chat-manager-no-headset-on-message"), source, source);
-        }
-
-        return (message, channel);
-    }
-}
index a8f875bf4aaaaafc3f0c1aa290684e911077568c..f180cf045a47743ee0ad87d264a98ebce271d528 100644 (file)
@@ -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 (file)
index fb9ee9a..0000000
+++ /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;
-/// <summary>
-///     This component relays radio messages to the parent entity's chat when equipped.
-/// </summary>
-[RegisterComponent]
-[Access(typeof(HeadsetSystem))]
-public sealed class HeadsetComponent : Component
-{
-    /// <summary>
-    ///     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.
-    /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("isKeysUnlocked")]
-    public bool IsKeysUnlocked = true;
-    /// <summary>
-    ///     Shows which tool a person should use to extract the encryption keys from the headset.
-    ///     default "Screwing"
-    /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField("keysExtractionMethod", customTypeSerializer: typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
-    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<string> Channels = new();
-
-    // Maybe make the defaultChannel an actual channel type some day, and use that for parsing messages
-    // [DataField("defaultChannel", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
-    // public readonly HashSet<string> defaultChannel = new();
-
-    /// <summary>
-    ///     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.
-    /// </summary>
-    [ViewVariables(VVAccess.ReadOnly)]
-    public string? DefaultChannel;
-
-    [DataField("enabled")]
-    public bool Enabled = true;
-
-    public bool IsEquipped = false;
-
-    [DataField("requiredSlot")]
-    public SlotFlags RequiredSlot = SlotFlags.EARS;
-}
index 2da6b90f0be757df4558584b33f4e6b685215353..eb28bac4ecdaa23d91ee06f0af6f271306c33d14 100644 (file)
@@ -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<RadioChannelPrototype>))]
-    public readonly HashSet<string> Channels = new() { "Common" };
+    public readonly HashSet<string> Channels = new() { SharedChatSystem.CommonChannel };
 }
index 86c750762bc985a684db1563310358a21672bdfc..5ab2484a9d5ab81fdf39c8742ec708c687be1b6f 100644 (file)
@@ -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<RadioChannelPrototype>))]
-    public string BroadcastChannel = "Common";
+    public string BroadcastChannel = SharedChatSystem.CommonChannel;
 
     [ViewVariables, DataField("supportedChannels", customTypeSerializer: typeof(PrototypeIdListSerializer<RadioChannelPrototype>))]
     public List<string>? SupportedChannels;
index e83c0aa0fb5fffc2c8df345bdaeb49dac4984113..22a7578eea69e288c2d93c01686a2f01991bf55a 100644 (file)
@@ -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<RadioChannelPrototype>))]
-    public HashSet<string> Channels = new () { "Common" };
+    public HashSet<string> Channels = new () { SharedChatSystem.CommonChannel };
 
     [DataField("enabled")]
     public bool Enabled;
index 2a66f985d3e846a3030f6d7d9ea7aa0bb42c1c44..87ec6448aec6df9ead0619a090ca76a8775f4f3c 100644 (file)
@@ -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<HeadsetComponent, ExaminedEvent>(OnExamined);
         SubscribeLocalEvent<HeadsetComponent, RadioReceiveEvent>(OnHeadsetReceive);
-        SubscribeLocalEvent<HeadsetComponent, GotEquippedEvent>(OnGotEquipped);
-        SubscribeLocalEvent<HeadsetComponent, GotUnequippedEvent>(OnGotUnequipped);
+        SubscribeLocalEvent<HeadsetComponent, EncryptionChannelsChangedEvent>(OnKeysChanged);
+
         SubscribeLocalEvent<WearingHeadsetComponent, EntitySpokeEvent>(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;
 
-        SubscribeLocalEvent<HeadsetComponent, ComponentStartup>(OnStartup);
-        SubscribeLocalEvent<HeadsetComponent, InteractUsingEvent>(OnInteractUsing);
-        SubscribeLocalEvent<HeadsetComponent, EntInsertedIntoContainerMessage>(OnContainerInserted);
+        if (keyHolder.Channels.Count == 0)
+            RemComp<ActiveRadioComponent>(uid);
+        else
+            EnsureComp<ActiveRadioComponent>(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<WearingHeadsetComponent>(args.Equipee).Headset = uid;
-            UpdateRadioChannelsInActiveRadio(uid, component, EnsureComp<ActiveRadioComponent>(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<ActiveRadioComponent>(uid);
         RemComp<WearingHeadsetComponent>(args.Equipee);
@@ -92,7 +89,7 @@ public sealed class HeadsetSystem : EntitySystem
         else if (component.IsEquipped)
         {
             EnsureComp<WearingHeadsetComponent>(Transform(uid).ParentUid).Headset = uid;
-            EnsureComp<ActiveRadioComponent>(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<RadioChannelPrototype>(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<Container>(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<EncryptionKeyComponent>(i, out var key))
-            {
-                UploadChannelsFromKey(component, key);
-            }
-        }
-    }
-
-    private void OnInteractUsing(EntityUid uid, HeadsetComponent component, InteractUsingEvent args)
-    {
-        if (!TryComp<ContainerManagerComponent>(uid, out var storage))
-            return;
-        if(!component.IsKeysUnlocked)
-        {
-            _popupSystem.PopupEntity(Loc.GetString("headset-encryption-keys-are-locked"), uid, args.User);
-            return;
-        }
-        if (TryComp<EncryptionKeyComponent>(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<ToolComponent>(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<EntityUid>();
-                    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<EncryptionKeyComponent>(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<EncryptionKeyComponent>(args.Entity, out var added))
-        {
-            UpdateDefaultChannel(component);
-            UploadChannelsFromKey(component, added);
-            UpdateRadioChannelsInActiveRadio(uid, component, EnsureComp<ActiveRadioComponent>(uid));
-        }
-        return;
-    }
 }
index 2b4d6d182a663e8632b69fae416ab96870323650..2ae4be595ecea768dfdd9fc98c0a653784940bc3 100644 (file)
@@ -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;
 
index 3ffdc3680f2bb704d83d1440261d3de778e566ed..0368c7bc29a433e3b0bb6c51546f8e15f5431cde 100644 (file)
@@ -101,7 +101,7 @@ namespace Content.Server.Tools
         ///          the <see cref="doAfterCompleteEvent"/> and <see cref="doAfterCancelledEvent"/> being broadcast
         ///          to see whether using the tool succeeded or not. If the <see cref="doAfterDelay"/> is zero,
         ///          this simply returns whether using the tool succeeded or not.</returns>
-        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.
-        /// <inheritdoc cref="UseTool(Robust.Shared.GameObjects.EntityUid,Robust.Shared.GameObjects.EntityUid,System.Nullable{Robust.Shared.GameObjects.EntityUid},float,float,System.Collections.Generic.IEnumerable{string},Robust.Shared.GameObjects.EntityUid,object,object,System.Func{bool}?,Content.Shared.Tools.Components.ToolComponent?)"/>
-        public bool UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel,
-            float doAfterDelay, string toolQualityNeeded, object doAfterCompleteEvent, object doAfterCancelledEvent, EntityUid? doAfterEventTarget = null,
-            Func<bool>? doAfterCheck = null, ToolComponent? toolComponent = null)
-        {
-            return UseTool(tool, user, target, fuel, doAfterDelay, new[] { toolQualityNeeded },
-                doAfterCompleteEvent, doAfterCancelledEvent, doAfterEventTarget, doAfterCheck, toolComponent);
-        }
-
         /// <summary>
         ///     Async version of UseTool.
         /// </summary>
index 0d046e12b8fc07bfb37dedaa20f6b4291a51f218..040cb3af204c019babbbc26bfcce2f9bd1b06519 100644 (file)
@@ -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!;
+
+    /// <summary>
+    /// Cache of the keycodes for faster lookup.
+    /// </summary>
+    private Dictionary<char, RadioChannelPrototype> _keyCodes = new();
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        DebugTools.Assert(_prototypeManager.HasIndex<RadioChannelPrototype>(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<RadioChannelPrototype>())
+        {
+            _keyCodes.Add(proto.KeyCode, proto);
+        }
+    }
+
+    public override void Shutdown()
+    {
+        _prototypeManager.PrototypesReloaded -= OnPrototypeReload;
+    }
+
+    /// <summary>
+    ///     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.
+    /// </summary>
+    /// <param name="source">Source of the message</param>
+    /// <param name="input">The message to be modified</param>
+    /// <param name="output">The modified message</param>
+    /// <param name="channel">The channel that was requested, if any</param>
+    /// <param name="quiet">Whether or not to generate an informative pop-up message.</param>
+    /// <returns></returns>
+    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<RadioChannelPrototype>(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;
+    }
 }
index d336e0f1b0c0f1b6c64381b7e7643b2fa8dcb857..6b2c4c4257f13bde7e52b3c29ffcefb493f2b481 100644 (file)
@@ -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<InventoryComponent, BeforeStripEvent>(RelayInventoryEvent);
         SubscribeLocalEvent<InventoryComponent, SeeIdentityAttemptEvent>(RelayInventoryEvent);
         SubscribeLocalEvent<InventoryComponent, ModifyChangedTemperatureEvent>(RelayInventoryEvent);
+        SubscribeLocalEvent<InventoryComponent, GetDefaultRadioChannelEvent>(RelayInventoryEvent);
     }
 
     protected void RelayInventoryEvent<T>(EntityUid uid, InventoryComponent component, T args) where T : EntityEventArgs, IInventoryRelayEvent
similarity index 76%
rename from Content.Shared/Radio/EncryptionKeyComponent.cs
rename to Content.Shared/Radio/Components/EncryptionKeyComponent.cs
index 9fcd649a4065961156c8e0971cf3d0767cc42ce5..5a7a54eb55abc1749798e3cdff56c41e32b0cd15 100644 (file)
@@ -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;
+
 /// <summary>
 ///     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<RadioChannelPrototype>))]
     public HashSet<string> Channels = new();
 
-
     /// <summary>
-    ///     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 (<see cref="SharedChatSystem.DefaultChannelKey"/>).
     /// </summary>
     [DataField("defaultChannel", customTypeSerializer: typeof(PrototypeIdSerializer<RadioChannelPrototype>))]
     public readonly string? DefaultChannel;
diff --git a/Content.Shared/Radio/Components/EncryptionKeyHolderComponent.cs b/Content.Shared/Radio/Components/EncryptionKeyHolderComponent.cs
new file mode 100644 (file)
index 0000000..ca5d1f6
--- /dev/null
@@ -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;
+
+/// <summary>
+///     This component is by entities that can contain encryption keys
+/// </summary>
+[RegisterComponent]
+public sealed class EncryptionKeyHolderComponent : Component
+{
+    /// <summary>
+    ///     Whether or not encryption keys can be removed from the headset.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField("keysUnlocked")]
+    public bool KeysUnlocked = true;
+
+    /// <summary>
+    ///     The tool required to extract the encryption keys from the headset.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    [DataField("keysExtractionMethod", customTypeSerializer: typeof(PrototypeIdSerializer<ToolQualityPrototype>))]
+    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";
+
+    /// <summary>
+    ///     Combined set of radio channels provided by all contained keys.
+    /// </summary>
+    [ViewVariables]
+    public HashSet<string> Channels = new();
+
+    /// <summary>
+    ///     This is the channel that will be used when using the default/department prefix (<see cref="SharedChatSystem.DefaultChannelKey"/>).
+    /// </summary>
+    [ViewVariables]
+    public string? DefaultChannel;
+}
diff --git a/Content.Shared/Radio/Components/HeadsetComponent.cs b/Content.Shared/Radio/Components/HeadsetComponent.cs
new file mode 100644 (file)
index 0000000..4d3b0fd
--- /dev/null
@@ -0,0 +1,18 @@
+using Content.Shared.Inventory;
+
+namespace Content.Shared.Radio.Components;
+
+/// <summary>
+///     This component relays radio messages to the parent entity's chat when equipped.
+/// </summary>
+[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 (file)
index 0000000..3841248
--- /dev/null
@@ -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 (file)
index 37bd5c7..0000000
+++ /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<EncryptionKeyComponent, ExaminedEvent>(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<RadioChannelPrototype>(component.DefaultChannel);
-                args.PushMarkup(Loc.GetString("examine-encryption-key-default-channel", ("channel", component.DefaultChannel), ("color", proto.Color)));
-            }
-        }
-    }
-
-    /// <summary>
-    ///     A static method for formating list of radio channels for examine events.
-    /// </summary>
-    /// <param name="channels">HashSet of channels in headset, encryptionkey or etc.</param>
-    /// <param name="protoManager">IPrototypeManager for getting prototypes of channels with their variables.</param>
-    /// <param name="channelFTLPattern">String that provide id of pattern in .ftl files to format channel with variables of it.</param>
-    public static void GetChannelsExamine(HashSet<string> channels, ExaminedEvent examineEvent, IPrototypeManager protoManager, string channelFTLPattern)
-    {
-        foreach (var id in channels)
-        {
-            var proto = protoManager.Index<RadioChannelPrototype>(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 (file)
index 0000000..23d4ee5
--- /dev/null
@@ -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;
+
+/// <summary>
+///     This system manages encryption keys & key holders for use with radio channels.
+/// </summary>
+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<EncryptionKeyComponent, ExaminedEvent>(OnKeyExamined);
+        SubscribeLocalEvent<EncryptionKeyHolderComponent, ExaminedEvent>(OnHolderExamined);
+
+        SubscribeLocalEvent<EncryptionKeyHolderComponent, ComponentStartup>(OnStartup);
+        SubscribeLocalEvent<EncryptionKeyHolderComponent, InteractUsingEvent>(OnInteractUsing);
+        SubscribeLocalEvent<EncryptionKeyHolderComponent, EntInsertedIntoContainerMessage>(OnContainerModified);
+        SubscribeLocalEvent<EncryptionKeyHolderComponent, EntRemovedFromContainerMessage>(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<EncryptionKeyComponent>(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<ContainerManagerComponent>(uid, out var storage))
+            return;
+
+        if (TryComp<EncryptionKeyComponent>(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<ToolComponent>(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<Container>(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");
+        }
+    }
+
+    /// <summary>
+    ///     A method for formating list of radio channels for examine events.
+    /// </summary>
+    /// <param name="channels">HashSet of channels in headset, encryptionkey or etc.</param>
+    /// <param name="protoManager">IPrototypeManager for getting prototypes of channels with their variables.</param>
+    /// <param name="channelFTLPattern">String that provide id of pattern in .ftl files to format channel with variables of it.</param>
+    public void AddChannelsExamine(HashSet<string> channels, string? defaultChannel, ExaminedEvent examineEvent, IPrototypeManager protoManager, string channelFTLPattern)
+    {
+        RadioChannelPrototype? proto;
+        foreach (var id in channels)
+        {
+            proto = protoManager.Index<RadioChannelPrototype>(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 (file)
index 0000000..8083c8c
--- /dev/null
@@ -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<HeadsetComponent, InventoryRelayedEvent<GetDefaultRadioChannelEvent>>(OnGetDefault);
+        SubscribeLocalEvent<HeadsetComponent, GotEquippedEvent>(OnGotEquipped);
+        SubscribeLocalEvent<HeadsetComponent, GotUnequippedEvent>(OnGotUnequipped);
+    }
+
+    private void OnGetDefault(EntityUid uid, HeadsetComponent component, InventoryRelayedEvent<GetDefaultRadioChannelEvent> 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 (file)
index 0000000..a8a48d6
--- /dev/null
@@ -0,0 +1,15 @@
+using Content.Shared.Chat;
+using Content.Shared.Inventory;
+
+namespace Content.Shared.Radio;
+
+public sealed class GetDefaultRadioChannelEvent : EntityEventArgs, IInventoryRelayEvent
+{
+    /// <summary>
+    ///     Id of the default <see cref="RadioChannelPrototype"/> that will get addressed when using the
+    ///     department/default channel prefix. See <see cref="SharedChatSystem.DefaultChannelKey"/>.
+    /// </summary>
+    public string? Channel;
+
+    public SlotFlags TargetSlots => ~SlotFlags.POCKET;
+}
index 2bfe09ecbdb22109cb26ad3d83fea2a4c8a1d157..9cce7337ecb0d5bd85bd77536668a56364437fbd 100644 (file)
@@ -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<MultipleToolComponent, ComponentHandleState>(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<bool>? 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<string> toolQualitiesNeeded,
+        object? doAfterCompleteEvent = null,
+        object? doAfterCancelledEvent = null,
+        EntityUid? doAfterEventTarget = null,
+        Func<bool>? 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)
index 7c818bcc65188309ef04a401609ffcc8fabf3d8a..c189cea10f6a52c81ef3145ece89595d95258ac3 100644 (file)
@@ -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:
index e36015094dc9be3da629a2dca50d56def6715306..e4706ecad2f8e99ca13fcd80c48d88a227f4289b 100644 (file)
@@ -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].
 
index 8dc10add35f1b1a3fb29e15db891bfdcf1d29e46..402de92138c4b8afbb56f805ca45304287acc27c 100644 (file)
@@ -13,7 +13,7 @@
       key_slots:
       - EncryptionKeyCommon
   - type: Headset
-    keysExtractionMethod: Screwing
+  - type: EncryptionKeyHolder
   - type: Sprite
     state: icon
   - type: Clothing
index 85770cab21d70df353fe59ae56c883368163707a..be5327b0a4b535b613673e348e4e700d391900e4 100644 (file)
   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: