From 3da454140db308f918bcbd8cab73649601ae4f3a Mon Sep 17 00:00:00 2001 From: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com> Date: Thu, 23 Jun 2022 20:11:03 +1000 Subject: [PATCH] Add department-specific radio channels (#9061) * Add department-specific radio channels This commit adds working department-specific radio channels, while minimizing damage to the current codebase. It is expected that a future refactor will clean this up a bit. ChatSystem now has a RadioPrefix() method that recognizes department-specific channels (e.g. ":e" and ":m") in addition to the global channel (";"). It strips the prefix from the message and assigns messages an integer representing the destination channel, if any. IListen and IRadio now accept optional 'channel' arguments with this channel in mind. The ugly is that the integer channel number is hard-coded and also shows up in chat. Comms are not modeled at this time. You cannot break comms (yet). All headsets have channels soldered into them. You cannot change encryption keys to hop on new channels. Steal a headset instead. * Remove debugging print * Convert to prototypes * Use prototype names in headset prototype * Adjust list style * Document prototype fields * cringe * some cleanup * colours * Remove alphas at least * cc Co-authored-by: Kevin Zheng --- Content.Server/Actions/ActionsSystem.cs | 1 + .../Administration/Commands/DSay.cs | 1 + .../Administration/UI/AdminAnnounceEui.cs | 1 + Content.Server/Advertise/AdvertiseSystem.cs | 1 + Content.Server/AlertLevel/AlertLevelSystem.cs | 1 + .../Announcements/AnnounceCommand.cs | 1 + Content.Server/Chat/Commands/LOOCCommand.cs | 1 + Content.Server/Chat/Commands/MeCommand.cs | 1 + Content.Server/Chat/Commands/SayCommand.cs | 1 + .../Chat/Commands/WhisperCommand.cs | 3 +- .../Chat/Systems/ChatSystem.Radio.cs | 88 +++++++++++++++++++ .../Chat/{ => Systems}/ChatSystem.cs | 48 +++------- .../CommunicationsConsoleSystem.cs | 1 + Content.Server/Disease/DiseaseSystem.cs | 1 + Content.Server/GameTicking/GameTicker.cs | 1 + .../Ghost/Components/GhostRadioComponent.cs | 26 +++--- Content.Server/Headset/HeadsetComponent.cs | 59 +++++++------ Content.Server/Headset/HeadsetSystem.cs | 19 +++- Content.Server/Nuke/NukeCodeSystem.cs | 1 + Content.Server/Nuke/NukeSystem.cs | 1 + .../Components/HandheldRadioComponent.cs | 35 +++++--- Content.Server/Radio/Components/IListen.cs | 6 +- Content.Server/Radio/Components/IRadio.cs | 8 +- .../Radio/EntitySystems/ListeningSystem.cs | 8 +- .../Radio/EntitySystems/RadioSystem.cs | 10 +-- Content.Server/Roles/Job.cs | 1 + Content.Server/RoundEnd/RoundEndSystem.cs | 1 + Content.Server/Salvage/SalvageSystem.cs | 1 + Content.Server/Speech/AccentSystem.cs | 1 + Content.Server/Speech/SpeechNoiseSystem.cs | 1 + .../Station/Systems/StationSystem.cs | 1 + .../StationEvents/Events/DiseaseOutbreak.cs | 1 + .../StationEvents/Events/RandomSentience.cs | 1 + .../StationEvents/Events/StationEvent.cs | 1 + .../StationEvents/Events/ZombieOutbreak.cs | 1 + Content.Shared/Radio/RadioChannelPrototype.cs | 26 ++++++ .../en-US/chat/managers/chat-manager.ftl | 1 + .../en-US/headset/headset-component.ftl | 3 +- .../Entities/Clothing/Ears/headsets.yml | 50 +++++++++++ .../Entities/Clothing/Ears/headsets_alt.yml | 22 +++++ Resources/Prototypes/radio_channels.yml | 66 ++++++++++++++ 41 files changed, 397 insertions(+), 105 deletions(-) create mode 100644 Content.Server/Chat/Systems/ChatSystem.Radio.cs rename Content.Server/Chat/{ => Systems}/ChatSystem.cs (94%) create mode 100644 Content.Shared/Radio/RadioChannelPrototype.cs create mode 100644 Resources/Prototypes/radio_channels.yml diff --git a/Content.Server/Actions/ActionsSystem.cs b/Content.Server/Actions/ActionsSystem.cs index a0f6133eb93..98a800b99b9 100644 --- a/Content.Server/Actions/ActionsSystem.cs +++ b/Content.Server/Actions/ActionsSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Chat; +using Content.Server.Chat.Systems; using Content.Shared.Actions; using Content.Shared.Actions.ActionTypes; using JetBrains.Annotations; diff --git a/Content.Server/Administration/Commands/DSay.cs b/Content.Server/Administration/Commands/DSay.cs index 37517acddf8..79b0e8db7d6 100644 --- a/Content.Server/Administration/Commands/DSay.cs +++ b/Content.Server/Administration/Commands/DSay.cs @@ -1,4 +1,5 @@ using Content.Server.Chat; +using Content.Server.Chat.Systems; using Content.Shared.Administration; using Robust.Server.Player; using Robust.Shared.Console; diff --git a/Content.Server/Administration/UI/AdminAnnounceEui.cs b/Content.Server/Administration/UI/AdminAnnounceEui.cs index 18e074d3f54..2a42c0ac8aa 100644 --- a/Content.Server/Administration/UI/AdminAnnounceEui.cs +++ b/Content.Server/Administration/UI/AdminAnnounceEui.cs @@ -1,6 +1,7 @@ using Content.Server.Administration.Managers; using Content.Server.Chat; using Content.Server.Chat.Managers; +using Content.Server.Chat.Systems; using Content.Server.EUI; using Content.Shared.Administration; using Content.Shared.Eui; diff --git a/Content.Server/Advertise/AdvertiseSystem.cs b/Content.Server/Advertise/AdvertiseSystem.cs index 05faeb4be2e..f07765b687a 100644 --- a/Content.Server/Advertise/AdvertiseSystem.cs +++ b/Content.Server/Advertise/AdvertiseSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Advertisements; using Content.Server.Chat; +using Content.Server.Chat.Systems; using Content.Server.Power.Components; using Content.Server.VendingMachines; using Robust.Shared.Prototypes; diff --git a/Content.Server/AlertLevel/AlertLevelSystem.cs b/Content.Server/AlertLevel/AlertLevelSystem.cs index 717cef52b95..d99f16b852d 100644 --- a/Content.Server/AlertLevel/AlertLevelSystem.cs +++ b/Content.Server/AlertLevel/AlertLevelSystem.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Server.Chat; +using Content.Server.Chat.Systems; using Content.Server.Station.Systems; using Robust.Shared.Audio; using Robust.Shared.Player; diff --git a/Content.Server/Announcements/AnnounceCommand.cs b/Content.Server/Announcements/AnnounceCommand.cs index c5eeaa23bf0..d51ea19ab07 100644 --- a/Content.Server/Announcements/AnnounceCommand.cs +++ b/Content.Server/Announcements/AnnounceCommand.cs @@ -1,5 +1,6 @@ using Content.Server.Administration; using Content.Server.Chat; +using Content.Server.Chat.Systems; using Content.Shared.Administration; using Robust.Shared.Console; diff --git a/Content.Server/Chat/Commands/LOOCCommand.cs b/Content.Server/Chat/Commands/LOOCCommand.cs index 98df2a26e3d..a897a0b2bfb 100644 --- a/Content.Server/Chat/Commands/LOOCCommand.cs +++ b/Content.Server/Chat/Commands/LOOCCommand.cs @@ -1,3 +1,4 @@ +using Content.Server.Chat.Systems; using Content.Shared.Administration; using Robust.Server.Player; using Robust.Shared.Console; diff --git a/Content.Server/Chat/Commands/MeCommand.cs b/Content.Server/Chat/Commands/MeCommand.cs index 6d205712ec9..dc87d38245f 100644 --- a/Content.Server/Chat/Commands/MeCommand.cs +++ b/Content.Server/Chat/Commands/MeCommand.cs @@ -1,3 +1,4 @@ +using Content.Server.Chat.Systems; using Content.Shared.Administration; using Robust.Server.Player; using Robust.Shared.Console; diff --git a/Content.Server/Chat/Commands/SayCommand.cs b/Content.Server/Chat/Commands/SayCommand.cs index 8aa157c31b3..889ce77d2f3 100644 --- a/Content.Server/Chat/Commands/SayCommand.cs +++ b/Content.Server/Chat/Commands/SayCommand.cs @@ -1,3 +1,4 @@ +using Content.Server.Chat.Systems; using Content.Shared.Administration; using Robust.Server.Player; using Robust.Shared.Console; diff --git a/Content.Server/Chat/Commands/WhisperCommand.cs b/Content.Server/Chat/Commands/WhisperCommand.cs index 6c7dbb78389..04fa3977356 100644 --- a/Content.Server/Chat/Commands/WhisperCommand.cs +++ b/Content.Server/Chat/Commands/WhisperCommand.cs @@ -1,4 +1,5 @@ -using Content.Shared.Administration; +using Content.Server.Chat.Systems; +using Content.Shared.Administration; using Robust.Server.Player; using Robust.Shared.Console; using Robust.Shared.Enums; diff --git a/Content.Server/Chat/Systems/ChatSystem.Radio.cs b/Content.Server/Chat/Systems/ChatSystem.Radio.cs new file mode 100644 index 00000000000..ef97ad83b14 --- /dev/null +++ b/Content.Server/Chat/Systems/ChatSystem.Radio.cs @@ -0,0 +1,88 @@ +using System.Linq; +using System.Text.RegularExpressions; +using Content.Server.Headset; +using Content.Shared.Radio; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Server.Chat.Systems; + +public sealed partial class ChatSystem +{ + /// + /// Cache of the keycodes for faster lookup. + /// + private Dictionary _keyCodes = new(); + + private void InitializeRadio() + { + _prototypeManager.PrototypesReloaded += OnPrototypeReload; + CacheRadios(); + } + + private void OnPrototypeReload(PrototypesReloadedEventArgs obj) + { + CacheRadios(); + } + + private void CacheRadios() + { + _keyCodes.Clear(); + + foreach (var proto in _prototypeManager.EnumeratePrototypes()) + { + _keyCodes.Add(proto.KeyCode, proto); + } + } + + private void ShutdownRadio() + { + _prototypeManager.PrototypesReloaded -= OnPrototypeReload; + } + + private (string, RadioChannelPrototype?) GetRadioPrefix(EntityUid source, string message) + { + // TODO: Turn common into a true frequency and support multiple aliases. + var channelMessage = message.StartsWith(':') || message.StartsWith('.'); + var radioMessage = message.StartsWith(';') || channelMessage; + if (!radioMessage) return (message, null); + + // Special case for empty messages + if (message.Length <= 1) + return (string.Empty, null); + + // Look for a prefix indicating a destination radio channel. + RadioChannelPrototype? chan; + if (channelMessage && message.Length >= 2) + { + _keyCodes.TryGetValue(message[1], out chan); + + if (chan == null) + { + _popup.PopupEntity(Loc.GetString("chat-manager-no-such-channel"), source, Filter.Entities(source)); + chan = null; + } + + // Strip message prefix. + message = message[2..].TrimStart(); + } + else + { + // Remove semicolon + message = message[1..].TrimStart(); + chan = _prototypeManager.Index("Common"); + } + + if (_inventory.TryGetSlotEntity(source, "ears", out var entityUid) && + TryComp(entityUid, out HeadsetComponent? headset)) + { + headset.RadioRequested = true; + } + else + { + _popup.PopupEntity(Loc.GetString("chat-manager-no-headset-on-message"), source, Filter.Entities(source)); + } + + return (message, chan); + } +} diff --git a/Content.Server/Chat/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs similarity index 94% rename from Content.Server/Chat/ChatSystem.cs rename to Content.Server/Chat/Systems/ChatSystem.cs index 9ca58abaff0..648bfcf40fc 100644 --- a/Content.Server/Chat/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -5,7 +5,6 @@ using Content.Server.Administration.Managers; using Content.Server.Chat.Managers; using Content.Server.GameTicking; using Content.Server.Ghost.Components; -using Content.Server.Headset; using Content.Server.Players; using Content.Server.Popups; using Content.Server.Radio.EntitySystems; @@ -23,22 +22,24 @@ using Robust.Shared.Console; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Players; +using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; -namespace Content.Server.Chat; +namespace Content.Server.Chat.Systems; /// /// ChatSystem is responsible for in-simulation chat handling, such as whispering, speaking, emoting, etc. /// ChatSystem depends on ChatManager to actually send the messages. /// -public sealed class ChatSystem : SharedChatSystem +public sealed partial class ChatSystem : SharedChatSystem { [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IChatSanitizationManager _sanitizer = default!; [Dependency] private readonly IAdminManager _adminManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; @@ -56,6 +57,7 @@ public sealed class ChatSystem : SharedChatSystem public override void Initialize() { + InitializeRadio(); _configurationManager.OnValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged, true); SubscribeLocalEvent(OnGameChange); @@ -63,6 +65,7 @@ public sealed class ChatSystem : SharedChatSystem public override void Shutdown() { + ShutdownRadio(); _configurationManager.UnsubValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged); } @@ -226,14 +229,18 @@ public sealed class ChatSystem : SharedChatSystem if (!_actionBlocker.CanSpeak(source)) return; message = TransformSpeech(source, message); - _listener.PingListeners(source, message); + (message, var channel) = GetRadioPrefix(source, message); + + if (channel != null) + _listener.PingListeners(source, message, channel); + var messageWrap = Loc.GetString("chat-manager-entity-say-wrap-message", ("entityName", Name(source))); SendInVoiceRange(ChatChannel.Local, message, messageWrap, source, hideChat); var ev = new EntitySpokeEvent(message); - RaiseLocalEvent(source, ev, false); + RaiseLocalEvent(source, ev); _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user}: {message}"); } @@ -242,7 +249,6 @@ public sealed class ChatSystem : SharedChatSystem if (!_actionBlocker.CanSpeak(source)) return; message = TransformSpeech(source, message); - _listener.PingListeners(source, message); var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f); var transformSource = Transform(source); @@ -410,34 +416,8 @@ public sealed class ChatSystem : SharedChatSystem private string SanitizeMessageCapital(EntityUid source, string message) { - if (message.StartsWith(';')) - { - // Special case for ";" messages - if (message.Length == 1) - return ""; - - // Remove semicolon - message = message.Substring(1).TrimStart(); - - // Capitalize first letter - message = message[0].ToString().ToUpper() + message.Remove(0, 1); - - if (_inventory.TryGetSlotEntity(source, "ears", out var entityUid) && - TryComp(entityUid, out HeadsetComponent? headset)) - { - headset.RadioRequested = true; - } - else - { - _popup.PopupEntity(Loc.GetString("chat-manager-no-headset-on-message"), source, Filter.Entities(source)); - } - } - else - { - // Capitalize first letter - message = message[0].ToString().ToUpper() + message.Remove(0, 1); - } - + // Capitalize first letter + message = message[0].ToString().ToUpper() + message.Remove(0, 1); return message; } diff --git a/Content.Server/Communications/CommunicationsConsoleSystem.cs b/Content.Server/Communications/CommunicationsConsoleSystem.cs index e43c6ebd39c..30303e9bbca 100644 --- a/Content.Server/Communications/CommunicationsConsoleSystem.cs +++ b/Content.Server/Communications/CommunicationsConsoleSystem.cs @@ -2,6 +2,7 @@ using System.Globalization; using Content.Server.Access.Systems; using Content.Server.AlertLevel; using Content.Server.Chat; +using Content.Server.Chat.Systems; using Content.Server.Popups; using Content.Server.RoundEnd; using Content.Server.Station.Systems; diff --git a/Content.Server/Disease/DiseaseSystem.cs b/Content.Server/Disease/DiseaseSystem.cs index 3e4dc03aa83..071460817e2 100644 --- a/Content.Server/Disease/DiseaseSystem.cs +++ b/Content.Server/Disease/DiseaseSystem.cs @@ -5,6 +5,7 @@ using Content.Shared.Disease.Components; using Content.Server.Disease.Components; using Content.Server.Clothing.Components; using Content.Server.Body.Systems; +using Content.Server.Chat.Systems; using Content.Shared.MobState.Components; using Content.Shared.Examine; using Content.Shared.Inventory; diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index 6963de83428..bdb4d2e8a19 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -2,6 +2,7 @@ using Content.Server.Administration.Logs; using Content.Server.Administration.Managers; using Content.Server.Chat; using Content.Server.Chat.Managers; +using Content.Server.Chat.Systems; using Content.Server.Database; using Content.Server.Ghost; using Content.Server.Maps; diff --git a/Content.Server/Ghost/Components/GhostRadioComponent.cs b/Content.Server/Ghost/Components/GhostRadioComponent.cs index 5c5cef96714..981ff49775e 100644 --- a/Content.Server/Ghost/Components/GhostRadioComponent.cs +++ b/Content.Server/Ghost/Components/GhostRadioComponent.cs @@ -1,7 +1,9 @@ using Content.Server.Radio.Components; using Content.Shared.Chat; +using Content.Shared.Radio; using Robust.Server.GameObjects; using Robust.Shared.Network; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; namespace Content.Server.Ghost.Components { @@ -12,27 +14,27 @@ namespace Content.Server.Ghost.Components [Dependency] private readonly IServerNetManager _netManager = default!; [Dependency] private readonly IEntityManager _entMan = default!; - [DataField("channels")] - private List _channels = new(){1459}; + [DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))] + private HashSet _channels = new(); - public IReadOnlyList Channels => _channels; - - public void Receive(string message, int channel, EntityUid speaker) + public void Receive(string message, RadioChannelPrototype channel, EntityUid speaker) { - if (!_entMan.TryGetComponent(Owner, out ActorComponent? actor)) + if (!_channels.Contains(channel.ID) || !_entMan.TryGetComponent(Owner, out ActorComponent? actor)) return; var playerChannel = actor.PlayerSession.ConnectedClient; - var msg = new MsgChatMessage(); + var msg = new MsgChatMessage + { + Channel = ChatChannel.Radio, + Message = message, + //Square brackets are added here to avoid issues with escaping + MessageWrap = Loc.GetString("chat-radio-message-wrap", ("channel", $"\\[{channel.Name}\\]"), ("name", _entMan.GetComponent(speaker).EntityName)) + }; - msg.Channel = ChatChannel.Radio; - msg.Message = message; - //Square brackets are added here to avoid issues with escaping - msg.MessageWrap = Loc.GetString("chat-radio-message-wrap", ("channel", $"\\[{channel}\\]"), ("name", _entMan.GetComponent(speaker).EntityName)); _netManager.ServerSendMessage(msg, playerChannel); } - public void Broadcast(string message, EntityUid speaker) { } + public void Broadcast(string message, EntityUid speaker, RadioChannelPrototype channel) { } } } diff --git a/Content.Server/Headset/HeadsetComponent.cs b/Content.Server/Headset/HeadsetComponent.cs index 64da4609a94..30e316d46fa 100644 --- a/Content.Server/Headset/HeadsetComponent.cs +++ b/Content.Server/Headset/HeadsetComponent.cs @@ -1,9 +1,13 @@ using Content.Server.Radio.Components; using Content.Server.Radio.EntitySystems; using Content.Shared.Chat; +using Content.Shared.Radio; using Robust.Server.GameObjects; using Robust.Shared.Containers; +using Robust.Shared.Prototypes; using Robust.Shared.Network; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; namespace Content.Server.Headset { @@ -19,19 +23,16 @@ namespace Content.Server.Headset private RadioSystem _radioSystem = default!; - [DataField("channels")] - private List _channels = new(){1459}; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("broadcastChannel")] - public int BroadcastFrequency { get; set; } = 1459; + [DataField("channels", customTypeSerializer:typeof(PrototypeIdHashSetSerializer))] + public HashSet Channels = new() + { + "Common" + }; [ViewVariables(VVAccess.ReadWrite)] [DataField("listenRange")] public int ListenRange { get; private set; } - public IReadOnlyList Channels => _channels; - public bool RadioRequested { get; set; } protected override void Initialize() @@ -41,38 +42,40 @@ namespace Content.Server.Headset _radioSystem = EntitySystem.Get(); } - public bool CanListen(string message, EntityUid source) + public bool CanListen(string message, EntityUid source, RadioChannelPrototype prototype) { - return RadioRequested; + return Channels.Contains(prototype.ID) && RadioRequested; } - public void Receive(string message, int channel, EntityUid source) + public void Receive(string message, RadioChannelPrototype channel, EntityUid source) { - if (Owner.TryGetContainer(out var container)) + if (!Channels.Contains(channel.ID) || !Owner.TryGetContainer(out var container)) return; + + if (!_entMan.TryGetComponent(container.Owner, out ActorComponent? actor)) return; + + var playerChannel = actor.PlayerSession.ConnectedClient; + + var msg = new MsgChatMessage { - if (!_entMan.TryGetComponent(container.Owner, out ActorComponent? actor)) - return; - - var playerChannel = actor.PlayerSession.ConnectedClient; - - var msg = new MsgChatMessage(); - - msg.Channel = ChatChannel.Radio; - msg.Message = message; + Channel = ChatChannel.Radio, + Message = message, //Square brackets are added here to avoid issues with escaping - msg.MessageWrap = Loc.GetString("chat-radio-message-wrap", ("channel", $"\\[{channel}\\]"), ("name", _entMan.GetComponent(source).EntityName)); - _netManager.ServerSendMessage(msg, playerChannel); - } + MessageWrap = Loc.GetString("chat-radio-message-wrap", ("color", channel.Color), ("channel", $"\\[{channel.Name}\\]"), ("name", _entMan.GetComponent(source).EntityName)) + }; + + _netManager.ServerSendMessage(msg, playerChannel); } - public void Listen(string message, EntityUid speaker) + public void Listen(string message, EntityUid speaker, RadioChannelPrototype channel) { - Broadcast(message, speaker); + Broadcast(message, speaker, channel); } - public void Broadcast(string message, EntityUid speaker) + public void Broadcast(string message, EntityUid speaker, RadioChannelPrototype channel) { - _radioSystem.SpreadMessage(this, speaker, message, BroadcastFrequency); + if (!Channels.Contains(channel.ID)) return; + + _radioSystem.SpreadMessage(this, speaker, message, channel); RadioRequested = false; } } diff --git a/Content.Server/Headset/HeadsetSystem.cs b/Content.Server/Headset/HeadsetSystem.cs index a4013166e23..4f9c81c06d2 100644 --- a/Content.Server/Headset/HeadsetSystem.cs +++ b/Content.Server/Headset/HeadsetSystem.cs @@ -1,9 +1,13 @@ using Content.Shared.Examine; +using Content.Shared.Radio; +using Robust.Shared.Prototypes; namespace Content.Server.Headset { public sealed class HeadsetSystem : EntitySystem { + [Dependency] private readonly IPrototypeManager _protoManager = default!; + public override void Initialize() { base.Initialize(); @@ -14,8 +18,21 @@ namespace Content.Server.Headset { if (!args.IsInDetailsRange) return; - args.PushMarkup(Loc.GetString("examine-radio-frequency", ("frequency", component.BroadcastFrequency))); + // args.PushMarkup(Loc.GetString("examine-radio-frequency", ("frequency", component.BroadcastFrequency))); args.PushMarkup(Loc.GetString("examine-headset")); + + foreach (var id in component.Channels) + { + if (id == "Common") continue; + + var proto = _protoManager.Index(id); + args.PushMarkup(Loc.GetString("examine-headset-channel", + ("color", proto.Color), + ("key", proto.KeyCode), + ("id", proto.Name), + ("freq", proto.Frequency))); + } + args.PushMarkup(Loc.GetString("examine-headset-chat-prefix", ("prefix", ";"))); } } diff --git a/Content.Server/Nuke/NukeCodeSystem.cs b/Content.Server/Nuke/NukeCodeSystem.cs index 4a362eb902b..80f2a42bf82 100644 --- a/Content.Server/Nuke/NukeCodeSystem.cs +++ b/Content.Server/Nuke/NukeCodeSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Chat; +using Content.Server.Chat.Systems; using Content.Server.Communications; using Content.Server.Station.Systems; using Content.Shared.GameTicking; diff --git a/Content.Server/Nuke/NukeSystem.cs b/Content.Server/Nuke/NukeSystem.cs index bd1f3ca3ca8..174fdf5d281 100644 --- a/Content.Server/Nuke/NukeSystem.cs +++ b/Content.Server/Nuke/NukeSystem.cs @@ -1,6 +1,7 @@ using Content.Server.AlertLevel; using Content.Server.Chat; using Content.Server.Chat.Managers; +using Content.Server.Chat.Systems; using Content.Server.Coordinates.Helpers; using Content.Server.Explosion.EntitySystems; using Content.Server.Popups; diff --git a/Content.Server/Radio/Components/HandheldRadioComponent.cs b/Content.Server/Radio/Components/HandheldRadioComponent.cs index 0fe483913ef..c914b575d64 100644 --- a/Content.Server/Radio/Components/HandheldRadioComponent.cs +++ b/Content.Server/Radio/Components/HandheldRadioComponent.cs @@ -1,7 +1,12 @@ using Content.Server.Chat; +using Content.Server.Chat.Systems; using Content.Server.Radio.EntitySystems; using Content.Shared.Interaction; using Content.Shared.Popups; +using Content.Shared.Radio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; namespace Content.Server.Radio.Components { @@ -18,12 +23,16 @@ namespace Content.Server.Radio.Components private RadioSystem _radioSystem = default!; private bool _radioOn; - [DataField("channels")] - private List _channels = new(){1459}; + [DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))] + private HashSet _channels = new(); + public int BroadcastFrequency => IoCManager.Resolve() + .Index(BroadcastChannel).Frequency; + + // TODO: Assert in componentinit that channels has this. [ViewVariables(VVAccess.ReadWrite)] - [DataField("broadcastChannel")] - public int BroadcastFrequency { get; set; } = 1459; + [DataField("broadcastChannel", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string BroadcastChannel { get; set; } = "Common"; [ViewVariables(VVAccess.ReadWrite)] [DataField("listenRange")] public int ListenRange { get; private set; } = 7; @@ -38,8 +47,6 @@ namespace Content.Server.Radio.Components } } - [ViewVariables] public IReadOnlyList Channels => _channels; - protected override void Initialize() { base.Initialize(); @@ -66,28 +73,30 @@ namespace Content.Server.Radio.Components return true; } - public bool CanListen(string message, EntityUid source) + public bool CanListen(string message, EntityUid source, RadioChannelPrototype prototype) { + if (!_channels.Contains(prototype.ID)) return false; + return RadioOn && EntitySystem.Get().InRangeUnobstructed(Owner, source, range: ListenRange); } - public void Receive(string message, int channel, EntityUid speaker) + public void Receive(string message, RadioChannelPrototype channel, EntityUid speaker) { - if (RadioOn) + if (_channels.Contains(channel.ID) && RadioOn) { Speak(message); } } - public void Listen(string message, EntityUid speaker) + public void Listen(string message, EntityUid speaker, RadioChannelPrototype channel) { - Broadcast(message, speaker); + Broadcast(message, speaker, channel); } - public void Broadcast(string message, EntityUid speaker) + public void Broadcast(string message, EntityUid speaker, RadioChannelPrototype channel) { - _radioSystem.SpreadMessage(this, speaker, message, BroadcastFrequency); + _radioSystem.SpreadMessage(this, speaker, message, channel); } void IActivate.Activate(ActivateEventArgs eventArgs) diff --git a/Content.Server/Radio/Components/IListen.cs b/Content.Server/Radio/Components/IListen.cs index 0a46738c3d4..1effcab101a 100644 --- a/Content.Server/Radio/Components/IListen.cs +++ b/Content.Server/Radio/Components/IListen.cs @@ -1,3 +1,5 @@ +using Content.Shared.Radio; + namespace Content.Server.Radio.Components { /// @@ -8,8 +10,8 @@ namespace Content.Server.Radio.Components { int ListenRange { get; } - bool CanListen(string message, EntityUid source); + bool CanListen(string message, EntityUid source, RadioChannelPrototype channelPrototype); - void Listen(string message, EntityUid speaker); + void Listen(string message, EntityUid speaker, RadioChannelPrototype channel); } } diff --git a/Content.Server/Radio/Components/IRadio.cs b/Content.Server/Radio/Components/IRadio.cs index 07ece2cda23..a7cc0148c09 100644 --- a/Content.Server/Radio/Components/IRadio.cs +++ b/Content.Server/Radio/Components/IRadio.cs @@ -1,11 +1,11 @@ +using Content.Shared.Radio; + namespace Content.Server.Radio.Components { public interface IRadio : IComponent { - IReadOnlyList Channels { get; } + void Receive(string message, RadioChannelPrototype channel, EntityUid speaker); - void Receive(string message, int channel, EntityUid speaker); - - void Broadcast(string message, EntityUid speaker); + void Broadcast(string message, EntityUid speaker, RadioChannelPrototype channel); } } diff --git a/Content.Server/Radio/EntitySystems/ListeningSystem.cs b/Content.Server/Radio/EntitySystems/ListeningSystem.cs index 6783f1c530c..08486ad7e67 100644 --- a/Content.Server/Radio/EntitySystems/ListeningSystem.cs +++ b/Content.Server/Radio/EntitySystems/ListeningSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Radio.Components; +using Content.Shared.Radio; using JetBrains.Annotations; namespace Content.Server.Radio.EntitySystems @@ -6,14 +7,15 @@ namespace Content.Server.Radio.EntitySystems [UsedImplicitly] public sealed class ListeningSystem : EntitySystem { - public void PingListeners(EntityUid source, string message) + public void PingListeners(EntityUid source, string message, RadioChannelPrototype channel) { foreach (var listener in EntityManager.EntityQuery(true)) { + // TODO: Listening code is hella stinky so please refactor it someone. // TODO: Map Position distance - if (listener.CanListen(message, source)) + if (listener.CanListen(message, source, channel)) { - listener.Listen(message, source); + listener.Listen(message, source, channel); } } } diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs index 41366987232..8ce54605081 100644 --- a/Content.Server/Radio/EntitySystems/RadioSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs @@ -1,6 +1,7 @@ using System.Linq; using Content.Shared.Examine; using Content.Server.Radio.Components; +using Content.Shared.Radio; using JetBrains.Annotations; namespace Content.Server.Radio.EntitySystems @@ -23,7 +24,7 @@ namespace Content.Server.Radio.EntitySystems args.PushMarkup(Loc.GetString("handheld-radio-component-on-examine",("frequency", component.BroadcastFrequency))); } - public void SpreadMessage(IRadio source, EntityUid speaker, string message, int channel) + public void SpreadMessage(IRadio source, EntityUid speaker, string message, RadioChannelPrototype channel) { if (_messages.Contains(message)) return; @@ -31,11 +32,8 @@ namespace Content.Server.Radio.EntitySystems foreach (var radio in EntityManager.EntityQuery(true)) { - if (radio.Channels.Contains(channel)) - { - //TODO: once voice identity gets added, pass into receiver via source.GetSpeakerVoice() - radio.Receive(message, channel, speaker); - } + //TODO: once voice identity gets added, pass into receiver via source.GetSpeakerVoice() + radio.Receive(message, channel, speaker); } _messages.Remove(message); diff --git a/Content.Server/Roles/Job.cs b/Content.Server/Roles/Job.cs index b66d9e3f545..e9da1536590 100644 --- a/Content.Server/Roles/Job.cs +++ b/Content.Server/Roles/Job.cs @@ -1,5 +1,6 @@ using Content.Server.Chat; using Content.Server.Chat.Managers; +using Content.Server.Chat.Systems; using Content.Shared.Roles; namespace Content.Server.Roles diff --git a/Content.Server/RoundEnd/RoundEndSystem.cs b/Content.Server/RoundEnd/RoundEndSystem.cs index f528fe55405..abe96b31949 100644 --- a/Content.Server/RoundEnd/RoundEndSystem.cs +++ b/Content.Server/RoundEnd/RoundEndSystem.cs @@ -2,6 +2,7 @@ using System.Threading; using Content.Server.Administration.Logs; using Content.Server.Chat; using Content.Server.Chat.Managers; +using Content.Server.Chat.Systems; using Content.Server.GameTicking; using Content.Shared.Database; using Content.Shared.GameTicking; diff --git a/Content.Server/Salvage/SalvageSystem.cs b/Content.Server/Salvage/SalvageSystem.cs index 212c6547f6a..992bf5990b4 100644 --- a/Content.Server/Salvage/SalvageSystem.cs +++ b/Content.Server/Salvage/SalvageSystem.cs @@ -13,6 +13,7 @@ using Robust.Shared.Random; using Robust.Shared.Utility; using System.Linq; using Content.Server.Chat; +using Content.Server.Chat.Systems; using Content.Server.Station.Systems; namespace Content.Server.Salvage diff --git a/Content.Server/Speech/AccentSystem.cs b/Content.Server/Speech/AccentSystem.cs index c12871169a8..13f174c01ef 100644 --- a/Content.Server/Speech/AccentSystem.cs +++ b/Content.Server/Speech/AccentSystem.cs @@ -1,5 +1,6 @@ using System.Text.RegularExpressions; using Content.Server.Chat; +using Content.Server.Chat.Systems; namespace Content.Server.Speech { diff --git a/Content.Server/Speech/SpeechNoiseSystem.cs b/Content.Server/Speech/SpeechNoiseSystem.cs index aacc987a3d3..7c0a179a35a 100644 --- a/Content.Server/Speech/SpeechNoiseSystem.cs +++ b/Content.Server/Speech/SpeechNoiseSystem.cs @@ -1,5 +1,6 @@ using Robust.Shared.Audio; using Content.Server.Chat; +using Content.Server.Chat.Systems; using Content.Shared.Speech; using Robust.Shared.Player; using Robust.Shared.Prototypes; diff --git a/Content.Server/Station/Systems/StationSystem.cs b/Content.Server/Station/Systems/StationSystem.cs index aa061379ee7..953c5c2c59a 100644 --- a/Content.Server/Station/Systems/StationSystem.cs +++ b/Content.Server/Station/Systems/StationSystem.cs @@ -1,6 +1,7 @@ using System.Linq; using Content.Server.Chat; using Content.Server.Chat.Managers; +using Content.Server.Chat.Systems; using Content.Server.GameTicking; using Content.Server.Station.Components; using Content.Shared.CCVar; diff --git a/Content.Server/StationEvents/Events/DiseaseOutbreak.cs b/Content.Server/StationEvents/Events/DiseaseOutbreak.cs index 756fa3f9122..34685bcff2c 100644 --- a/Content.Server/StationEvents/Events/DiseaseOutbreak.cs +++ b/Content.Server/StationEvents/Events/DiseaseOutbreak.cs @@ -1,4 +1,5 @@ using Content.Server.Chat; +using Content.Server.Chat.Systems; using Content.Server.Disease.Components; using Content.Server.Disease; using Content.Server.Station.Systems; diff --git a/Content.Server/StationEvents/Events/RandomSentience.cs b/Content.Server/StationEvents/Events/RandomSentience.cs index d9580d29790..1ed371651c0 100644 --- a/Content.Server/StationEvents/Events/RandomSentience.cs +++ b/Content.Server/StationEvents/Events/RandomSentience.cs @@ -1,5 +1,6 @@ using System.Linq; using Content.Server.Chat; +using Content.Server.Chat.Systems; using Content.Server.Ghost.Roles.Components; using Content.Server.Mind.Commands; using Content.Server.Station.Systems; diff --git a/Content.Server/StationEvents/Events/StationEvent.cs b/Content.Server/StationEvents/Events/StationEvent.cs index e379aa98816..8218c292310 100644 --- a/Content.Server/StationEvents/Events/StationEvent.cs +++ b/Content.Server/StationEvents/Events/StationEvent.cs @@ -2,6 +2,7 @@ using Content.Server.Administration.Logs; using Content.Server.Atmos.EntitySystems; using Content.Server.Chat; using Content.Server.Chat.Managers; +using Content.Server.Chat.Systems; using Content.Server.GameTicking; using Content.Server.Station.Components; using Content.Server.Station.Systems; diff --git a/Content.Server/StationEvents/Events/ZombieOutbreak.cs b/Content.Server/StationEvents/Events/ZombieOutbreak.cs index 1025aafaf89..b05d6c0c81a 100644 --- a/Content.Server/StationEvents/Events/ZombieOutbreak.cs +++ b/Content.Server/StationEvents/Events/ZombieOutbreak.cs @@ -1,6 +1,7 @@ using Content.Server.Chat; using Robust.Shared.Random; using Content.Server.Chat.Managers; +using Content.Server.Chat.Systems; using Content.Server.Station.Systems; using Content.Shared.MobState.Components; using Content.Shared.Sound; diff --git a/Content.Shared/Radio/RadioChannelPrototype.cs b/Content.Shared/Radio/RadioChannelPrototype.cs new file mode 100644 index 00000000000..0348320212c --- /dev/null +++ b/Content.Shared/Radio/RadioChannelPrototype.cs @@ -0,0 +1,26 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.Radio +{ + [Prototype("radioChannel")] + public sealed class RadioChannelPrototype : IPrototype + { + /// + /// Human-readable name for the channel. + /// + [ViewVariables] [DataField("name")] public string Name { get; private set; } = string.Empty; + + /// + /// Single-character prefix to determine what channel a message should be sent to. + /// + [ViewVariables] [DataField("keycode")] public char KeyCode { get; private set; } = '\0'; + + [ViewVariables] [DataField("frequency")] public int Frequency { get; private set; } = 0; + + [ViewVariables] [DataField("color")] public Color Color { get; private set; } = Color.Lime; + + [ViewVariables] + [IdDataFieldAttribute] + public string ID { get; } = default!; + } +} diff --git a/Resources/Locale/en-US/chat/managers/chat-manager.ftl b/Resources/Locale/en-US/chat/managers/chat-manager.ftl index 7e4d3065e67..c555150fce4 100644 --- a/Resources/Locale/en-US/chat/managers/chat-manager.ftl +++ b/Resources/Locale/en-US/chat/managers/chat-manager.ftl @@ -9,6 +9,7 @@ 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-whisper-headset-on-message = You can't whisper on the radio! chat-manager-server-wrap-message = SERVER: {"{0}"} chat-manager-sender-announcement-wrap-message = {$sender} Announcement: diff --git a/Resources/Locale/en-US/headset/headset-component.ftl b/Resources/Locale/en-US/headset/headset-component.ftl index f057757c205..9ff3ec06b3f 100644 --- a/Resources/Locale/en-US/headset/headset-component.ftl +++ b/Resources/Locale/en-US/headset/headset-component.ftl @@ -1,8 +1,9 @@ # Chat window radio wrap (prefix and postfix) -chat-radio-message-wrap = {$channel} {$name} says, "{"{"}0{"}"}" +chat-radio-message-wrap = [color={$color}]{$channel} {$name} says, "{"{"}0{"}"}"[/color] examine-radio-frequency = It's set to broadcast over the {$frequency} frequency. examine-headset = A small screen on the headset displays the following available frequencies: +examine-headset-channel = [color={$color}]:{$key} for {$id} ({$freq})[/color] examine-headset-chat-prefix = Use {$prefix} for the currently tuned frequency. diff --git a/Resources/Prototypes/Entities/Clothing/Ears/headsets.yml b/Resources/Prototypes/Entities/Clothing/Ears/headsets.yml index 0ce99086c58..5da758a44e9 100644 --- a/Resources/Prototypes/Entities/Clothing/Ears/headsets.yml +++ b/Resources/Prototypes/Entities/Clothing/Ears/headsets.yml @@ -19,6 +19,10 @@ name: cargo headset description: A headset used by the quartermaster and his slaves. components: + - type: Headset + channels: + - Common + - Supply - type: Sprite sprite: Clothing/Ears/Headsets/cargo.rsi @@ -28,6 +32,17 @@ name: centcomm headset description: A headset used by the upper echelons of Nanotrasen. components: + - type: Headset + channels: + - Common + - Command + - CentCom + - Engineering + - Medical + - Science + - Security + - Service + - Supply - type: Sprite sprite: Clothing/Ears/Headsets/centcom.rsi @@ -37,6 +52,16 @@ name: command headset description: A headset with a commanding channel. components: + - type: Headset + channels: + - Common + - Command + - Engineering + - Medical + - Science + - Security + - Service + - Supply - type: Sprite sprite: Clothing/Ears/Headsets/command.rsi @@ -46,6 +71,10 @@ name: engineering headset description: A headset for engineers to chat while the station burns around them. components: + - type: Headset + channels: + - Common + - Engineering - type: Sprite sprite: Clothing/Ears/Headsets/engineering.rsi @@ -55,6 +84,10 @@ name: medical headset description: A headset for the trained staff of the medbay. components: + - type: Headset + channels: + - Common + - Medical - type: Sprite sprite: Clothing/Ears/Headsets/medical.rsi @@ -64,6 +97,11 @@ name: medical research headset description: A headset that is a result of the mating between medical and science. components: + - type: Headset + channels: + - Common + - Medical + - Science - type: Sprite sprite: Clothing/Ears/Headsets/medicalscience.rsi @@ -91,6 +129,10 @@ name: science headset description: A sciency headset. Like usual. components: + - type: Headset + channels: + - Common + - Science - type: Sprite sprite: Clothing/Ears/Headsets/science.rsi @@ -100,6 +142,10 @@ name: security headset description: This is used by your elite security force. components: + - type: Headset + channels: + - Common + - Security - type: Sprite sprite: Clothing/Ears/Headsets/security.rsi @@ -109,5 +155,9 @@ name: service headset description: Headset used by the service staff, tasked with keeping the station full, happy and clean. components: + - type: Headset + channels: + - Common + - Service - type: Sprite sprite: Clothing/Ears/Headsets/service.rsi diff --git a/Resources/Prototypes/Entities/Clothing/Ears/headsets_alt.yml b/Resources/Prototypes/Entities/Clothing/Ears/headsets_alt.yml index 10087a9c39b..f8eb04670d4 100644 --- a/Resources/Prototypes/Entities/Clothing/Ears/headsets_alt.yml +++ b/Resources/Prototypes/Entities/Clothing/Ears/headsets_alt.yml @@ -17,6 +17,16 @@ id: ClothingHeadsetAltCommand name: command overear-headset components: + - type: Headset + channels: + - Common + - Command + - Engineering + - Medical + - Science + - Security + - Service + - Supply - type: Sprite sprite: Clothing/Ears/Headsets/command.rsi - type: Clothing @@ -27,6 +37,10 @@ id: ClothingHeadsetAltMedical name: medical overear-headset components: + - type: Headset + channels: + - Common + - Medical - type: Sprite sprite: Clothing/Ears/Headsets/medical.rsi - type: Clothing @@ -37,6 +51,10 @@ id: ClothingHeadsetAltSecurity name: security overear-headset components: + - type: Headset + channels: + - Common + - Security - type: Sprite sprite: Clothing/Ears/Headsets/security.rsi - type: Clothing @@ -48,6 +66,10 @@ name: syndicate overear-headset description: A syndicate headset that can be used to hear all radio frequencies. Protects ears from flashbangs. components: + - type: Headset + channels: + - Common + - Syndicate - type: Sprite sprite: Clothing/Ears/Headsets/syndicate.rsi - type: Clothing diff --git a/Resources/Prototypes/radio_channels.yml b/Resources/Prototypes/radio_channels.yml new file mode 100644 index 00000000000..8fd7bfb1d22 --- /dev/null +++ b/Resources/Prototypes/radio_channels.yml @@ -0,0 +1,66 @@ +- type: radioChannel + id: Common + name: "Common" + keycode: ";" + frequency: 1459 + +- type: radioChannel + id: CentCom + name: "CentCom" + keycode: 'y' + frequency: 1337 + +- type: radioChannel + id: Command + name: "Command" + keycode: 'c' + frequency: 1353 + color: "#334e6d" + +- type: radioChannel + id: Engineering + name: "Engineering" + keycode: 'e' + frequency: 1357 + color: "#efb341" + +- type: radioChannel + id: Medical + name: "Medical" + keycode: 'm' + frequency: 1355 + color: "#52b4e9" + +- type: radioChannel + id: Science + name: "Science" + keycode: 'n' + frequency: 1351 + color: "#d381c9" + +- type: radioChannel + id: Security + name: "Security" + keycode: 's' + frequency: 1359 + color: "#de3a3a" + +- type: radioChannel + id: Service + name: "Service" + keycode: 'v' + frequency: 1349 + color: "#9fed58" + +- type: radioChannel + id: Supply + name: "Supply" + keycode: 'u' + frequency: 1347 + color: "#a46106" + +- type: radioChannel + id: Syndicate + name: "Syndicate" + keycode: 't' + frequency: 1213