IListener and IRadio purge (#11980)

This commit is contained in:
Leon Friedrich
2022-11-15 17:09:27 +13:00
committed by GitHub
parent bc525425da
commit 0b5a58001c
48 changed files with 946 additions and 643 deletions

View File

@@ -0,0 +1,17 @@
using Content.Shared.Radio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.Radio.Components;
/// <summary>
/// This component is required to receive radio message events.
/// </summary>
[RegisterComponent]
public sealed class ActiveRadioComponent : Component
{
/// <summary>
/// The channels that this radio is listening on.
/// </summary>
[DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
public HashSet<string> Channels = new();
}

View File

@@ -1,114 +0,0 @@
using System.Linq;
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
{
[RegisterComponent]
[ComponentProtoName("Radio")]
[ComponentReference(typeof(IRadio))]
[ComponentReference(typeof(IListen))]
#pragma warning disable 618
public sealed class HandheldRadioComponent : Component, IListen, IRadio
#pragma warning restore 618
{
private ChatSystem? _chatSystem;
private RadioSystem? _radioSystem;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private bool _radioOn;
[DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
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)]
[DataField("broadcastChannel", customTypeSerializer: typeof(PrototypeIdSerializer<RadioChannelPrototype>))]
public string BroadcastChannel { get; set; } = "Common";
[ViewVariables(VVAccess.ReadWrite)] [DataField("listenRange")] public int ListenRange { get; private set; } = 7;
[ViewVariables(VVAccess.ReadWrite)]
public bool RadioOn
{
get => _radioOn;
private set
{
_radioOn = value;
Dirty();
}
}
protected override void Initialize()
{
base.Initialize();
_radioSystem = EntitySystem.Get<RadioSystem>();
_chatSystem = EntitySystem.Get<ChatSystem>();
RadioOn = false;
}
public void Speak(string message)
{
_chatSystem?.TrySendInGameICMessage(Owner, message, InGameICChatType.Speak, false);
}
public bool Use(EntityUid user)
{
RadioOn = !RadioOn;
var message = Loc.GetString("handheld-radio-component-on-use",
("radioState", Loc.GetString(RadioOn ? "handheld-radio-component-on-state" : "handheld-radio-component-off-state")));
Owner.PopupMessage(user, message);
return true;
}
public bool CanListen(string message, EntityUid source, RadioChannelPrototype? prototype)
{
if (prototype != null && !_channels.Contains(prototype.ID)
|| !_prototypeManager.HasIndex<RadioChannelPrototype>(BroadcastChannel))
{
return false;
}
return RadioOn
&& EntitySystem.Get<SharedInteractionSystem>().InRangeUnobstructed(Owner, source, range: ListenRange);
}
public void Receive(string message, RadioChannelPrototype channel, EntityUid speaker)
{
if (_channels.Contains(channel.ID) && RadioOn)
{
Speak(message);
}
}
public void Listen(string message, EntityUid speaker, RadioChannelPrototype? prototype)
{
// if we can't get the channel, we need to just use the broadcast frequency
if (prototype == null
&& !_prototypeManager.TryIndex(BroadcastChannel, out prototype))
{
return;
}
Broadcast(message, speaker, prototype);
}
public void Broadcast(string message, EntityUid speaker, RadioChannelPrototype channel)
{
_radioSystem?.SpreadMessage(this, speaker, message, channel);
}
}
}

View File

@@ -0,0 +1,25 @@
using Content.Server.Radio.EntitySystems;
using Content.Shared.Inventory;
using Content.Shared.Radio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.Radio.Components;
/// <summary>
/// This component relays radio messages to the parent entity's chat when equipped.
/// </summary>
[RegisterComponent]
[Access(typeof(HeadsetSystem))]
public sealed class HeadsetComponent : Component
{
[DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
public readonly HashSet<string> Channels = new() { "Common" };
[DataField("enabled")]
public bool Enabled = true;
public bool IsEquipped = false;
[DataField("requiredSlot")]
public SlotFlags RequiredSlot = SlotFlags.EARS;
}

View File

@@ -1,17 +0,0 @@
using Content.Shared.Radio;
namespace Content.Server.Radio.Components
{
/// <summary>
/// Interface for objects such as radios meant to have an effect when speech is
/// heard. Requires component reference.
/// </summary>
public interface IListen : IComponent
{
int ListenRange { get; }
bool CanListen(string message, EntityUid source, RadioChannelPrototype? channelPrototype);
void Listen(string message, EntityUid speaker, RadioChannelPrototype? channel);
}
}

View File

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

View File

@@ -0,0 +1,11 @@
namespace Content.Server.Radio.Components;
/// <summary>
/// This component allows an entity to directly translate radio messages into chat messages. Note that this does not
/// automatically add an <see cref="ActiveRadioComponent"/>, which is required to receive radio messages on specific
/// channels.
/// </summary>
[RegisterComponent]
public sealed class IntrinsicRadioReceiverComponent : Component
{
}

View File

@@ -0,0 +1,15 @@
using Content.Shared.Radio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.Radio.Components;
/// <summary>
/// This component allows an entity to directly translate spoken text into radio messages (effectively an intrinsic
/// radio headset).
/// </summary>
[RegisterComponent]
public sealed class IntrinsicRadioTransmitterComponent : Component
{
[DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
public readonly HashSet<string> Channels = new() { "Common" };
}

View File

@@ -0,0 +1,24 @@
using Content.Server.Radio.EntitySystems;
using Content.Shared.Radio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Radio.Components;
/// <summary>
/// Listens for local chat messages and relays them to some radio frequency
/// </summary>
[RegisterComponent]
[Access(typeof(RadioDeviceSystem))]
public sealed class RadioMicrophoneComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("broadcastChannel", customTypeSerializer: typeof(PrototypeIdSerializer<RadioChannelPrototype>))]
public string BroadcastChannel = "Common";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("listenRange")]
public int ListenRange = 4;
[DataField("enabled")]
public bool Enabled = false;
}

