Update radio prefix parsing (#13777)
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
7
Content.Client/Radio/EntitySystems/HeadsetSystem.cs
Normal file
7
Content.Client/Radio/EntitySystems/HeadsetSystem.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using Content.Shared.Radio.EntitySystems;
|
||||
|
||||
namespace Content.Client.Radio.EntitySystems;
|
||||
|
||||
public sealed class HeadsetSystem : SharedHeadsetSystem
|
||||
{
|
||||
}
|
||||
@@ -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,34 +579,69 @@ 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 (prefixChannel, text) = SplitInputContents(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)
|
||||
@@ -630,12 +652,15 @@ public sealed class ChatUIController : UIController
|
||||
return;
|
||||
}
|
||||
|
||||
_manager.SendMessage(text, prefixChannel == 0 ? channel : prefixChannel);
|
||||
if (prefixChannel != ChatSelectChannel.None)
|
||||
channel = prefixChannel;
|
||||
else if (channel == ChatSelectChannel.Radio)
|
||||
{
|
||||
// radio must have prefix as it goes through the say command.
|
||||
text = $";{text}";
|
||||
}
|
||||
|
||||
box.ChatInput.Input.Clear();
|
||||
box.UpdateSelectedChannel();
|
||||
box.ChatInput.Input.ReleaseKeyboardFocus();
|
||||
_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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
18
Content.Shared/Radio/Components/HeadsetComponent.cs
Normal file
18
Content.Shared/Radio/Components/HeadsetComponent.cs
Normal 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;
|
||||
}
|
||||
13
Content.Shared/Radio/EncryptionChannelsChangedEvent.cs
Normal file
13
Content.Shared/Radio/EncryptionChannelsChangedEvent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
196
Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs
Normal file
196
Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Content.Shared/Radio/EntitySystems/SharedHeadsetSystem.cs
Normal file
38
Content.Shared/Radio/EntitySystems/SharedHeadsetSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
15
Content.Shared/Radio/GetDefaultRadioChannelEvent.cs
Normal file
15
Content.Shared/Radio/GetDefaultRadioChannelEvent.cs
Normal 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;
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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].
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
key_slots:
|
||||
- EncryptionKeyCommon
|
||||
- type: Headset
|
||||
keysExtractionMethod: Screwing
|
||||
- type: EncryptionKeyHolder
|
||||
- type: Sprite
|
||||
state: icon
|
||||
- type: Clothing
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user