Update radio prefix parsing (#13777)

This commit is contained in:
Leon Friedrich
2023-02-19 06:27:56 +13:00
committed by GitHub
parent 63a0c76ecc
commit 75a559fa55
32 changed files with 659 additions and 606 deletions

View 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;

View 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);
}
}

View File

@@ -0,0 +1,7 @@
using Content.Shared.Radio.EntitySystems;
namespace Content.Client.Radio.EntitySystems;
public sealed class HeadsetSystem : SharedHeadsetSystem
{
}

View 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();
if (text.Length == 0)
return default;
radioChannel = null;
return _player.LocalPlayer?.ControlledEntity is EntityUid { Valid: true } uid
&& _chatSys != null
&& _chatSys.TryProccessRadioMessage(uid, text, out _, out radioChannel, quiet: true);
}
var prefixChar = text.Span[0];
var channel = PrefixToChannel.GetValueOrDefault(prefixChar);
public void UpdateSelectedChannel(ChatBox box)
{
var (prefixChannel, _, radioChannel) = SplitInputContents(box.ChatInput.Input.Text);
if ((CanSendChannels & channel) != 0)
// Cut off prefix if it's valid and we can use the channel in question.
text = text[1..];
if (prefixChannel == ChatSelectChannel.None)
box.ChatInput.ChannelSelector.UpdateChannelSelectButton(box.SelectedChannel, null);
else
channel = 0;
box.ChatInput.ChannelSelector.UpdateChannelSelectButton(prefixChannel, radioChannel);
}
channel = MapLocalIfGhost(channel);
public (ChatSelectChannel chatChannel, string text, RadioChannelPrototype? radioChannel) SplitInputContents(string text)
{
text = text.Trim();
if (text.Length == 0)
return (ChatSelectChannel.None, text, null);
// Trim from start again to cut out any whitespace between the prefix and message, if any.
return (channel, text.TrimStart());
// We only cut off prefix only if it is not a radio or local channel, which both map to the same /say command
// because ????????
ChatSelectChannel chatChannel;
if (TryGetRadioChannel(text, out var radioChannel))
chatChannel = ChatSelectChannel.Radio;
else
chatChannel = PrefixToChannel.GetValueOrDefault(text[0]);
if ((CanSendChannels & chatChannel) == 0)
return (ChatSelectChannel.None, text, null);
if (chatChannel == ChatSelectChannel.Radio)
return (chatChannel, text, radioChannel);
if (chatChannel == ChatSelectChannel.Local)
{
if (_ghost?.IsGhost != true)
return (chatChannel, text, null);
else
chatChannel = ChatSelectChannel.Dead;
}
return (chatChannel, text[1..].TrimStart(), null);
}
public void SendMessage(ChatBox box, ChatSelectChannel channel)
{
_typingIndicator?.ClientSubmittedChatText();
if (!string.IsNullOrWhiteSpace(box.ChatInput.Input.Text))
var text = box.ChatInput.Input.Text;
box.ChatInput.Input.Clear();
box.ChatInput.Input.ReleaseKeyboardFocus();
UpdateSelectedChannel(box);
if (string.IsNullOrWhiteSpace(text))
return;
(var prefixChannel, text, var _) = SplitInputContents(text);
// Check if message is longer than the character limit
if (text.Length > MaxMessageLength)
{
var (prefixChannel, text) = SplitInputContents(box.ChatInput.Input.Text);
// Check if message is longer than the character limit
if (text.Length > MaxMessageLength)
{
var locWarning = Loc.GetString("chat-manager-max-message-length",
("maxMessageLength", MaxMessageLength));
box.AddLine(locWarning, Color.Orange);
return;
}
_manager.SendMessage(text, prefixChannel == 0 ? channel : prefixChannel);
var locWarning = Loc.GetString("chat-manager-max-message-length",
("maxMessageLength", MaxMessageLength));
box.AddLine(locWarning, Color.Orange);
return;
}
box.ChatInput.Input.Clear();
box.UpdateSelectedChannel();
box.ChatInput.Input.ReleaseKeyboardFocus();
if (prefixChannel != ChatSelectChannel.None)
channel = prefixChannel;
else if (channel == ChatSelectChannel.Radio)
{
// radio must have prefix as it goes through the say command.
text = $";{text}";
}
_manager.SendMessage(text, prefixChannel == 0 ? channel : prefixChannel);
}
private void OnChatMessage(MsgChatMessage message) => ProcessChatMessage(message.Message);
@@ -687,11 +712,6 @@ public sealed class ChatUIController : UIController
}
}
public char GetPrefixFromChannel(ChatSelectChannel channel)
{
return ChannelPrefixes.GetValueOrDefault(channel);
}
public void RegisterChat(ChatBox chat)
{
_chats.Add(chat);

View 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)

View 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));
}
}

View 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());

View 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();

View File

@@ -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);
}
}

View 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))

View File

@@ -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;
}

View 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 };
}

View 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;

View 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;

View 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<WearingHeadsetComponent, EntitySpokeEvent>(OnSpeak);
SubscribeLocalEvent<HeadsetComponent, EncryptionChannelsChangedEvent>(OnKeysChanged);
SubscribeLocalEvent<HeadsetComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<HeadsetComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<HeadsetComponent, EntInsertedIntoContainerMessage>(OnContainerInserted);
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;
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;
}
}

View 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;

View 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>

View 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;
}
}

View 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

View 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;

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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)));
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View 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)

View 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:

View 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].

View File

@@ -13,7 +13,7 @@
key_slots:
- EncryptionKeyCommon
- type: Headset
keysExtractionMethod: Screwing
- type: EncryptionKeyHolder
- type: Sprite
state: icon
- type: Clothing

View File

@@ -115,6 +115,7 @@
description: An updated, modular syndicate intercom that fits over the head and takes encryption keys (there are 4 slots for them).
components:
- type: Headset
- type: EncryptionKeyHolder
keySlots: 4
- type: ContainerFill
containers: