using Robust.Shared.Replays;
using Robust.Shared.Utility;
using Content.Shared.Popups;
+using Robust.Shared.Map;
+using Content.Shared.Radio.Components;
+using Content.Server.Power.Components;
namespace Content.Server.Radio.EntitySystems;
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly IReplayRecordingManager _replay = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
- [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
// set used to prevent radio feedback loops.
private readonly HashSet<string> _messages = new();
{
if (args.Channel != null && component.Channels.Contains(args.Channel.ID))
{
- SendRadioMessage(uid, args.Message, args.Channel);
+ SendRadioMessage(uid, args.Message, args.Channel, uid);
args.Channel = null; // prevent duplicate messages from other listeners.
}
}
- private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent component, RadioReceiveEvent args)
+ private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent component, ref RadioReceiveEvent args)
{
if (TryComp(uid, out ActorComponent? actor))
_netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.ConnectedClient);
}
- public void SendRadioMessage(EntityUid source, string message, RadioChannelPrototype channel, EntityUid? radioSource = null)
+ /// <summary>
+ /// Send radio message to all active radio listeners
+ /// </summary>
+ /// <param name="messageSource">Entity that spoke the message</param>
+ /// <param name="radioSource">Entity that picked up the message and will send it, e.g. headset</param>
+ public void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource)
{
// TODO if radios ever garble / modify messages, feedback-prevention needs to be handled better than this.
if (!_messages.Add(message))
return;
- var name = TryComp(source, out VoiceMaskComponent? mask) && mask.Enabled
+ var name = TryComp(messageSource, out VoiceMaskComponent? mask) && mask.Enabled
? mask.VoiceName
- : MetaData(source).EntityName;
+ : MetaData(messageSource).EntityName;
name = FormattedMessage.EscapeText(name);
Loc.GetString("chat-radio-message-wrap", ("color", channel.Color), ("channel", $"\\[{channel.LocalizedName}\\]"), ("name", name), ("message", FormattedMessage.EscapeText(message))),
EntityUid.Invalid);
var chatMsg = new MsgChatMessage { Message = chat };
+ var ev = new RadioReceiveEvent(message, messageSource, channel, chatMsg);
- var ev = new RadioReceiveEvent(message, source, channel, chatMsg, radioSource);
- var attemptEv = new RadioReceiveAttemptEvent(message, source, channel, radioSource);
- var sentAtLeastOnce = false;
+ var sourceMapId = Transform(radioSource).MapID;
+ var hasActiveServer = HasActiveServer(sourceMapId, channel.ID);
+ var hasMicro = HasComp<RadioMicrophoneComponent>(radioSource);
- foreach (var radio in EntityQuery<ActiveRadioComponent>())
+ var speakerQuery = GetEntityQuery<RadioSpeakerComponent>();
+ var radioQuery = AllEntityQuery<ActiveRadioComponent, TransformComponent>();
+ var sentAtLeastOnce = false;
+ while (radioQuery.MoveNext(out var receiver, out var radio, out var transform))
{
- var ent = radio.Owner;
- // TODO map/station/range checks?
-
if (!radio.Channels.Contains(channel.ID))
continue;
- RaiseLocalEvent(ent, attemptEv);
+ if (!channel.LongRange && transform.MapID != sourceMapId)
+ continue;
+
+ // don't need telecom server for long range channels or handheld radios and intercoms
+ var needServer = !channel.LongRange && (!hasMicro || !speakerQuery.HasComponent(receiver));
+ if (needServer && !hasActiveServer)
+ continue;
+
+ // check if message can be sent to specific receiver
+ var attemptEv = new RadioReceiveAttemptEvent(channel, radioSource, receiver);
+ RaiseLocalEvent(ref attemptEv);
if (attemptEv.Cancelled)
- {
- attemptEv.Uncancel();
continue;
- }
+
+ // send the message
+ RaiseLocalEvent(receiver, ref ev);
sentAtLeastOnce = true;
- RaiseLocalEvent(ent, ev);
}
if (!sentAtLeastOnce)
- _popupSystem.PopupEntity(Loc.GetString("failed-to-send-message"), source, source, PopupType.MediumCaution);
+ _popup.PopupEntity(Loc.GetString("failed-to-send-message"), messageSource, messageSource, PopupType.MediumCaution);
- if (name != Name(source))
- _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Radio message from {ToPrettyString(source):user} as {name} on {channel.LocalizedName}: {message}");
+ if (name != Name(messageSource))
+ _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Radio message from {ToPrettyString(messageSource):user} as {name} on {channel.LocalizedName}: {message}");
else
- _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Radio message from {ToPrettyString(source):user} on {channel.LocalizedName}: {message}");
+ _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Radio message from {ToPrettyString(messageSource):user} on {channel.LocalizedName}: {message}");
_replay.QueueReplayMessage(chat);
_messages.Remove(message);
}
+
+ /// <inheritdoc cref="TelecomServerComponent"/>
+ private bool HasActiveServer(MapId mapId, string channelId)
+ {
+ var servers = EntityQuery<TelecomServerComponent, EncryptionKeyHolderComponent, ApcPowerReceiverComponent, TransformComponent>();
+ foreach (var (_, keys, power, transform) in servers)
+ {
+ if (transform.MapID == mapId &&
+ power.Powered &&
+ keys.Channels.Contains(channelId))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
}
namespace Content.Server.Radio;
-public sealed class RadioReceiveEvent : EntityEventArgs
+[ByRefEvent]
+public struct RadioReceiveEvent
{
public readonly string Message;
- public readonly EntityUid Source;
+ public readonly EntityUid MessageSource;
public readonly RadioChannelPrototype Channel;
public readonly MsgChatMessage ChatMsg;
- public readonly EntityUid? RadioSource;
- public RadioReceiveEvent(string message, EntityUid source, RadioChannelPrototype channel, MsgChatMessage chatMsg, EntityUid? radioSource)
+ public RadioReceiveEvent(string message, EntityUid messageSource, RadioChannelPrototype channel, MsgChatMessage chatMsg)
{
Message = message;
- Source = source;
+ MessageSource = messageSource;
Channel = channel;
ChatMsg = chatMsg;
- RadioSource = radioSource;
}
}
-public sealed class RadioReceiveAttemptEvent : CancellableEntityEventArgs
+/// <summary>
+/// Use this event to cancel sending messages by doing various checks (e.g. range)
+/// </summary>
+[ByRefEvent]
+public struct RadioReceiveAttemptEvent
{
- public readonly string Message;
- public readonly EntityUid Source;
public readonly RadioChannelPrototype Channel;
- public readonly EntityUid? RadioSource;
+ public readonly EntityUid RadioSource;
+ public readonly EntityUid RadioReceiver;
- public RadioReceiveAttemptEvent(string message, EntityUid source, RadioChannelPrototype channel, EntityUid? radioSource)
+ public bool Cancelled = false;
+
+ public RadioReceiveAttemptEvent(RadioChannelPrototype channel, EntityUid radioSource, EntityUid radioReceiver)
{
- Message = message;
- Source = source;
Channel = channel;
RadioSource = radioSource;
+ RadioReceiver = radioReceiver;
}
}
using Robust.Shared.Prototypes;
-namespace Content.Shared.Radio
+namespace Content.Shared.Radio;
+
+[Prototype("radioChannel")]
+public sealed class RadioChannelPrototype : IPrototype
{
- [Prototype("radioChannel")]
- public sealed class RadioChannelPrototype : IPrototype
- {
- /// <summary>
- /// Human-readable name for the channel.
- /// </summary>
- [DataField("name")] public string Name { get; private set; } = string.Empty;
+ /// <summary>
+ /// Human-readable name for the channel.
+ /// </summary>
+ [DataField("name")]
+ public string Name { get; private set; } = string.Empty;
+
+ [ViewVariables(VVAccess.ReadOnly)]
+ public string LocalizedName => Loc.GetString(Name);
- [ViewVariables(VVAccess.ReadOnly)] public string LocalizedName => Loc.GetString(Name);
+ /// <summary>
+ /// Single-character prefix to determine what channel a message should be sent to.
+ /// </summary>
+ [DataField("keycode")]
+ public char KeyCode { get; private set; } = '\0';
- /// <summary>
- /// Single-character prefix to determine what channel a message should be sent to.
- /// </summary>
- [DataField("keycode")] public char KeyCode { get; private set; } = '\0';
+ [DataField("frequency")]
+ public int Frequency { get; private set; } = 0;
- [DataField("frequency")] public int Frequency { get; private set; } = 0;
+ [DataField("color")]
+ public Color Color { get; private set; } = Color.Lime;
- [DataField("color")] public Color Color { get; private set; } = Color.Lime;
+ [IdDataField, ViewVariables]
+ public string ID { get; } = default!;
- [ViewVariables]
- [IdDataField]
- public string ID { get; } = default!;
- }
+ /// <summary>
+ /// If channel is long range it doesn't require telecommunication server
+ /// and messages can be sent across different stations
+ /// </summary>
+ [DataField("longRange"), ViewVariables]
+ public bool LongRange = false;
}