_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:
_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;
{
void Initialize();
- public void SendMessage(ReadOnlyMemory<char> text, ChatSelectChannel channel);
+ public void SendMessage(string text, ChatSelectChannel channel);
}
}
--- /dev/null
+using Content.Shared.Radio.EntitySystems;
+
+namespace Content.Client.Radio.EntitySystems;
+
+public sealed class HeadsetSystem : SharedHeadsetSystem
+{
+}
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;
[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 =
}
}
- public static string GetChannelSelectorName(ChatSelectChannel channelSelector)
- {
- return channelSelector.ToString();
- }
-
private void UpdateChannelPermissions()
{
CanSendChannels = default;
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);
}
}
- public char GetPrefixFromChannel(ChatSelectChannel channel)
- {
- return ChannelPrefixes.GetValueOrDefault(channel);
- }
-
public void RegisterChat(ChatBox chat)
{
_chats.Add(chat);
-using Content.Shared.Chat;
+using Content.Shared.Chat;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
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}");
}
};
}
- 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)
-using Content.Client.Stylesheets;
+using Content.Client.Stylesheets;
using Content.Shared.Chat;
using Robust.Client.UserInterface.Controls;
{
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));
}
}
-using Content.Shared.Chat;
+using Content.Shared.Chat;
using Robust.Client.UserInterface.Controls;
using static Robust.Client.UserInterface.Controls.BaseButton;
}
}
- /*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());
-using Content.Client.Chat;
+using Content.Client.Chat;
using Content.Client.Chat.TypingIndicator;
using Content.Client.UserInterface.Systems.Chat.Controls;
using Content.Shared.Chat;
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;
private void OnChannelSelect(ChatSelectChannel channel)
{
- UpdateSelectedChannel();
+ _controller.UpdateSelectedChannel(this);
}
public void Repopulate()
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();
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();
+++ /dev/null
-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);
- }
-}
public override void Initialize()
{
base.Initialize();
- InitializeRadio();
InitializeEmotes();
_configurationManager.OnValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged, true);
_configurationManager.OnValueChanged(CCVars.DeadLoocEnabled, OnDeadLoocEnabledChanged, true);
public override void Shutdown()
{
base.Shutdown();
- ShutdownRadio();
ShutdownEmotes();
_configurationManager.UnsubValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged);
}
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);
// 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;
}
}
.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))
+++ /dev/null
-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;
-}
+using Content.Server.Chat.Systems;
+using Content.Shared.Chat;
using Content.Shared.Radio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
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 };
}
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;
{
[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;
using Content.Server.Radio.EntitySystems;
+using Content.Shared.Chat;
using Content.Shared.Radio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
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;
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);
else if (component.IsEquipped)
{
EnsureComp<WearingHeadsetComponent>(Transform(uid).ParentUid).Headset = uid;
- EnsureComp<ActiveRadioComponent>(uid).Channels.UnionWith(component.Channels);
+ UpdateRadioChannels(uid, component);
}
}
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;
- }
}
using Robust.Shared.Random;
using Content.Server.Light.EntitySystems;
using Content.Server.Light.Components;
+using Content.Shared.Radio.Components;
namespace Content.Server.StationEvents.Events;
/// 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,
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>
+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;
+ }
}
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;
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
-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.
[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;
--- /dev/null
+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;
+}
--- /dev/null
+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;
+}
--- /dev/null
+using Content.Shared.Radio.Components;
+
+namespace Content.Shared.Radio;
+
+public sealed class EncryptionChannelsChangedEvent : EntityEventArgs
+{
+ public readonly EncryptionKeyHolderComponent Component;
+
+ public EncryptionChannelsChangedEvent(EncryptionKeyHolderComponent component)
+ {
+ Component = component;
+ }
+}
+++ /dev/null
-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)));
- }
- }
-}
--- /dev/null
+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);
+ }
+ }
+}
--- /dev/null
+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;
+ }
+}
--- /dev/null
+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;
+}
using System.Linq;
+using System.Threading;
using Content.Shared.Interaction;
using Content.Shared.Tools.Components;
using Robust.Shared.GameStates;
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)
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:
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].
key_slots:
- EncryptionKeyCommon
- type: Headset
- keysExtractionMethod: Screwing
+ - type: EncryptionKeyHolder
- type: Sprite
state: icon
- type: Clothing
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: