diff --git a/Content.Client/Radio/EntitySystems/RadioDeviceSystem.cs b/Content.Client/Radio/EntitySystems/RadioDeviceSystem.cs
index 505c4d6503..3c8954abce 100644
--- a/Content.Client/Radio/EntitySystems/RadioDeviceSystem.cs
+++ b/Content.Client/Radio/EntitySystems/RadioDeviceSystem.cs
@@ -6,16 +6,12 @@ using Robust.Client.GameObjects;
namespace Content.Client.Radio.EntitySystems;
+///
public sealed partial class RadioDeviceSystem : SharedRadioDeviceSystem
{
[Dependency] private UserInterfaceSystem _ui = default!;
- ///
- public override void Initialize()
- {
- SubscribeLocalEvent(OnAfterHandleState);
- }
-
+ [SubscribeLocalEvent]
private void OnAfterHandleState(Entity ent, ref AfterAutoHandleStateEvent args)
{
if (_ui.TryGetOpenUi(ent.Owner, IntercomUiKey.Key, out var bui))
diff --git a/Content.Client/Radio/EntitySystems/RadioSystem.cs b/Content.Client/Radio/EntitySystems/RadioSystem.cs
new file mode 100644
index 0000000000..70f6305e14
--- /dev/null
+++ b/Content.Client/Radio/EntitySystems/RadioSystem.cs
@@ -0,0 +1,6 @@
+using Content.Shared.Radio.EntitySystems;
+
+namespace Content.Client.Radio.EntitySystems;
+
+///
+public sealed partial class RadioSystem : SharedRadioSystem;
diff --git a/Content.Server/Animals/Systems/ParrotMemorySystem.cs b/Content.Server/Animals/Systems/ParrotMemorySystem.cs
index 50aa89267f..728bca5123 100644
--- a/Content.Server/Animals/Systems/ParrotMemorySystem.cs
+++ b/Content.Server/Animals/Systems/ParrotMemorySystem.cs
@@ -2,12 +2,12 @@ using Content.Server.Administration.Logs;
using Content.Server.Administration.Systems;
using Content.Server.Animals.Components;
using Content.Server.Mind;
-using Content.Server.Radio;
using Content.Server.Vocalization.Systems;
using Content.Shared.Animals.Components;
using Content.Shared.Animals.Systems;
using Content.Shared.Database;
using Content.Shared.Mobs.Systems;
+using Content.Shared.Radio;
using Content.Shared.Speech;
using Content.Shared.Speech.Components;
using Content.Shared.Whitelist;
diff --git a/Content.Server/Emp/EmpSystem.cs b/Content.Server/Emp/EmpSystem.cs
index 38ab09e1cf..46fc120402 100644
--- a/Content.Server/Emp/EmpSystem.cs
+++ b/Content.Server/Emp/EmpSystem.cs
@@ -1,7 +1,7 @@
using Content.Server.Power.EntitySystems;
-using Content.Server.Radio;
using Content.Server.SurveillanceCamera;
using Content.Shared.Emp;
+using Content.Shared.Radio;
namespace Content.Server.Emp;
diff --git a/Content.Server/Radio/EntitySystems/JammerSystem.cs b/Content.Server/Radio/EntitySystems/JammerSystem.cs
index d43c7baa0a..4cef9ad9bc 100644
--- a/Content.Server/Radio/EntitySystems/JammerSystem.cs
+++ b/Content.Server/Radio/EntitySystems/JammerSystem.cs
@@ -1,5 +1,7 @@
+using Content.Shared.Radio;
using Content.Shared.Radio.EntitySystems;
using Content.Shared.Radio.Components;
+using Content.Shared.FixedPoint;
namespace Content.Server.Radio.EntitySystems;
@@ -27,7 +29,7 @@ public sealed partial class JammerSystem : SharedJammerSystem
args.Cancelled = true;
}
- private bool ShouldCancel(EntityUid sourceUid, int frequency)
+ private bool ShouldCancel(EntityUid sourceUid, FixedPoint2 frequency)
{
var source = Transform(sourceUid).Coordinates;
var query = EntityQueryEnumerator();
diff --git a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs
index 04d516a983..b1e21772a5 100644
--- a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs
+++ b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs
@@ -1,248 +1,6 @@
-using System.Linq;
-using Content.Server.Chat.Systems;
-using Content.Server.Interaction;
-using Content.Server.Popups;
-using Content.Server.Power.EntitySystems;
-using Content.Shared.Chat;
-using Content.Shared.Examine;
-using Content.Shared.Interaction;
-using Content.Shared.Power;
-using Content.Shared.Radio;
-using Content.Shared.Radio.Components;
using Content.Shared.Radio.EntitySystems;
-using Content.Shared.Speech;
-using Content.Shared.Speech.Components;
-using Robust.Shared.Prototypes;
namespace Content.Server.Radio.EntitySystems;
-///
-/// This system handles radio speakers and microphones (which together form a hand-held radio).
-///
-public sealed partial class RadioDeviceSystem : SharedRadioDeviceSystem
-{
- [Dependency] private PopupSystem _popup = default!;
- [Dependency] private ChatSystem _chat = default!;
- [Dependency] private RadioSystem _radio = default!;
- [Dependency] private InteractionSystem _interaction = default!;
- [Dependency] private SharedAppearanceSystem _appearance = default!;
-
- // Used to prevent a shitter from using a bunch of radios to spam chat.
- private HashSet<(string, EntityUid, RadioChannelPrototype)> _recentlySent = new();
-
- public override void Initialize()
- {
- base.Initialize();
- SubscribeLocalEvent(OnMicrophoneInit);
- SubscribeLocalEvent(OnExamine);
- SubscribeLocalEvent(OnActivateMicrophone);
- SubscribeLocalEvent(OnListen);
- SubscribeLocalEvent(OnAttemptListen);
- SubscribeLocalEvent(OnPowerChanged);
-
- SubscribeLocalEvent(OnSpeakerInit);
- SubscribeLocalEvent(OnActivateSpeaker);
- SubscribeLocalEvent(OnReceiveRadio);
-
- SubscribeLocalEvent(OnIntercomEncryptionChannelsChanged);
- SubscribeLocalEvent(OnToggleIntercomMic);
- SubscribeLocalEvent(OnToggleIntercomSpeaker);
- SubscribeLocalEvent(OnSelectIntercomChannel);
- }
-
- 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(uid).Range = component.ListenRange;
- else
- RemCompDeferred(uid);
- }
-
- private void OnSpeakerInit(EntityUid uid, RadioSpeakerComponent component, ComponentInit args)
- {
- if (component.Enabled)
- EnsureComp(uid).Channels.UnionWith(component.Channels);
- else
- RemCompDeferred(uid);
- }
- #endregion
-
- #region Toggling
- private void OnActivateMicrophone(EntityUid uid, RadioMicrophoneComponent component, ActivateInWorldEvent args)
- {
- if (!args.Complex)
- return;
-
- if (!component.ToggleOnInteract)
- return;
-
- ToggleRadioMicrophone(uid, args.User, args.Handled, component);
- args.Handled = true;
- }
-
- private void OnActivateSpeaker(EntityUid uid, RadioSpeakerComponent component, ActivateInWorldEvent args)
- {
- if (!args.Complex)
- return;
-
- if (!component.ToggleOnInteract)
- return;
-
- ToggleRadioSpeaker(uid, args.User, args.Handled, component);
- args.Handled = true;
- }
- private void OnPowerChanged(EntityUid uid, RadioMicrophoneComponent component, ref PowerChangedEvent args)
- {
- if (args.Powered)
- return;
- SetMicrophoneEnabled(uid, null, false, true, component);
- }
-
-
- public override void SetMicrophoneEnabled(EntityUid uid, EntityUid? user, bool enabled, bool quiet = false, RadioMicrophoneComponent? component = null)
- {
- if (!Resolve(uid, ref component, false))
- return;
-
- if (component.PowerRequired && !this.IsPowered(uid, EntityManager))
- return;
-
- component.Enabled = enabled;
-
- if (!quiet && user != null)
- {
- 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.Value, user.Value);
- }
-
- _appearance.SetData(uid, RadioDeviceVisuals.Broadcasting, component.Enabled);
- if (component.Enabled)
- EnsureComp(uid).Range = component.ListenRange;
- else
- RemCompDeferred(uid);
- }
-
- #endregion
-
- private void OnExamine(EntityUid uid, RadioMicrophoneComponent component, ExaminedEvent args)
- {
- if (!args.IsInDetailsRange)
- return;
-
- var proto = ProtoMan.Index(component.BroadcastChannel);
-
- using (args.PushGroup(nameof(RadioMicrophoneComponent)))
- {
- args.PushMarkup(Loc.GetString("handheld-radio-component-on-examine", ("frequency", proto.Frequency)));
- args.PushMarkup(Loc.GetString("handheld-radio-component-chennel-examine",
- ("channel", proto.LocalizedName)));
- }
- }
-
- private void OnListen(EntityUid uid, RadioMicrophoneComponent component, ListenEvent args)
- {
- if (HasComp(args.Source))
- return; // no feedback loops please.
-
- var channel = ProtoMan.Index(component.BroadcastChannel)!;
- if (_recentlySent.Add((args.Message, args.Source, channel)))
- _radio.SendRadioMessage(args.Source, args.Message, channel, uid);
- }
-
- private void OnAttemptListen(EntityUid uid, RadioMicrophoneComponent component, ListenAttemptEvent args)
- {
- if (component.PowerRequired && !this.IsPowered(uid, EntityManager)
- || component.UnobstructedRequired && !_interaction.InRangeUnobstructed(args.Source, uid, 0))
- {
- args.Cancel();
- }
- }
-
- private void OnReceiveRadio(EntityUid uid, RadioSpeakerComponent component, ref RadioReceiveEvent args)
- {
- if (uid == args.RadioSource)
- return;
-
- var nameEv = new TransformSpeakerNameEvent(args.MessageSource, Name(args.MessageSource));
- RaiseLocalEvent(args.MessageSource, nameEv);
-
- var name = Loc.GetString("speech-name-relay",
- ("speaker", Name(uid)),
- ("originalName", nameEv.VoiceName));
-
- // 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.Whisper, ChatTransmitRange.GhostRangeLimit, nameOverride: name, checkRadioPrefix: false);
- }
-
- private void OnIntercomEncryptionChannelsChanged(Entity ent, ref EncryptionChannelsChangedEvent args)
- {
- ent.Comp.SupportedChannels = args.Component.Channels.Select(p => new ProtoId(p)).ToList();
-
- var channel = args.Component.DefaultChannel;
- if (ent.Comp.CurrentChannel != null && ent.Comp.SupportedChannels.Contains(ent.Comp.CurrentChannel.Value))
- channel = ent.Comp.CurrentChannel;
-
- SetIntercomChannel(ent, channel);
- }
-
- private void OnToggleIntercomMic(Entity ent, ref ToggleIntercomMicMessage args)
- {
- if (ent.Comp.RequiresPower && !this.IsPowered(ent, EntityManager))
- return;
-
- SetMicrophoneEnabled(ent, args.Actor, args.Enabled, true);
- ent.Comp.MicrophoneEnabled = args.Enabled;
- Dirty(ent);
- }
-
- private void OnToggleIntercomSpeaker(Entity ent, ref ToggleIntercomSpeakerMessage args)
- {
- if (ent.Comp.RequiresPower && !this.IsPowered(ent, EntityManager))
- return;
-
- SetSpeakerEnabled(ent, args.Actor, args.Enabled, true);
- ent.Comp.SpeakerEnabled = args.Enabled;
- Dirty(ent);
- }
-
- private void OnSelectIntercomChannel(Entity ent, ref SelectIntercomChannelMessage args)
- {
- if (ent.Comp.RequiresPower && !this.IsPowered(ent, EntityManager))
- return;
-
- if (!ProtoMan.HasIndex(args.Channel) || !ent.Comp.SupportedChannels.Contains(args.Channel))
- return;
-
- SetIntercomChannel(ent, args.Channel);
- }
-
- private void SetIntercomChannel(Entity ent, ProtoId? channel)
- {
- ent.Comp.CurrentChannel = channel;
-
- if (channel == null)
- {
- SetSpeakerEnabled(ent, null, false);
- SetMicrophoneEnabled(ent, null, false);
- ent.Comp.MicrophoneEnabled = false;
- ent.Comp.SpeakerEnabled = false;
- Dirty(ent);
- return;
- }
-
- if (TryComp(ent, out var mic))
- mic.BroadcastChannel = channel.Value;
- if (TryComp(ent, out var speaker))
- speaker.Channels = new() { channel.Value };
- Dirty(ent);
- }
-}
+///
+public sealed partial class RadioDeviceSystem : SharedRadioDeviceSystem;
diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs
index e512715e6e..33c18af42d 100644
--- a/Content.Server/Radio/EntitySystems/RadioSystem.cs
+++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs
@@ -7,21 +7,19 @@ using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Radio;
using Content.Shared.Radio.Components;
+using Content.Shared.Radio.EntitySystems;
using Content.Shared.Speech;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
-using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Replays;
using Robust.Shared.Utility;
namespace Content.Server.Radio.EntitySystems;
-///
-/// This system handles intrinsic radios and the general process of converting radio messages into chat messages.
-///
-public sealed partial class RadioSystem : EntitySystem
+///
+public sealed partial class RadioSystem : SharedRadioSystem
{
[Dependency] private INetManager _netMan = default!;
[Dependency] private IReplayRecordingManager _replay = default!;
@@ -74,20 +72,8 @@ public sealed partial class RadioSystem : EntitySystem
_netMan.ServerSendMessage(msg, actor.PlayerSession.Channel);
}
- ///
- /// Send radio message to all active radio listeners
- ///
- public void SendRadioMessage(EntityUid messageSource, string message, ProtoId channel, EntityUid radioSource, bool escapeMarkup = true)
- {
- SendRadioMessage(messageSource, message, ProtoMan.Index(channel), radioSource, escapeMarkup: escapeMarkup);
- }
-
- ///
- /// Send radio message to all active radio listeners
- ///
- /// Entity that spoke the message
- /// Entity that picked up the message and will send it, e.g. headset
- public void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource, bool escapeMarkup = true)
+ ///
+ public override void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource, bool escapeMarkup = true)
{
// TODO if radios ever garble / modify messages, feedback-prevention needs to be handled better than this.
if (!_messages.Add(message))
diff --git a/Content.Server/StationEvents/Events/SolarFlareRule.cs b/Content.Server/StationEvents/Events/SolarFlareRule.cs
index 24254e2bc1..bab0618835 100644
--- a/Content.Server/StationEvents/Events/SolarFlareRule.cs
+++ b/Content.Server/StationEvents/Events/SolarFlareRule.cs
@@ -1,14 +1,12 @@
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.Radio;
-using Robust.Shared.Random;
using Content.Server.Light.EntitySystems;
-using Content.Server.Light.Components;
using Content.Server.StationEvents.Components;
-using Content.Shared.Radio.Components;
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.GameTicking.Components;
using Content.Shared.Light.Components;
+using Content.Shared.Radio;
+using Content.Shared.Radio.Components;
+using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
diff --git a/Content.Shared/Radio/Components/RadioJammerComponent.cs b/Content.Shared/Radio/Components/RadioJammerComponent.cs
index af4f9e45c8..4a25cf479c 100644
--- a/Content.Shared/Radio/Components/RadioJammerComponent.cs
+++ b/Content.Shared/Radio/Components/RadioJammerComponent.cs
@@ -1,3 +1,4 @@
+using Content.Shared.FixedPoint;
using Robust.Shared.Serialization;
using Robust.Shared.GameStates;
@@ -50,7 +51,7 @@ public sealed partial class RadioJammerComponent : Component
/// Frequencies that are NOT jammed by this jammer.
///
[DataField]
- public HashSet FrequenciesExcluded = [];
+ public HashSet FrequenciesExcluded = [];
///
/// Index of the currently selected setting.
diff --git a/Content.Shared/Radio/Components/RadioMicrophoneComponent.cs b/Content.Shared/Radio/Components/RadioMicrophoneComponent.cs
index 25f4100212..fdbe42eca8 100644
--- a/Content.Shared/Radio/Components/RadioMicrophoneComponent.cs
+++ b/Content.Shared/Radio/Components/RadioMicrophoneComponent.cs
@@ -6,35 +6,45 @@ using Robust.Shared.Prototypes;
namespace Content.Shared.Radio.Components;
///
-/// Listens for local chat messages and relays them to some radio frequency
+/// Listens for local chat messages and relays them to some radio frequency.
///
-[RegisterComponent, NetworkedComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedRadioDeviceSystem))]
public sealed partial class RadioMicrophoneComponent : Component
{
- [DataField]
+ ///
+ /// Radio channel on which local speech is broadcast.
+ ///
+ [DataField, AutoNetworkedField]
public ProtoId BroadcastChannel = SharedChatSystem.CommonChannel;
+ ///
+ /// Maximum distance from the microphone at which speech is heard.
+ ///
[DataField]
public int ListenRange = 4;
- [DataField]
- public bool Enabled = false;
-
- [DataField]
- public bool PowerRequired = false;
+ ///
+ /// Whether the microphone is currently broadcasting.
+ ///
+ [DataField, AutoNetworkedField]
+ public bool Enabled;
///
- /// Whether or not interacting with this entity
- /// toggles it on or off.
+ /// Whether the microphone requires power to operate.
+ ///
+ [DataField]
+ public bool PowerRequired;
+
+ ///
+ /// Whether interacting with this entity toggles it on/off, or not.
///
[DataField]
public bool ToggleOnInteract = true;
///
- /// Whether or not the speaker must have an
- /// unobstructed path to the radio to speak
+ /// Whether the speaker must have an unobstructed path to the radio to speak, or now.
///
[DataField]
- public bool UnobstructedRequired = false;
+ public bool UnobstructedRequired;
}
diff --git a/Content.Shared/Radio/Components/RadioSpeakerComponent.cs b/Content.Shared/Radio/Components/RadioSpeakerComponent.cs
index b7dff28def..891f60c408 100644
--- a/Content.Shared/Radio/Components/RadioSpeakerComponent.cs
+++ b/Content.Shared/Radio/Components/RadioSpeakerComponent.cs
@@ -6,22 +6,27 @@ using Robust.Shared.Prototypes;
namespace Content.Shared.Radio.Components;
///
-/// Listens for radio messages and relays them to local chat.
+/// Listens for radio messages and relays them to local chat.
///
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedRadioDeviceSystem))]
public sealed partial class RadioSpeakerComponent : Component
{
///
- /// Whether or not interacting with this entity
- /// toggles it on or off.
+ /// Whether interacting with this entity toggles it on/off, or not.
///
[DataField]
public bool ToggleOnInteract = true;
- [DataField]
+ ///
+ /// Radio channels from which messages are received.
+ ///
+ [DataField, AutoNetworkedField]
public HashSet> Channels = new() { SharedChatSystem.CommonChannel };
+ ///
+ /// Whether the speaker is currently receiving radio messages.
+ ///
[DataField, AutoNetworkedField]
public bool Enabled;
}
diff --git a/Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs b/Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs
index 4c8f72f543..2987f0b986 100644
--- a/Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs
+++ b/Content.Shared/Radio/EntitySystems/EncryptionKeySystem.cs
@@ -219,7 +219,7 @@ public sealed partial class EncryptionKeySystem : EntitySystem
("color", proto.Color),
("key", key),
("id", proto.LocalizedName),
- ("freq", proto.Frequency / 10f)));
+ ("freq", proto.Frequency)));
}
if (defaultChannel != null && ProtoMan.TryIndex(defaultChannel, out proto))
diff --git a/Content.Shared/Radio/EntitySystems/SharedRadioDeviceSystem.cs b/Content.Shared/Radio/EntitySystems/SharedRadioDeviceSystem.cs
index bdeb453bc6..b6e1a9ad89 100644
--- a/Content.Shared/Radio/EntitySystems/SharedRadioDeviceSystem.cs
+++ b/Content.Shared/Radio/EntitySystems/SharedRadioDeviceSystem.cs
@@ -1,53 +1,360 @@
+using System.Linq;
+using Content.Shared.Chat;
+using Content.Shared.Examine;
+using Content.Shared.Interaction;
using Content.Shared.Popups;
+using Content.Shared.Power;
+using Content.Shared.Power.EntitySystems;
using Content.Shared.Radio.Components;
+using Content.Shared.Speech;
+using Content.Shared.Speech.Components;
+
+using Robust.Shared.Prototypes;
namespace Content.Shared.Radio.EntitySystems;
+///
+/// This system handles radio speakers and microphones (which together form a hand-held radio).
+///
public abstract partial class SharedRadioDeviceSystem : EntitySystem
{
- [Dependency] private SharedPopupSystem _popup = default!;
[Dependency] private SharedAppearanceSystem _appearance = default!;
+ [Dependency] private SharedPopupSystem _popup = default!;
+ [Dependency] private SharedChatSystem _chat = default!;
+ [Dependency] private SharedRadioSystem _radio = default!;
+ [Dependency] private SharedPowerReceiverSystem _power = default!;
+ [Dependency] private SharedInteractionSystem _interaction = default!;
+
+ // Used to prevent a shitter from using a bunch of radios to spam chat.
+ private readonly HashSet<(string, EntityUid, RadioChannelPrototype)> _recentlySent = [];
+
+ ///
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ _recentlySent.Clear();
+ }
+
+ #region Component Init
+
+ [SubscribeLocalEvent]
+ private void OnMicrophoneInit(Entity ent, ref ComponentInit args)
+ {
+ if (ent.Comp.Enabled)
+ EnsureComp(ent).Range = ent.Comp.ListenRange;
+ else
+ RemCompDeferred(ent);
+ }
+
+ [SubscribeLocalEvent]
+ private void OnSpeakerInit(Entity ent, ref ComponentInit args)
+ {
+ if (ent.Comp.Enabled)
+ EnsureComp(ent).Channels.UnionWith(ent.Comp.Channels);
+ else
+ RemCompDeferred(ent);
+ }
+
+ #endregion
#region Toggling
- public void ToggleRadioMicrophone(EntityUid uid, EntityUid user, bool quiet = false, RadioMicrophoneComponent? component = null)
+
+ [SubscribeLocalEvent]
+ private void OnActivateMicrophone(Entity ent, ref ActivateInWorldEvent args)
{
- if (!Resolve(uid, ref component))
+ if (!args.Complex)
return;
- SetMicrophoneEnabled(uid, user, !component.Enabled, quiet, component);
+ if (!ent.Comp.ToggleOnInteract)
+ return;
+
+ ToggleRadioMicrophone(ent.AsNullable(), args.User, args.Handled);
+ args.Handled = true;
}
- public virtual void SetMicrophoneEnabled(EntityUid uid, EntityUid? user, bool enabled, bool quiet = false, RadioMicrophoneComponent? component = null) { }
-
- public void ToggleRadioSpeaker(EntityUid uid, EntityUid user, bool quiet = false, RadioSpeakerComponent? component = null)
+ [SubscribeLocalEvent]
+ private void OnActivateSpeaker(Entity ent, ref ActivateInWorldEvent args)
{
- if (!Resolve(uid, ref component))
+ if (!args.Complex)
return;
- SetSpeakerEnabled(uid, user, !component.Enabled, quiet, component);
+ if (!ent.Comp.ToggleOnInteract)
+ return;
+
+ ToggleRadioSpeaker(ent.AsNullable(), args.User, args.Handled);
+ args.Handled = true;
}
- public void SetSpeakerEnabled(EntityUid uid, EntityUid? user, bool enabled, bool quiet = false, RadioSpeakerComponent? component = null)
+ [SubscribeLocalEvent]
+ private void OnPowerChanged(Entity ent, ref PowerChangedEvent args)
{
- if (!Resolve(uid, ref component))
+ if (args.Powered)
return;
- component.Enabled = enabled;
- Dirty(uid, component);
+ SetMicrophoneEnabled(ent.AsNullable(), null, false, true);
+ }
+
+ ///
+ /// Enables or disables a radio microphone.
+ ///
+ /// The entity with the microphone.
+ /// The entity toggling the microphone, if any.
+ /// Whether the microphone should be enabled.
+ /// Whether to suppress the user-facing popup.
+ public void SetMicrophoneEnabled(
+ Entity ent,
+ EntityUid? user,
+ bool enabled,
+ bool quiet = false
+ )
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return;
+
+ if (ent.Comp.PowerRequired && !_power.IsPowered(ent.Owner))
+ return;
+
+ ent.Comp.Enabled = enabled;
+ Dirty(ent);
if (!quiet && user != null)
{
- var state = Loc.GetString(component.Enabled ? "handheld-radio-component-on-state" : "handheld-radio-component-off-state");
+ var state = Loc.GetString(ent.Comp.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.Value, user.Value);
}
- _appearance.SetData(uid, RadioDeviceVisuals.Speaker, component.Enabled);
- if (component.Enabled)
- EnsureComp(uid).Channels.UnionWith(component.Channels);
+ _appearance.SetData(ent, RadioDeviceVisuals.Broadcasting, ent.Comp.Enabled);
+ if (ent.Comp.Enabled)
+ EnsureComp(ent).Range = ent.Comp.ListenRange;
else
- RemCompDeferred(uid);
+ RemCompDeferred(ent);
}
+
#endregion
+
+ #region Toggling
+
+ ///
+ /// Toggles a radio microphone.
+ ///
+ /// The entity with the microphone.
+ /// The entity toggling the microphone.
+ /// Whether to suppress the user-facing popup.
+ public void ToggleRadioMicrophone(
+ Entity ent,
+ EntityUid user,
+ bool quiet = false
+ )
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return;
+
+ SetMicrophoneEnabled(ent, user, !ent.Comp.Enabled, quiet);
+ }
+
+ ///
+ /// Toggles a radio speaker.
+ ///
+ /// The entity with the speaker.
+ /// The entity toggling the speaker.
+ /// Whether to suppress the user-facing popup.
+ public void ToggleRadioSpeaker(
+ Entity ent,
+ EntityUid user,
+ bool quiet = false
+ )
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return;
+
+ SetSpeakerEnabled(ent, user, !ent.Comp.Enabled, quiet);
+ }
+
+ ///
+ /// Enables or disables a radio speaker.
+ ///
+ /// The entity with the speaker.
+ /// The entity toggling the speaker, if any.
+ /// Whether the speaker should be enabled.
+ /// Whether to suppress the user-facing popup.
+ public void SetSpeakerEnabled(
+ Entity ent,
+ EntityUid? user,
+ bool enabled,
+ bool quiet = false
+ )
+ {
+ if (!Resolve(ent, ref ent.Comp, false))
+ return;
+
+ ent.Comp.Enabled = enabled;
+ Dirty(ent);
+
+ if (!quiet && user != null)
+ {
+ var state = Loc.GetString(ent.Comp.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.Value, user.Value);
+ }
+
+ _appearance.SetData(ent, RadioDeviceVisuals.Speaker, ent.Comp.Enabled);
+ if (ent.Comp.Enabled)
+ EnsureComp(ent).Channels.UnionWith(ent.Comp.Channels);
+ else
+ RemCompDeferred(ent);
+ }
+
+ #endregion
+
+ [SubscribeLocalEvent]
+ private void OnExamine(Entity ent, ref ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange)
+ return;
+
+ var proto = ProtoMan.Index(ent.Comp.BroadcastChannel);
+
+ using (args.PushGroup(nameof(RadioMicrophoneComponent)))
+ {
+ args.PushMarkup(Loc.GetString("radio-microphone-component-examine",
+ ("color", proto.Color),
+ ("channel", proto.LocalizedName),
+ ("frequency", proto.Frequency)));
+ }
+ }
+
+ [SubscribeLocalEvent]
+ private void OnListen(Entity ent, ref ListenEvent args)
+ {
+ if (HasComp(args.Source))
+ return; // no feedback loops please.
+
+ var channel = ProtoMan.Index(ent.Comp.BroadcastChannel);
+ if (_recentlySent.Add((args.Message, args.Source, channel)))
+ _radio.SendRadioMessage(args.Source, args.Message, channel, ent.Owner);
+ }
+
+ [SubscribeLocalEvent]
+ private void OnAttemptListen(Entity ent, ref ListenAttemptEvent args)
+ {
+ if (ent.Comp.PowerRequired && !_power.IsPowered(ent.Owner)
+ || ent.Comp.UnobstructedRequired && !_interaction.InRangeUnobstructed(args.Source, ent.Owner, 0))
+ {
+ args.Cancel();
+ }
+ }
+
+ [SubscribeLocalEvent]
+ private void OnReceiveRadio(Entity ent, ref RadioReceiveEvent args)
+ {
+ if (ent.Owner == args.RadioSource)
+ return;
+
+ var nameEv = new TransformSpeakerNameEvent(args.MessageSource, Name(args.MessageSource));
+ RaiseLocalEvent(args.MessageSource, nameEv);
+
+ var name = Loc.GetString("speech-name-relay",
+ ("speaker", Name(ent.Owner)),
+ ("originalName", nameEv.VoiceName));
+
+ // log to chat so people can identity the speaker/source, but avoid clogging ghost chat if there are many radios
+ _chat.TrySendInGameICMessage(ent.Owner,
+ args.Message,
+ InGameICChatType.Whisper,
+ ChatTransmitRange.GhostRangeLimit,
+ nameOverride: name,
+ checkRadioPrefix: false);
+ }
+
+ [SubscribeLocalEvent]
+ private void OnIntercomEncryptionChannelsChanged(
+ Entity ent,
+ ref EncryptionChannelsChangedEvent args
+ )
+ {
+ ent.Comp.SupportedChannels =
+ args.Component.Channels.Select(p => new ProtoId(p)).ToList();
+
+ var channel = args.Component.DefaultChannel;
+ if (ent.Comp.CurrentChannel != null && ent.Comp.SupportedChannels.Contains(ent.Comp.CurrentChannel.Value))
+ channel = ent.Comp.CurrentChannel;
+
+ SetIntercomChannel(ent, channel);
+ }
+
+ [SubscribeLocalEvent]
+ private void OnToggleIntercomMic(Entity ent, ref ToggleIntercomMicMessage args)
+ {
+ if (ent.Comp.RequiresPower && !_power.IsPowered(ent.Owner))
+ return;
+
+ SetMicrophoneEnabled(ent.Owner, args.Actor, args.Enabled, true);
+ ent.Comp.MicrophoneEnabled = args.Enabled;
+ Dirty(ent);
+ }
+
+ [SubscribeLocalEvent]
+ private void OnToggleIntercomSpeaker(Entity ent, ref ToggleIntercomSpeakerMessage args)
+ {
+ if (ent.Comp.RequiresPower && !_power.IsPowered(ent.Owner))
+ return;
+
+ SetSpeakerEnabled(ent.Owner, args.Actor, args.Enabled, true);
+ ent.Comp.SpeakerEnabled = args.Enabled;
+ Dirty(ent);
+ }
+
+ [SubscribeLocalEvent]
+ private void OnSelectIntercomChannel(Entity ent, ref SelectIntercomChannelMessage args)
+ {
+ if (ent.Comp.RequiresPower && !_power.IsPowered(ent.Owner))
+ return;
+
+ if (!ProtoMan.HasIndex(args.Channel) ||
+ !ent.Comp.SupportedChannels.Contains(args.Channel))
+ return;
+
+ SetIntercomChannel(ent, args.Channel);
+ }
+
+ private void SetIntercomChannel(Entity ent, ProtoId? channel)
+ {
+ ent.Comp.CurrentChannel = channel;
+
+ if (TryComp(ent, out var mic))
+ {
+ if (channel == null)
+ {
+ SetMicrophoneEnabled(ent.Owner, null, false);
+ ent.Comp.MicrophoneEnabled = false;
+ }
+ else
+ {
+ mic.BroadcastChannel = channel.Value;
+ Dirty(ent, mic);
+ }
+ }
+
+ if (TryComp(ent, out var speaker))
+ {
+ if (channel == null)
+ {
+ SetSpeakerEnabled(ent.Owner, null, false);
+ ent.Comp.SpeakerEnabled = false;
+ }
+ else
+ {
+ speaker.Channels = [channel.Value];
+ Dirty(ent, speaker);
+ }
+ }
+
+ Dirty(ent);
+ }
}
diff --git a/Content.Shared/Radio/EntitySystems/SharedRadioSystem.cs b/Content.Shared/Radio/EntitySystems/SharedRadioSystem.cs
new file mode 100644
index 0000000000..6a46818597
--- /dev/null
+++ b/Content.Shared/Radio/EntitySystems/SharedRadioSystem.cs
@@ -0,0 +1,41 @@
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Radio.EntitySystems;
+
+///
+/// This system handles intrinsic radios and the general process of converting radio messages into chat messages.
+///
+public abstract partial class SharedRadioSystem : EntitySystem
+{
+ ///
+ /// Send radio message to all active radio listeners.
+ ///
+ [PublicAPI]
+ public void SendRadioMessage(EntityUid messageSource,
+ string message,
+ ProtoId channel,
+ EntityUid radioSource,
+ bool escapeMarkup = true)
+ {
+ SendRadioMessage(messageSource, message, ProtoMan.Index(channel), radioSource, escapeMarkup: escapeMarkup);
+ }
+
+ ///
+ /// Sends a radio message to all active radio listeners.
+ ///
+ /// Entity that spoke the message.
+ /// Message to send over the radio.
+ /// Radio channel to send the message on.
+ /// Entity transmitting the message.
+ /// Whether markup in the message should be escaped.
+ [PublicAPI]
+ public virtual void SendRadioMessage(EntityUid messageSource,
+ string message,
+ RadioChannelPrototype channel,
+ EntityUid radioSource,
+ bool escapeMarkup = true)
+ {
+
+ }
+}
diff --git a/Content.Shared/Radio/RadioChannelPrototype.cs b/Content.Shared/Radio/RadioChannelPrototype.cs
index 4787a54c0d..7a94634c8f 100644
--- a/Content.Shared/Radio/RadioChannelPrototype.cs
+++ b/Content.Shared/Radio/RadioChannelPrototype.cs
@@ -1,14 +1,21 @@
+using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Shared.Radio;
+///
+/// Defines a radio channel and its transmission properties.
+///
[Prototype]
public sealed partial class RadioChannelPrototype : IPrototype
{
+ [IdDataField, ViewVariables]
+ public string ID { get; private set; } = default!;
+
///
/// Human-readable name for the channel.
///
- [DataField("name")]
+ [DataField]
public LocId Name { get; private set; } = string.Empty;
[ViewVariables(VVAccess.ReadOnly)]
@@ -20,19 +27,21 @@ public sealed partial class RadioChannelPrototype : IPrototype
[DataField("keycode")]
public char KeyCode { get; private set; } = '\0';
- [DataField("frequency")]
- public int Frequency { get; private set; } = 0;
-
- [DataField("color")]
- public Color Color { get; private set; } = Color.Lime;
-
- [IdDataField, ViewVariables]
- public string ID { get; private set; } = default!;
+ ///
+ /// Frequency used by the channel.
+ ///
+ [DataField]
+ public FixedPoint2 Frequency { get; private set; } = 0;
///
- /// If channel is long range it doesn't require telecommunication server
- /// and messages can be sent across different stations
+ /// Color used to display the channel.
///
- [DataField("longRange"), ViewVariables]
- public bool LongRange = false;
+ [DataField]
+ public Color Color { get; private set; } = Color.Lime;
+
+ ///
+ /// Whether the channel can transmit across different stations without a telecommunications server.
+ ///
+ [DataField]
+ public bool LongRange;
}
diff --git a/Content.Server/Radio/RadioEvent.cs b/Content.Shared/Radio/RadioEvent.cs
similarity index 80%
rename from Content.Server/Radio/RadioEvent.cs
rename to Content.Shared/Radio/RadioEvent.cs
index 49ff63f824..6003a75746 100644
--- a/Content.Server/Radio/RadioEvent.cs
+++ b/Content.Shared/Radio/RadioEvent.cs
@@ -1,19 +1,21 @@
using Content.Shared.Chat;
-using Content.Shared.Radio;
-namespace Content.Server.Radio;
+namespace Content.Shared.Radio;
+///
+/// Event raised when a radio message is received.
+///
[ByRefEvent]
public readonly record struct RadioReceiveEvent(string Message, EntityUid MessageSource, RadioChannelPrototype Channel, EntityUid RadioSource, MsgChatMessage ChatMsg);
///
-/// Event raised on the parent entity of a headset radio when a radio message is received
+/// Event raised on the parent entity of a headset radio when a radio message is received.
///
[ByRefEvent]
public readonly record struct HeadsetRadioReceiveRelayEvent(RadioReceiveEvent RelayedEvent);
///
-/// Use this event to cancel sending message per receiver
+/// Use this event to cancel sending message per receiver.
///
[ByRefEvent]
public record struct RadioReceiveAttemptEvent(RadioChannelPrototype Channel, EntityUid RadioSource, EntityUid RadioReceiver)
@@ -25,7 +27,7 @@ public record struct RadioReceiveAttemptEvent(RadioChannelPrototype Channel, Ent
}
///
-/// Use this event to cancel sending message to every receiver
+/// Use this event to cancel sending message to every receiver.
///
[ByRefEvent]
public record struct RadioSendAttemptEvent(RadioChannelPrototype Channel, EntityUid RadioSource)
diff --git a/Resources/Locale/en-US/radio/components/encryption-key-component.ftl b/Resources/Locale/en-US/radio/components/encryption-key-component.ftl
index 5bdf3faeb2..d595cd3790 100644
--- a/Resources/Locale/en-US/radio/components/encryption-key-component.ftl
+++ b/Resources/Locale/en-US/radio/components/encryption-key-component.ftl
@@ -6,5 +6,5 @@ encryption-keys-are-locked = Encryption key slots are locked!
encryption-keys-panel-locked = Open maintenance panel first!
examine-encryption-channels-prefix = Available frequencies:
-examine-encryption-channel = [color={$color}]{$key} for {$id} ({NATURALFIXED($freq, 1)})[/color]
+examine-encryption-channel = [color={$color}]{$key} for {$id} ({$freq})[/color]
examine-encryption-default-channel = The default channel is [color={$color}]{$channel}[/color].
diff --git a/Resources/Locale/en-US/radio/components/handheld-radio-component.ftl b/Resources/Locale/en-US/radio/components/handheld-radio-component.ftl
index 39bbf8f968..3bac5d4935 100644
--- a/Resources/Locale/en-US/radio/components/handheld-radio-component.ftl
+++ b/Resources/Locale/en-US/radio/components/handheld-radio-component.ftl
@@ -1,6 +1,4 @@
handheld-radio-component-on-use = The radio is now {$radioState}.
-handheld-radio-component-on-examine = It's set to broadcast over the {$frequency} frequency.
handheld-radio-component-on-state = on
handheld-radio-component-off-state = off
handheld-radio-component-channel-set = Channel set to {$channel}
-handheld-radio-component-chennel-examine = The current channel is {$channel}.
\ No newline at end of file
diff --git a/Resources/Locale/en-US/radio/components/radio-microphone-component.ftl b/Resources/Locale/en-US/radio/components/radio-microphone-component.ftl
new file mode 100644
index 0000000000..290c86c266
--- /dev/null
+++ b/Resources/Locale/en-US/radio/components/radio-microphone-component.ftl
@@ -0,0 +1 @@
+radio-microphone-component-examine = The current channel is [color={$color}]{$channel} ({$frequency})[/color].
diff --git a/Resources/Prototypes/radio_channels.yml b/Resources/Prototypes/radio_channels.yml
index b4ef2b4ff5..9a6e647f3b 100644
--- a/Resources/Prototypes/radio_channels.yml
+++ b/Resources/Prototypes/radio_channels.yml
@@ -2,14 +2,14 @@
id: Common
name: chat-radio-common
keycode: ";"
- frequency: 1459
+ frequency: 145.9
color: "#2cdb2c"
- type: radioChannel
id: CentCom
name: chat-radio-centcom
keycode: 'y'
- frequency: 1337
+ frequency: 133.7
color: "#2681a5"
longRange: true
@@ -17,63 +17,63 @@
id: Command
name: chat-radio-command
keycode: 'c'
- frequency: 1353
+ frequency: 135.3
color: "#fcdf03"
- type: radioChannel
id: Engineering
name: chat-radio-engineering
keycode: 'e'
- frequency: 1357
+ frequency: 135.7
color: "#ff733c"
- type: radioChannel
id: Medical
name: chat-radio-medical
keycode: 'm'
- frequency: 1355
+ frequency: 135.5
color: "#57b8f0"
- type: radioChannel
id: Science
name: chat-radio-science
keycode: 'n'
- frequency: 1351
+ frequency: 135.1
color: "#cd7ccd"
- type: radioChannel
id: Security
name: chat-radio-security
keycode: 's'
- frequency: 1359
+ frequency: 135.9
color: "#ff4242"
- type: radioChannel
id: Service
name: chat-radio-service
keycode: 'v'
- frequency: 1349
+ frequency: 134.9
color: "#539c00"
- type: radioChannel
id: Supply
name: chat-radio-supply
keycode: 'u'
- frequency: 1347
+ frequency: 134.7
color: "#b48b57"
- type: radioChannel
id: Syndicate
name: chat-radio-syndicate
keycode: 't'
- frequency: 1213
+ frequency: 121.3
color: "#8f4a4b"
longRange: true
- type: radioChannel
id: Handheld
name: chat-radio-handheld
- frequency: 1330
+ frequency: 133.0
color: "#967101"
# long range since otherwise it'd defeat the point of a handheld radio independent of telecomms
longRange: true
@@ -82,7 +82,7 @@
id: Binary
name: chat-radio-binary
keycode: 'b'
- frequency: 1001
+ frequency: 100.1
color: "#5ed7aa"
# long range since otherwise it'd defeat the point of a handheld radio independent of telecomms
longRange: true
@@ -91,7 +91,7 @@
id: Freelance
name: chat-radio-freelance
keycode: 'f'
- frequency: 1984
+ frequency: 198.4
color: "#f6ce64"
# long range since otherwise it'd defeat the point of a handheld radio independent of telecomms
longRange: true
@@ -100,7 +100,7 @@
id: Xenoborg
name: chat-radio-xenoborg
keycode: 'x'
- frequency: 2002
+ frequency: 200.2
color: "#2288ff"
# long range since I don't wanna make a special xenoborg telecomm server
longRange: true
@@ -109,7 +109,7 @@
id: Mothership
name: chat-radio-mothership
keycode: 'z'
- frequency: 2003
+ frequency: 200.3
color: "#ff2222"
# long range since I don't wanna make a special xenoborg telecomm server
- longRange: true
\ No newline at end of file
+ longRange: true