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 <kevinz5000@gmail.com>
This commit is contained in:
metalgearsloth
2022-06-23 20:11:03 +10:00
committed by GitHub
parent de760942e7
commit 3da454140d
41 changed files with 397 additions and 105 deletions

View File

@@ -1,4 +1,5 @@
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Shared.Actions; using Content.Shared.Actions;
using Content.Shared.Actions.ActionTypes; using Content.Shared.Actions.ActionTypes;
using JetBrains.Annotations; using JetBrains.Annotations;

View File

@@ -1,4 +1,5 @@
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Shared.Administration; using Content.Shared.Administration;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Console; using Robust.Shared.Console;

View File

@@ -1,6 +1,7 @@
using Content.Server.Administration.Managers; using Content.Server.Administration.Managers;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.EUI; using Content.Server.EUI;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.Eui; using Content.Shared.Eui;

View File

@@ -1,5 +1,6 @@
using Content.Server.Advertisements; using Content.Server.Advertisements;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Power.Components; using Content.Server.Power.Components;
using Content.Server.VendingMachines; using Content.Server.VendingMachines;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;

View File

@@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Player; using Robust.Shared.Player;

View File

@@ -1,5 +1,6 @@
using Content.Server.Administration; using Content.Server.Administration;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Shared.Administration; using Content.Shared.Administration;
using Robust.Shared.Console; using Robust.Shared.Console;

View File

@@ -1,3 +1,4 @@
using Content.Server.Chat.Systems;
using Content.Shared.Administration; using Content.Shared.Administration;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Console; using Robust.Shared.Console;

View File

@@ -1,3 +1,4 @@
using Content.Server.Chat.Systems;
using Content.Shared.Administration; using Content.Shared.Administration;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Console; using Robust.Shared.Console;

View File

@@ -1,3 +1,4 @@
using Content.Server.Chat.Systems;
using Content.Shared.Administration; using Content.Shared.Administration;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Console; using Robust.Shared.Console;

View File

@@ -1,4 +1,5 @@
using Content.Shared.Administration; using Content.Server.Chat.Systems;
using Content.Shared.Administration;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Console; using Robust.Shared.Console;
using Robust.Shared.Enums; using Robust.Shared.Enums;

View File

@@ -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
{
/// <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 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<RadioChannelPrototype>("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);
}
}

View File

@@ -5,7 +5,6 @@ using Content.Server.Administration.Managers;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.Ghost.Components; using Content.Server.Ghost.Components;
using Content.Server.Headset;
using Content.Server.Players; using Content.Server.Players;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.Radio.EntitySystems; using Content.Server.Radio.EntitySystems;
@@ -23,22 +22,24 @@ using Robust.Shared.Console;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Players; using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Server.Chat; namespace Content.Server.Chat.Systems;
/// <summary> /// <summary>
/// ChatSystem is responsible for in-simulation chat handling, such as whispering, speaking, emoting, etc. /// ChatSystem is responsible for in-simulation chat handling, such as whispering, speaking, emoting, etc.
/// ChatSystem depends on ChatManager to actually send the messages. /// ChatSystem depends on ChatManager to actually send the messages.
/// </summary> /// </summary>
public sealed class ChatSystem : SharedChatSystem public sealed partial class ChatSystem : SharedChatSystem
{ {
[Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!; [Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IChatSanitizationManager _sanitizer = default!; [Dependency] private readonly IChatSanitizationManager _sanitizer = default!;
[Dependency] private readonly IAdminManager _adminManager = default!; [Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
@@ -56,6 +57,7 @@ public sealed class ChatSystem : SharedChatSystem
public override void Initialize() public override void Initialize()
{ {
InitializeRadio();
_configurationManager.OnValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged, true); _configurationManager.OnValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged, true);
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameChange); SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameChange);
@@ -63,6 +65,7 @@ public sealed class ChatSystem : SharedChatSystem
public override void Shutdown() public override void Shutdown()
{ {
ShutdownRadio();
_configurationManager.UnsubValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged); _configurationManager.UnsubValueChanged(CCVars.LoocEnabled, OnLoocEnabledChanged);
} }
@@ -226,14 +229,18 @@ public sealed class ChatSystem : SharedChatSystem
if (!_actionBlocker.CanSpeak(source)) return; if (!_actionBlocker.CanSpeak(source)) return;
message = TransformSpeech(source, message); 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", var messageWrap = Loc.GetString("chat-manager-entity-say-wrap-message",
("entityName", Name(source))); ("entityName", Name(source)));
SendInVoiceRange(ChatChannel.Local, message, messageWrap, source, hideChat); SendInVoiceRange(ChatChannel.Local, message, messageWrap, source, hideChat);
var ev = new EntitySpokeEvent(message); var ev = new EntitySpokeEvent(message);
RaiseLocalEvent(source, ev, false); RaiseLocalEvent(source, ev);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {ToPrettyString(source):user}: {message}"); _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; if (!_actionBlocker.CanSpeak(source)) return;
message = TransformSpeech(source, message); message = TransformSpeech(source, message);
_listener.PingListeners(source, message);
var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f); var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f);
var transformSource = Transform(source); var transformSource = Transform(source);
@@ -410,34 +416,8 @@ public sealed class ChatSystem : SharedChatSystem
private string SanitizeMessageCapital(EntityUid source, string message) private string SanitizeMessageCapital(EntityUid source, string message)
{ {
if (message.StartsWith(';')) // Capitalize first letter
{ message = message[0].ToString().ToUpper() + message.Remove(0, 1);
// 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);
}
return message; return message;
} }

View File

@@ -2,6 +2,7 @@ using System.Globalization;
using Content.Server.Access.Systems; using Content.Server.Access.Systems;
using Content.Server.AlertLevel; using Content.Server.AlertLevel;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.RoundEnd; using Content.Server.RoundEnd;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;

View File

@@ -5,6 +5,7 @@ using Content.Shared.Disease.Components;
using Content.Server.Disease.Components; using Content.Server.Disease.Components;
using Content.Server.Clothing.Components; using Content.Server.Clothing.Components;
using Content.Server.Body.Systems; using Content.Server.Body.Systems;
using Content.Server.Chat.Systems;
using Content.Shared.MobState.Components; using Content.Shared.MobState.Components;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Inventory; using Content.Shared.Inventory;

View File

@@ -2,6 +2,7 @@ using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers; using Content.Server.Administration.Managers;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.Database; using Content.Server.Database;
using Content.Server.Ghost; using Content.Server.Ghost;
using Content.Server.Maps; using Content.Server.Maps;

View File

@@ -1,7 +1,9 @@
using Content.Server.Radio.Components; using Content.Server.Radio.Components;
using Content.Shared.Chat; using Content.Shared.Chat;
using Content.Shared.Radio;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.Ghost.Components namespace Content.Server.Ghost.Components
{ {
@@ -12,27 +14,27 @@ namespace Content.Server.Ghost.Components
[Dependency] private readonly IServerNetManager _netManager = default!; [Dependency] private readonly IServerNetManager _netManager = default!;
[Dependency] private readonly IEntityManager _entMan = default!; [Dependency] private readonly IEntityManager _entMan = default!;
[DataField("channels")] [DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
private List<int> _channels = new(){1459}; private HashSet<string> _channels = new();
public IReadOnlyList<int> Channels => _channels; public void Receive(string message, RadioChannelPrototype channel, EntityUid speaker)
public void Receive(string message, int channel, EntityUid speaker)
{ {
if (!_entMan.TryGetComponent(Owner, out ActorComponent? actor)) if (!_channels.Contains(channel.ID) || !_entMan.TryGetComponent(Owner, out ActorComponent? actor))
return; return;
var playerChannel = actor.PlayerSession.ConnectedClient; 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<MetaDataComponent>(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<MetaDataComponent>(speaker).EntityName));
_netManager.ServerSendMessage(msg, playerChannel); _netManager.ServerSendMessage(msg, playerChannel);
} }
public void Broadcast(string message, EntityUid speaker) { } public void Broadcast(string message, EntityUid speaker, RadioChannelPrototype channel) { }
} }
} }

View File

@@ -1,9 +1,13 @@
using Content.Server.Radio.Components; using Content.Server.Radio.Components;
using Content.Server.Radio.EntitySystems; using Content.Server.Radio.EntitySystems;
using Content.Shared.Chat; using Content.Shared.Chat;
using Content.Shared.Radio;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Network; 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 namespace Content.Server.Headset
{ {
@@ -19,19 +23,16 @@ namespace Content.Server.Headset
private RadioSystem _radioSystem = default!; private RadioSystem _radioSystem = default!;
[DataField("channels")] [DataField("channels", customTypeSerializer:typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
private List<int> _channels = new(){1459}; public HashSet<string> Channels = new()
{
[ViewVariables(VVAccess.ReadWrite)] "Common"
[DataField("broadcastChannel")] };
public int BroadcastFrequency { get; set; } = 1459;
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
[DataField("listenRange")] [DataField("listenRange")]
public int ListenRange { get; private set; } public int ListenRange { get; private set; }
public IReadOnlyList<int> Channels => _channels;
public bool RadioRequested { get; set; } public bool RadioRequested { get; set; }
protected override void Initialize() protected override void Initialize()
@@ -41,38 +42,40 @@ namespace Content.Server.Headset
_radioSystem = EntitySystem.Get<RadioSystem>(); _radioSystem = EntitySystem.Get<RadioSystem>();
} }
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)) Channel = ChatChannel.Radio,
return; Message = message,
var playerChannel = actor.PlayerSession.ConnectedClient;
var msg = new MsgChatMessage();
msg.Channel = ChatChannel.Radio;
msg.Message = message;
//Square brackets are added here to avoid issues with escaping //Square brackets are added here to avoid issues with escaping
msg.MessageWrap = Loc.GetString("chat-radio-message-wrap", ("channel", $"\\[{channel}\\]"), ("name", _entMan.GetComponent<MetaDataComponent>(source).EntityName)); MessageWrap = Loc.GetString("chat-radio-message-wrap", ("color", channel.Color), ("channel", $"\\[{channel.Name}\\]"), ("name", _entMan.GetComponent<MetaDataComponent>(source).EntityName))
_netManager.ServerSendMessage(msg, playerChannel); };
}
_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; RadioRequested = false;
} }
} }

View File

@@ -1,9 +1,13 @@
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.Radio;
using Robust.Shared.Prototypes;
namespace Content.Server.Headset namespace Content.Server.Headset
{ {
public sealed class HeadsetSystem : EntitySystem public sealed class HeadsetSystem : EntitySystem
{ {
[Dependency] private readonly IPrototypeManager _protoManager = default!;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -14,8 +18,21 @@ namespace Content.Server.Headset
{ {
if (!args.IsInDetailsRange) if (!args.IsInDetailsRange)
return; 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")); args.PushMarkup(Loc.GetString("examine-headset"));
foreach (var id in component.Channels)
{
if (id == "Common") continue;
var proto = _protoManager.Index<RadioChannelPrototype>(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", ";"))); args.PushMarkup(Loc.GetString("examine-headset-chat-prefix", ("prefix", ";")));
} }
} }

View File

@@ -1,4 +1,5 @@
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Communications; using Content.Server.Communications;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;
using Content.Shared.GameTicking; using Content.Shared.GameTicking;

View File

@@ -1,6 +1,7 @@
using Content.Server.AlertLevel; using Content.Server.AlertLevel;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.Coordinates.Helpers; using Content.Server.Coordinates.Helpers;
using Content.Server.Explosion.EntitySystems; using Content.Server.Explosion.EntitySystems;
using Content.Server.Popups; using Content.Server.Popups;

View File

@@ -1,7 +1,12 @@
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Radio.EntitySystems; using Content.Server.Radio.EntitySystems;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.Popups; 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 namespace Content.Server.Radio.Components
{ {
@@ -18,12 +23,16 @@ namespace Content.Server.Radio.Components
private RadioSystem _radioSystem = default!; private RadioSystem _radioSystem = default!;
private bool _radioOn; private bool _radioOn;
[DataField("channels")] [DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
private List<int> _channels = new(){1459}; private HashSet<string> _channels = new();
public int BroadcastFrequency => IoCManager.Resolve<IPrototypeManager>()
.Index<RadioChannelPrototype>(BroadcastChannel).Frequency;
// TODO: Assert in componentinit that channels has this.
[ViewVariables(VVAccess.ReadWrite)] [ViewVariables(VVAccess.ReadWrite)]
[DataField("broadcastChannel")] [DataField("broadcastChannel", customTypeSerializer: typeof(PrototypeIdSerializer<RadioChannelPrototype>))]
public int BroadcastFrequency { get; set; } = 1459; public string BroadcastChannel { get; set; } = "Common";
[ViewVariables(VVAccess.ReadWrite)] [DataField("listenRange")] public int ListenRange { get; private set; } = 7; [ViewVariables(VVAccess.ReadWrite)] [DataField("listenRange")] public int ListenRange { get; private set; } = 7;
@@ -38,8 +47,6 @@ namespace Content.Server.Radio.Components
} }
} }
[ViewVariables] public IReadOnlyList<int> Channels => _channels;
protected override void Initialize() protected override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -66,28 +73,30 @@ namespace Content.Server.Radio.Components
return true; 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 && return RadioOn &&
EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(Owner, source, range: ListenRange); EntitySystem.Get<SharedInteractionSystem>().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); 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) void IActivate.Activate(ActivateEventArgs eventArgs)

View File

@@ -1,3 +1,5 @@
using Content.Shared.Radio;
namespace Content.Server.Radio.Components namespace Content.Server.Radio.Components
{ {
/// <summary> /// <summary>
@@ -8,8 +10,8 @@ namespace Content.Server.Radio.Components
{ {
int ListenRange { get; } 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);
} }
} }

View File

@@ -1,11 +1,11 @@
using Content.Shared.Radio;
namespace Content.Server.Radio.Components namespace Content.Server.Radio.Components
{ {
public interface IRadio : IComponent public interface IRadio : IComponent
{ {
IReadOnlyList<int> Channels { get; } void Receive(string message, RadioChannelPrototype channel, EntityUid speaker);
void Receive(string message, int channel, EntityUid speaker); void Broadcast(string message, EntityUid speaker, RadioChannelPrototype channel);
void Broadcast(string message, EntityUid speaker);
} }
} }

View File

@@ -1,4 +1,5 @@
using Content.Server.Radio.Components; using Content.Server.Radio.Components;
using Content.Shared.Radio;
using JetBrains.Annotations; using JetBrains.Annotations;
namespace Content.Server.Radio.EntitySystems namespace Content.Server.Radio.EntitySystems
@@ -6,14 +7,15 @@ namespace Content.Server.Radio.EntitySystems
[UsedImplicitly] [UsedImplicitly]
public sealed class ListeningSystem : EntitySystem 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<IListen>(true)) foreach (var listener in EntityManager.EntityQuery<IListen>(true))
{ {
// TODO: Listening code is hella stinky so please refactor it someone.
// TODO: Map Position distance // TODO: Map Position distance
if (listener.CanListen(message, source)) if (listener.CanListen(message, source, channel))
{ {
listener.Listen(message, source); listener.Listen(message, source, channel);
} }
} }
} }

View File

@@ -1,6 +1,7 @@
using System.Linq; using System.Linq;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Server.Radio.Components; using Content.Server.Radio.Components;
using Content.Shared.Radio;
using JetBrains.Annotations; using JetBrains.Annotations;
namespace Content.Server.Radio.EntitySystems 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))); 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; if (_messages.Contains(message)) return;
@@ -31,11 +32,8 @@ namespace Content.Server.Radio.EntitySystems
foreach (var radio in EntityManager.EntityQuery<IRadio>(true)) foreach (var radio in EntityManager.EntityQuery<IRadio>(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); _messages.Remove(message);

View File

@@ -1,5 +1,6 @@
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Shared.Roles; using Content.Shared.Roles;
namespace Content.Server.Roles namespace Content.Server.Roles

View File

@@ -2,6 +2,7 @@ using System.Threading;
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.GameTicking; using Content.Shared.GameTicking;

View File

@@ -13,6 +13,7 @@ using Robust.Shared.Random;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using System.Linq; using System.Linq;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;
namespace Content.Server.Salvage namespace Content.Server.Salvage

View File

@@ -1,5 +1,6 @@
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Systems;
namespace Content.Server.Speech namespace Content.Server.Speech
{ {

View File

@@ -1,5 +1,6 @@
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Shared.Speech; using Content.Shared.Speech;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;

View File

@@ -1,6 +1,7 @@
using System.Linq; using System.Linq;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.Station.Components; using Content.Server.Station.Components;
using Content.Shared.CCVar; using Content.Shared.CCVar;

View File

@@ -1,4 +1,5 @@
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Disease.Components; using Content.Server.Disease.Components;
using Content.Server.Disease; using Content.Server.Disease;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;

View File

@@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Ghost.Roles.Components; using Content.Server.Ghost.Roles.Components;
using Content.Server.Mind.Commands; using Content.Server.Mind.Commands;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;

View File

@@ -2,6 +2,7 @@ using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Chat; using Content.Server.Chat;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.Station.Components; using Content.Server.Station.Components;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;

View File

@@ -1,6 +1,7 @@
using Content.Server.Chat; using Content.Server.Chat;
using Robust.Shared.Random; using Robust.Shared.Random;
using Content.Server.Chat.Managers; using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Server.Station.Systems; using Content.Server.Station.Systems;
using Content.Shared.MobState.Components; using Content.Shared.MobState.Components;
using Content.Shared.Sound; using Content.Shared.Sound;

View File

@@ -0,0 +1,26 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Radio
{
[Prototype("radioChannel")]
public sealed class RadioChannelPrototype : IPrototype
{
/// <summary>
/// Human-readable name for the channel.
/// </summary>
[ViewVariables] [DataField("name")] public string Name { get; private set; } = string.Empty;
/// <summary>
/// Single-character prefix to determine what channel a message should be sent to.
/// </summary>
[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!;
}
}

View File

@@ -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-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-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-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-whisper-headset-on-message = You can't whisper on the radio!
chat-manager-server-wrap-message = SERVER: {"{0}"} chat-manager-server-wrap-message = SERVER: {"{0}"}
chat-manager-sender-announcement-wrap-message = {$sender} Announcement: chat-manager-sender-announcement-wrap-message = {$sender} Announcement:

View File

@@ -1,8 +1,9 @@
# Chat window radio wrap (prefix and postfix) # 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-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 = 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. examine-headset-chat-prefix = Use {$prefix} for the currently tuned frequency.

View File

@@ -19,6 +19,10 @@
name: cargo headset name: cargo headset
description: A headset used by the quartermaster and his slaves. description: A headset used by the quartermaster and his slaves.
components: components:
- type: Headset
channels:
- Common
- Supply
- type: Sprite - type: Sprite
sprite: Clothing/Ears/Headsets/cargo.rsi sprite: Clothing/Ears/Headsets/cargo.rsi
@@ -28,6 +32,17 @@
name: centcomm headset name: centcomm headset
description: A headset used by the upper echelons of Nanotrasen. description: A headset used by the upper echelons of Nanotrasen.
components: components:
- type: Headset
channels:
- Common
- Command
- CentCom
- Engineering
- Medical
- Science
- Security
- Service
- Supply
- type: Sprite - type: Sprite
sprite: Clothing/Ears/Headsets/centcom.rsi sprite: Clothing/Ears/Headsets/centcom.rsi
@@ -37,6 +52,16 @@
name: command headset name: command headset
description: A headset with a commanding channel. description: A headset with a commanding channel.
components: components:
- type: Headset
channels:
- Common
- Command
- Engineering
- Medical
- Science
- Security
- Service
- Supply
- type: Sprite - type: Sprite
sprite: Clothing/Ears/Headsets/command.rsi sprite: Clothing/Ears/Headsets/command.rsi
@@ -46,6 +71,10 @@
name: engineering headset name: engineering headset
description: A headset for engineers to chat while the station burns around them. description: A headset for engineers to chat while the station burns around them.
components: components:
- type: Headset
channels:
- Common
- Engineering
- type: Sprite - type: Sprite
sprite: Clothing/Ears/Headsets/engineering.rsi sprite: Clothing/Ears/Headsets/engineering.rsi
@@ -55,6 +84,10 @@
name: medical headset name: medical headset
description: A headset for the trained staff of the medbay. description: A headset for the trained staff of the medbay.
components: components:
- type: Headset
channels:
- Common
- Medical
- type: Sprite - type: Sprite
sprite: Clothing/Ears/Headsets/medical.rsi sprite: Clothing/Ears/Headsets/medical.rsi
@@ -64,6 +97,11 @@
name: medical research headset name: medical research headset
description: A headset that is a result of the mating between medical and science. description: A headset that is a result of the mating between medical and science.
components: components:
- type: Headset
channels:
- Common
- Medical
- Science
- type: Sprite - type: Sprite
sprite: Clothing/Ears/Headsets/medicalscience.rsi sprite: Clothing/Ears/Headsets/medicalscience.rsi
@@ -91,6 +129,10 @@
name: science headset name: science headset
description: A sciency headset. Like usual. description: A sciency headset. Like usual.
components: components:
- type: Headset
channels:
- Common
- Science
- type: Sprite - type: Sprite
sprite: Clothing/Ears/Headsets/science.rsi sprite: Clothing/Ears/Headsets/science.rsi
@@ -100,6 +142,10 @@
name: security headset name: security headset
description: This is used by your elite security force. description: This is used by your elite security force.
components: components:
- type: Headset
channels:
- Common
- Security
- type: Sprite - type: Sprite
sprite: Clothing/Ears/Headsets/security.rsi sprite: Clothing/Ears/Headsets/security.rsi
@@ -109,5 +155,9 @@
name: service headset name: service headset
description: Headset used by the service staff, tasked with keeping the station full, happy and clean. description: Headset used by the service staff, tasked with keeping the station full, happy and clean.
components: components:
- type: Headset
channels:
- Common
- Service
- type: Sprite - type: Sprite
sprite: Clothing/Ears/Headsets/service.rsi sprite: Clothing/Ears/Headsets/service.rsi

View File

@@ -17,6 +17,16 @@
id: ClothingHeadsetAltCommand id: ClothingHeadsetAltCommand
name: command overear-headset name: command overear-headset
components: components:
- type: Headset
channels:
- Common
- Command
- Engineering
- Medical
- Science
- Security
- Service
- Supply
- type: Sprite - type: Sprite
sprite: Clothing/Ears/Headsets/command.rsi sprite: Clothing/Ears/Headsets/command.rsi
- type: Clothing - type: Clothing
@@ -27,6 +37,10 @@
id: ClothingHeadsetAltMedical id: ClothingHeadsetAltMedical
name: medical overear-headset name: medical overear-headset
components: components:
- type: Headset
channels:
- Common
- Medical
- type: Sprite - type: Sprite
sprite: Clothing/Ears/Headsets/medical.rsi sprite: Clothing/Ears/Headsets/medical.rsi
- type: Clothing - type: Clothing
@@ -37,6 +51,10 @@
id: ClothingHeadsetAltSecurity id: ClothingHeadsetAltSecurity
name: security overear-headset name: security overear-headset
components: components:
- type: Headset
channels:
- Common
- Security
- type: Sprite - type: Sprite
sprite: Clothing/Ears/Headsets/security.rsi sprite: Clothing/Ears/Headsets/security.rsi
- type: Clothing - type: Clothing
@@ -48,6 +66,10 @@
name: syndicate overear-headset name: syndicate overear-headset
description: A syndicate headset that can be used to hear all radio frequencies. Protects ears from flashbangs. description: A syndicate headset that can be used to hear all radio frequencies. Protects ears from flashbangs.
components: components:
- type: Headset
channels:
- Common
- Syndicate
- type: Sprite - type: Sprite
sprite: Clothing/Ears/Headsets/syndicate.rsi sprite: Clothing/Ears/Headsets/syndicate.rsi
- type: Clothing - type: Clothing

View File

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