View File

@@ -0,0 +1,19 @@
using Content.Server.Radio.EntitySystems;
using Content.Shared.Radio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Server.Radio.Components;
/// <summary>
/// Listens for radio messages and relays them to local chat.
/// </summary>
[RegisterComponent]
[Access(typeof(RadioDeviceSystem))]
public sealed class RadioSpeakerComponent : Component
{
[DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
public HashSet<string> Channels = new () { "Common" };
[DataField("enabled")]
public bool Enabled;
}

View File

@@ -0,0 +1,13 @@
using Content.Server.Radio.EntitySystems;
namespace Content.Server.Radio.Components;
/// <summary>
/// This component is used to tag players that are currently wearing an ACTIVE headset.
/// </summary>
[RegisterComponent]
public sealed class WearingHeadsetComponent : Component
{
[DataField("headset")]
public EntityUid Headset;
}

View File

@@ -0,0 +1,106 @@
using Content.Server.Chat.Systems;
using Content.Server.Radio.Components;
using Content.Shared.Examine;
using Content.Shared.Inventory.Events;
using Content.Shared.Radio;
using Robust.Server.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.Server.Radio.EntitySystems;
public sealed class HeadsetSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly RadioSystem _radio = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HeadsetComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<HeadsetComponent, RadioReceiveEvent>(OnHeadsetReceive);
SubscribeLocalEvent<HeadsetComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<HeadsetComponent, GotUnequippedEvent>(OnGotUnequipped);
SubscribeLocalEvent<WearingHeadsetComponent, EntitySpokeEvent>(OnSpeak);
}
private void OnSpeak(EntityUid uid, WearingHeadsetComponent component, EntitySpokeEvent args)
{
if (args.Channel != null
&& TryComp(component.Headset, out HeadsetComponent? headset)
&& headset.Channels.Contains(args.Channel.ID))
{
_radio.SendRadioMessage(uid, args.Message, args.Channel);
args.Channel = null; // prevent duplicate messages from other listeners.
}
}
private void OnGotEquipped(EntityUid uid, HeadsetComponent component, GotEquippedEvent args)
{
component.IsEquipped = args.SlotFlags.HasFlag(component.RequiredSlot);
if (component.IsEquipped && component.Enabled)
{
EnsureComp<WearingHeadsetComponent>(args.Equipee).Headset = uid;
EnsureComp<ActiveRadioComponent>(uid).Channels.UnionWith(component.Channels);
}
}
private void OnGotUnequipped(EntityUid uid, HeadsetComponent component, GotUnequippedEvent args)
{
component.IsEquipped = false;
RemCompDeferred<ActiveRadioComponent>(uid);
RemCompDeferred<WearingHeadsetComponent>(args.Equipee);
}
public void SetEnabled(EntityUid uid, bool value, HeadsetComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (component.Enabled == value)
return;
if (!value)
{
RemCompDeferred<ActiveRadioComponent>(uid);
if (component.IsEquipped)
RemCompDeferred<WearingHeadsetComponent>(Transform(uid).ParentUid);
}
else if (component.IsEquipped)
{
EnsureComp<WearingHeadsetComponent>(Transform(uid).ParentUid).Headset = uid;
EnsureComp<ActiveRadioComponent>(uid).Channels.UnionWith(component.Channels);
}
}
private void OnHeadsetReceive(EntityUid uid, HeadsetComponent component, RadioReceiveEvent args)
{
if (TryComp(Transform(uid).ParentUid, out ActorComponent? actor))
_netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.ConnectedClient);
}
private void OnExamined(EntityUid uid, HeadsetComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
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.LocalizedName),
("freq", proto.Frequency)));
}
args.PushMarkup(Loc.GetString("examine-headset-chat-prefix", ("prefix", ";")));
}
}

View File

@@ -1,23 +0,0 @@
using Content.Server.Radio.Components;
using Content.Shared.Radio;
using JetBrains.Annotations;
namespace Content.Server.Radio.EntitySystems
{
[UsedImplicitly]
public sealed class ListeningSystem : EntitySystem
{
public void PingListeners(EntityUid source, string message, RadioChannelPrototype? channel)
{
foreach (var listener in EntityManager.EntityQuery<IListen>(true))
{
// TODO: Listening code is hella stinky so please refactor it someone.
// TODO: Map Position distance
if (listener.CanListen(message, source, channel))
{
listener.Listen(message, source, channel);
}
}
}
}
}

View File

@@ -0,0 +1,148 @@
using Content.Server.Chat.Systems;
using Content.Server.Popups;
using Content.Server.Radio.Components;
using Content.Server.Speech;
using Content.Server.Speech.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Radio;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Radio.EntitySystems;
/// <summary>
/// This system handles radio speakers and microphones (which together form a hand-held radio).
/// </summary>
public sealed class RadioDeviceSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly RadioSystem _radio = default!;
// Used to prevent a shitter from using a bunch of radios to spam chat.
private HashSet<(string, EntityUid)> _recentlySent = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RadioMicrophoneComponent, ComponentInit>(OnMicrophoneInit);
SubscribeLocalEvent<RadioMicrophoneComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<RadioMicrophoneComponent, ActivateInWorldEvent>(OnActivateMicrophone);
SubscribeLocalEvent<RadioMicrophoneComponent, ListenEvent>(OnListen);
SubscribeLocalEvent<RadioSpeakerComponent, ComponentInit>(OnSpeakerInit);
SubscribeLocalEvent<RadioSpeakerComponent, ActivateInWorldEvent>(OnActivateSpeaker);
SubscribeLocalEvent<RadioSpeakerComponent, RadioReceiveEvent>(OnReceiveRadio);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
_recentlySent.Clear();
}
#region Component Init
private void OnMicrophoneInit(EntityUid uid, RadioMicrophoneComponent component, ComponentInit args)
{
if (component.Enabled)
EnsureComp<ActiveListenerComponent>(uid).Range = component.ListenRange;
else
RemCompDeferred<ActiveListenerComponent>(uid);
}
private void OnSpeakerInit(EntityUid uid, RadioSpeakerComponent component, ComponentInit args)
{
if (component.Enabled)
EnsureComp<ActiveRadioComponent>(uid).Channels.UnionWith(component.Channels);
else
RemCompDeferred<ActiveRadioComponent>(uid);
}
#endregion
#region Toggling
private void OnActivateMicrophone(EntityUid uid, RadioMicrophoneComponent component, ActivateInWorldEvent args)
{
ToggleRadioMicrophone(uid, args.User, args.Handled, component);
args.Handled = true;
}
private void OnActivateSpeaker(EntityUid uid, RadioSpeakerComponent component, ActivateInWorldEvent args)
{
ToggleRadioSpeaker(uid, args.User, args.Handled, component);
args.Handled = true;
}
public void ToggleRadioMicrophone(EntityUid uid, EntityUid user, bool quiet = false, RadioMicrophoneComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.Enabled = !component.Enabled;
if (!quiet)
{
var state = Loc.GetString(component.Enabled ? "handheld-radio-component-on-state" : "handheld-radio-component-off-state");
var message = Loc.GetString("handheld-radio-component-on-use", ("radioState", state));
_popup.PopupEntity(message, user, Filter.Entities(user));
}
if (component.Enabled)
EnsureComp<ActiveListenerComponent>(uid).Range = component.ListenRange;
else
RemCompDeferred<ActiveListenerComponent>(uid);
}
public void ToggleRadioSpeaker(EntityUid uid, EntityUid user, bool quiet = false, RadioSpeakerComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
component.Enabled = !component.Enabled;
if (!quiet)
{
var state = Loc.GetString(component.Enabled ? "handheld-radio-component-on-state" : "handheld-radio-component-off-state");
var message = Loc.GetString("handheld-radio-component-on-use", ("radioState", state));
_popup.PopupEntity(message, user, Filter.Entities(user));
}
if (component.Enabled)
EnsureComp<ActiveRadioComponent>(uid).Channels.UnionWith(component.Channels);
else
RemCompDeferred<ActiveRadioComponent>(uid);
}
#endregion
private void OnExamine(EntityUid uid, RadioMicrophoneComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
var freq = _protoMan.Index<RadioChannelPrototype>(component.BroadcastChannel).Frequency;
args.PushMarkup(Loc.GetString("handheld-radio-component-on-examine", ("frequency", freq)));
}
private void OnListen(EntityUid uid, RadioMicrophoneComponent component, ListenEvent args)
{
if (HasComp<RadioSpeakerComponent>(args.Source))
return; // no feedback loops please.
if (_recentlySent.Add((args.Message, args.Source)))
_radio.SendRadioMessage(args.Source, args.Message, _protoMan.Index<RadioChannelPrototype>(component.BroadcastChannel));
}
private void OnReceiveRadio(EntityUid uid, RadioSpeakerComponent component, RadioReceiveEvent args)
{
var nameEv = new TransformSpeakerNameEvent(args.Source, Name(args.Source));
RaiseLocalEvent(args.Source, nameEv);
var name = Loc.GetString("speech-name-relay", ("speaker", Name(uid)),
("originalName", nameEv.Name));
var hideGlobalGhostChat = true; // log to chat so people can identity the speaker/source, but avoid clogging ghost chat if there are many radios
_chat.TrySendInGameICMessage(uid, args.Message, InGameICChatType.Speak, false, nameOverride: name, hideGlobalGhostChat:hideGlobalGhostChat);
}
}

View File

@@ -1,52 +1,89 @@
using System.Linq;
using Content.Shared.Examine;
using Content.Server.Chat.Systems;
using Content.Server.Radio.Components;
using Content.Server.Speech;
using Content.Server.VoiceMask;
using Content.Shared.Chat;
using Content.Shared.IdentityManagement;
using Content.Shared.Radio;
using JetBrains.Annotations;
using Content.Shared.Interaction;
using Robust.Server.GameObjects;
using Robust.Shared.Network;
using Robust.Shared.Utility;
namespace Content.Server.Radio.EntitySystems
namespace Content.Server.Radio.EntitySystems;
/// <summary>
/// This system handles radio speakers and microphones (which together form a hand-held radio).
/// </summary>
public sealed class RadioSystem : EntitySystem
{
[UsedImplicitly]
public sealed class RadioSystem : EntitySystem
[Dependency] private readonly INetManager _netMan = default!;
// set used to prevent radio feedback loops.
private readonly HashSet<string> _messages = new();
public override void Initialize()
{
private readonly List<string> _messages = new();
base.Initialize();
SubscribeLocalEvent<IntrinsicRadioReceiverComponent, RadioReceiveEvent>(OnIntrinsicReceive);
SubscribeLocalEvent<IntrinsicRadioTransmitterComponent, EntitySpokeEvent>(OnIntrinsicSpeak);
}
public override void Initialize()
private void OnIntrinsicSpeak(EntityUid uid, IntrinsicRadioTransmitterComponent component, EntitySpokeEvent args)
{
if (args.Channel != null && component.Channels.Contains(args.Channel.ID))
{
base.Initialize();
SubscribeLocalEvent<HandheldRadioComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<HandheldRadioComponent, ActivateInWorldEvent>(OnActivate);
}
private void OnActivate(EntityUid uid, HandheldRadioComponent component, ActivateInWorldEvent args)
{
if (args.Handled)
return;
args.Handled = true;
component.Use(args.User);
}
private void OnExamine(EntityUid uid, HandheldRadioComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
args.PushMarkup(Loc.GetString("handheld-radio-component-on-examine",("frequency", component.BroadcastFrequency)));
}
public void SpreadMessage(IRadio source, EntityUid speaker, string message, RadioChannelPrototype channel)
{
if (_messages.Contains(message)) return;
_messages.Add(message);
foreach (var radio in EntityManager.EntityQuery<IRadio>(true))
{
radio.Receive(message, channel, speaker);
}
_messages.Remove(message);
SendRadioMessage(uid, args.Message, args.Channel);
args.Channel = null; // prevent duplicate messages from other listeners.
}
}
private void OnIntrinsicReceive(EntityUid uid, IntrinsicRadioReceiverComponent component, RadioReceiveEvent args)
{
if (TryComp(uid, out ActorComponent? actor))
_netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.ConnectedClient);
}
public void SendRadioMessage(EntityUid source, string message, RadioChannelPrototype channel)
{
// 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
? Identity.Name(source, EntityManager)
: MetaData(source).EntityName;
name = FormattedMessage.EscapeText(name);
// most radios are relayed to chat, so lets parse the chat message beforehand
var chatMsg = new MsgChatMessage
{
Channel = ChatChannel.Radio,
Message = message,
//Square brackets are added here to avoid issues with escaping
WrappedMessage = Loc.GetString("chat-radio-message-wrap", ("color", channel.Color), ("channel", $"\\[{channel.LocalizedName}\\]"), ("name", name), ("message", FormattedMessage.EscapeText(message)))
};
var ev = new RadioReceiveEvent(message, source, channel, chatMsg);
var attemptEv = new RadioReceiveAttemptEvent(message, source, channel);
foreach (var radio in EntityQuery<ActiveRadioComponent>())
{
// TODO map/station/range checks?
if (!radio.Channels.Contains(channel.ID))
continue;
RaiseLocalEvent(radio.Owner, attemptEv);
if (attemptEv.Cancelled)
{
attemptEv.Uncancel();
continue;
}
RaiseLocalEvent(radio.Owner, ev);
}
_messages.Remove(message);
}
}

View File

@@ -0,0 +1,34 @@
using Content.Shared.Chat;
using Content.Shared.Radio;
namespace Content.Server.Radio;
public sealed class RadioReceiveEvent : EntityEventArgs
{
public readonly string Message;
public readonly EntityUid Source;
public readonly RadioChannelPrototype Channel;
public readonly MsgChatMessage ChatMsg;
public RadioReceiveEvent(string message, EntityUid source, RadioChannelPrototype channel, MsgChatMessage chatMsg)
{
Message = message;
Source = source;
Channel = channel;
ChatMsg = chatMsg;
}
}
public sealed class RadioReceiveAttemptEvent : CancellableEntityEventArgs
{
public readonly string Message;
public readonly EntityUid Source;
public readonly RadioChannelPrototype Channel;
public RadioReceiveAttemptEvent(string message, EntityUid source, RadioChannelPrototype channel)
{
Message = message;
Source = source;
Channel = channel;
}
}