From da8bf6df006113e2e495d79f59eceb89fd6abb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ph=C3=B6nix?= Date: Mon, 10 Aug 2026 10:59:08 +0300 Subject: [PATCH] Predict intercom examine. (#43107) * radiosystem is funny * review Co-authored-by: slarticodefast Co-authored-by: moonheart08 * dont forget your tests * dont forget your loc * networking Co-authored-by: Tayrtahn * dont return because its breaks * uh * review Co-authored-by: Pok245 <113675512+Pok27@users.noreply.github.com> * more predictions and cleanup * refactor: just me, being picky about whitespaces and commetns formatting --------- Co-authored-by: slarticodefast Co-authored-by: moonheart08 Co-authored-by: Tayrtahn Co-authored-by: Tayrtahn Co-authored-by: Pok245 <113675512+Pok27@users.noreply.github.com> Co-authored-by: Fildrance --- .../Radio/EntitySystems/RadioDeviceSystem.cs | 8 +- .../Radio/EntitySystems/RadioSystem.cs | 6 + .../Animals/Systems/ParrotMemorySystem.cs | 2 +- Content.Server/Emp/EmpSystem.cs | 2 +- .../Radio/EntitySystems/JammerSystem.cs | 4 +- .../Radio/EntitySystems/RadioDeviceSystem.cs | 246 +------------ .../Radio/EntitySystems/RadioSystem.cs | 24 +- .../StationEvents/Events/SolarFlareRule.cs | 8 +- .../Radio/Components/RadioJammerComponent.cs | 3 +- .../Components/RadioMicrophoneComponent.cs | 36 +- .../Radio/Components/RadioSpeakerComponent.cs | 13 +- .../EntitySystems/EncryptionKeySystem.cs | 2 +- .../EntitySystems/SharedRadioDeviceSystem.cs | 343 +++++++++++++++++- .../Radio/EntitySystems/SharedRadioSystem.cs | 41 +++ Content.Shared/Radio/RadioChannelPrototype.cs | 35 +- .../Radio/RadioEvent.cs | 12 +- .../components/encryption-key-component.ftl | 2 +- .../components/handheld-radio-component.ftl | 2 - .../components/radio-microphone-component.ftl | 1 + Resources/Prototypes/radio_channels.yml | 32 +- 20 files changed, 471 insertions(+), 351 deletions(-) create mode 100644 Content.Client/Radio/EntitySystems/RadioSystem.cs create mode 100644 Content.Shared/Radio/EntitySystems/SharedRadioSystem.cs rename {Content.Server => Content.Shared}/Radio/RadioEvent.cs (80%) create mode 100644 Resources/Locale/en-US/radio/components/radio-microphone-component.ftl 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