Predict intercom examine. (#43107)

* radiosystem is funny

* review

Co-authored-by: slarticodefast <slarticodefast@users.noreply.github.com>
Co-authored-by: moonheart08 <moonheart08@users.noreply.github.com>

* dont forget your tests

* dont forget your loc

* networking

Co-authored-by: Tayrtahn <Tayrtahn@users.noreply.github.com>

* 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 <slarticodefast@users.noreply.github.com>
Co-authored-by: moonheart08 <moonheart08@users.noreply.github.com>
Co-authored-by: Tayrtahn <Tayrtahn@users.noreply.github.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: Pok245 <113675512+Pok27@users.noreply.github.com>
Co-authored-by: Fildrance <fildrance@gmail.com>
This commit is contained in:
phönix 2026-08-10 10:59:08 +03:00 • committed by GitHub
parent 0757710b46
commit da8bf6df00
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 471 additions and 351 deletions

View file

@ -6,16 +6,12 @@ using Robust.Client.GameObjects;
namespace Content.Client.Radio.EntitySystems;
/// <inheritdoc/>
public sealed partial class RadioDeviceSystem : SharedRadioDeviceSystem
{
[Dependency] private UserInterfaceSystem _ui = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<IntercomComponent, AfterAutoHandleStateEvent>(OnAfterHandleState);
}
[SubscribeLocalEvent]
private void OnAfterHandleState(Entity<IntercomComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (_ui.TryGetOpenUi<IntercomBoundUserInterface>(ent.Owner, IntercomUiKey.Key, out var bui))

View file

@ -0,0 +1,6 @@
using Content.Shared.Radio.EntitySystems;
namespace Content.Client.Radio.EntitySystems;
/// <inheritdoc/>
public sealed partial class RadioSystem : SharedRadioSystem;

View file

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

View file

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

View file

@ -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<ActiveRadioJammerComponent, RadioJammerComponent, TransformComponent>();

View file

@ -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;
/// <summary>
/// This system handles radio speakers and microphones (which together form a hand-held radio).
/// </summary>
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<RadioMicrophoneComponent, ComponentInit>(OnMicrophoneInit);
SubscribeLocalEvent<RadioMicrophoneComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<RadioMicrophoneComponent, ActivateInWorldEvent>(OnActivateMicrophone);
SubscribeLocalEvent<RadioMicrophoneComponent, ListenEvent>(OnListen);
SubscribeLocalEvent<RadioMicrophoneComponent, ListenAttemptEvent>(OnAttemptListen);
SubscribeLocalEvent<RadioMicrophoneComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<RadioSpeakerComponent, ComponentInit>(OnSpeakerInit);
SubscribeLocalEvent<RadioSpeakerComponent, ActivateInWorldEvent>(OnActivateSpeaker);
SubscribeLocalEvent<RadioSpeakerComponent, RadioReceiveEvent>(OnReceiveRadio);
SubscribeLocalEvent<IntercomComponent, EncryptionChannelsChangedEvent>(OnIntercomEncryptionChannelsChanged);
SubscribeLocalEvent<IntercomComponent, ToggleIntercomMicMessage>(OnToggleIntercomMic);
SubscribeLocalEvent<IntercomComponent, ToggleIntercomSpeakerMessage>(OnToggleIntercomSpeaker);
SubscribeLocalEvent<IntercomComponent, SelectIntercomChannelMessage>(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<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)
{
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<ActiveListenerComponent>(uid).Range = component.ListenRange;
else
RemCompDeferred<ActiveListenerComponent>(uid);
}
#endregion
private void OnExamine(EntityUid uid, RadioMicrophoneComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
var proto = ProtoMan.Index<RadioChannelPrototype>(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<RadioSpeakerComponent>(args.Source))
return; // no feedback loops please.
var channel = ProtoMan.Index<RadioChannelPrototype>(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<IntercomComponent> ent, ref EncryptionChannelsChangedEvent args)
{
ent.Comp.SupportedChannels = args.Component.Channels.Select(p => new ProtoId<RadioChannelPrototype>(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<IntercomComponent> 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<IntercomComponent> 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<IntercomComponent> ent, ref SelectIntercomChannelMessage args)
{
if (ent.Comp.RequiresPower && !this.IsPowered(ent, EntityManager))
return;
if (!ProtoMan.HasIndex<RadioChannelPrototype>(args.Channel) || !ent.Comp.SupportedChannels.Contains(args.Channel))
return;
SetIntercomChannel(ent, args.Channel);
}
private void SetIntercomChannel(Entity<IntercomComponent> ent, ProtoId<RadioChannelPrototype>? 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<RadioMicrophoneComponent>(ent, out var mic))
mic.BroadcastChannel = channel.Value;
if (TryComp<RadioSpeakerComponent>(ent, out var speaker))
speaker.Channels = new() { channel.Value };
Dirty(ent);
}
}
/// <inheritdoc/>
public sealed partial class RadioDeviceSystem : SharedRadioDeviceSystem;

View file

@ -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;
/// <summary>
/// This system handles intrinsic radios and the general process of converting radio messages into chat messages.
/// </summary>
public sealed partial class RadioSystem : EntitySystem
/// <inheritdoc/>
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);
}
/// <summary>
/// Send radio message to all active radio listeners
/// </summary>
public void SendRadioMessage(EntityUid messageSource, string message, ProtoId<RadioChannelPrototype> channel, EntityUid radioSource, bool escapeMarkup = true)
{
SendRadioMessage(messageSource, message, ProtoMan.Index(channel), radioSource, escapeMarkup: escapeMarkup);
}
/// <summary>
/// Send radio message to all active radio listeners
/// </summary>
/// <param name="messageSource">Entity that spoke the message</param>
/// <param name="radioSource">Entity that picked up the message and will send it, e.g. headset</param>
public void SendRadioMessage(EntityUid messageSource, string message, RadioChannelPrototype channel, EntityUid radioSource, bool escapeMarkup = true)
/// <inheritdoc/>
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))

View file

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

View file

@ -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.
/// </summary>
[DataField]
public HashSet<int> FrequenciesExcluded = [];
public HashSet<FixedPoint2> FrequenciesExcluded = [];
/// <summary>
/// Index of the currently selected setting.

View file

@ -6,35 +6,45 @@ using Robust.Shared.Prototypes;
namespace Content.Shared.Radio.Components;
/// <summary>
/// Listens for local chat messages and relays them to some radio frequency
/// Listens for local chat messages and relays them to some radio frequency.
/// </summary>
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedRadioDeviceSystem))]
public sealed partial class RadioMicrophoneComponent : Component
{
[DataField]
/// <summary>
/// Radio channel on which local speech is broadcast.
/// </summary>
[DataField, AutoNetworkedField]
public ProtoId<RadioChannelPrototype> BroadcastChannel = SharedChatSystem.CommonChannel;
/// <summary>
/// Maximum distance from the microphone at which speech is heard.
/// </summary>
[DataField]
public int ListenRange = 4;
[DataField]
public bool Enabled = false;
[DataField]
public bool PowerRequired = false;
/// <summary>
/// Whether the microphone is currently broadcasting.
/// </summary>
[DataField, AutoNetworkedField]
public bool Enabled;
/// <summary>
/// Whether or not interacting with this entity
/// toggles it on or off.
/// Whether the microphone requires power to operate.
/// </summary>
[DataField]
public bool PowerRequired;
/// <summary>
/// Whether interacting with this entity toggles it on/off, or not.
/// </summary>
[DataField]
public bool ToggleOnInteract = true;
/// <summary>
/// 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.
/// </summary>
[DataField]
public bool UnobstructedRequired = false;
public bool UnobstructedRequired;
}

View file

@ -6,22 +6,27 @@ using Robust.Shared.Prototypes;
namespace Content.Shared.Radio.Components;
/// <summary>
/// Listens for radio messages and relays them to local chat.
/// Listens for radio messages and relays them to local chat.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedRadioDeviceSystem))]
public sealed partial class RadioSpeakerComponent : Component
{
/// <summary>
/// Whether or not interacting with this entity
/// toggles it on or off.
/// Whether interacting with this entity toggles it on/off, or not.
/// </summary>
[DataField]
public bool ToggleOnInteract = true;
[DataField]
/// <summary>
/// Radio channels from which messages are received.
/// </summary>
[DataField, AutoNetworkedField]
public HashSet<ProtoId<RadioChannelPrototype>> Channels = new() { SharedChatSystem.CommonChannel };
/// <summary>
/// Whether the speaker is currently receiving radio messages.
/// </summary>
[DataField, AutoNetworkedField]
public bool Enabled;
}

View file

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

View file

@ -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;
/// <summary>
/// This system handles radio speakers and microphones (which together form a hand-held radio).
/// </summary>
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 = [];
/// <inheritdoc/>
public override void Update(float frameTime)
{
base.Update(frameTime);
_recentlySent.Clear();
}
#region Component Init
[SubscribeLocalEvent]
private void OnMicrophoneInit(Entity<RadioMicrophoneComponent> ent, ref ComponentInit args)
{
if (ent.Comp.Enabled)
EnsureComp<ActiveListenerComponent>(ent).Range = ent.Comp.ListenRange;
else
RemCompDeferred<ActiveListenerComponent>(ent);
}
[SubscribeLocalEvent]
private void OnSpeakerInit(Entity<RadioSpeakerComponent> ent, ref ComponentInit args)
{
if (ent.Comp.Enabled)
EnsureComp<ActiveRadioComponent>(ent).Channels.UnionWith(ent.Comp.Channels);
else
RemCompDeferred<ActiveRadioComponent>(ent);
}
#endregion
#region Toggling
public void ToggleRadioMicrophone(EntityUid uid, EntityUid user, bool quiet = false, RadioMicrophoneComponent? component = null)
[SubscribeLocalEvent]
private void OnActivateMicrophone(Entity<RadioMicrophoneComponent> 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<RadioSpeakerComponent> 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<RadioMicrophoneComponent> 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);
}
/// <summary>
/// Enables or disables a radio microphone.
/// </summary>
/// <param name="ent">The entity with the microphone.</param>
/// <param name="user">The entity toggling the microphone, if any.</param>
/// <param name="enabled">Whether the microphone should be enabled.</param>
/// <param name="quiet">Whether to suppress the user-facing popup.</param>
public void SetMicrophoneEnabled(
Entity<RadioMicrophoneComponent?> 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<ActiveRadioComponent>(uid).Channels.UnionWith(component.Channels);
_appearance.SetData(ent, RadioDeviceVisuals.Broadcasting, ent.Comp.Enabled);
if (ent.Comp.Enabled)
EnsureComp<ActiveListenerComponent>(ent).Range = ent.Comp.ListenRange;
else
RemCompDeferred<ActiveRadioComponent>(uid);
RemCompDeferred<ActiveListenerComponent>(ent);
}
#endregion
#region Toggling
/// <summary>
/// Toggles a radio microphone.
/// </summary>
/// <param name="ent">The entity with the microphone.</param>
/// <param name="user">The entity toggling the microphone.</param>
/// <param name="quiet">Whether to suppress the user-facing popup.</param>
public void ToggleRadioMicrophone(
Entity<RadioMicrophoneComponent?> ent,
EntityUid user,
bool quiet = false
)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
SetMicrophoneEnabled(ent, user, !ent.Comp.Enabled, quiet);
}
/// <summary>
/// Toggles a radio speaker.
/// </summary>
/// <param name="ent">The entity with the speaker.</param>
/// <param name="user">The entity toggling the speaker.</param>
/// <param name="quiet">Whether to suppress the user-facing popup.</param>
public void ToggleRadioSpeaker(
Entity<RadioSpeakerComponent?> ent,
EntityUid user,
bool quiet = false
)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
SetSpeakerEnabled(ent, user, !ent.Comp.Enabled, quiet);
}
/// <summary>
/// Enables or disables a radio speaker.
/// </summary>
/// <param name="ent">The entity with the speaker.</param>
/// <param name="user">The entity toggling the speaker, if any.</param>
/// <param name="enabled">Whether the speaker should be enabled.</param>
/// <param name="quiet">Whether to suppress the user-facing popup.</param>
public void SetSpeakerEnabled(
Entity<RadioSpeakerComponent?> 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<ActiveRadioComponent>(ent).Channels.UnionWith(ent.Comp.Channels);
else
RemCompDeferred<ActiveRadioComponent>(ent);
}
#endregion
[SubscribeLocalEvent]
private void OnExamine(Entity<RadioMicrophoneComponent> 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<RadioMicrophoneComponent> ent, ref ListenEvent args)
{
if (HasComp<RadioSpeakerComponent>(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<RadioMicrophoneComponent> 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<RadioSpeakerComponent> 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<IntercomComponent> ent,
ref EncryptionChannelsChangedEvent args
)
{
ent.Comp.SupportedChannels =
args.Component.Channels.Select(p => new ProtoId<RadioChannelPrototype>(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<IntercomComponent> 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<IntercomComponent> 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<IntercomComponent> ent, ref SelectIntercomChannelMessage args)
{
if (ent.Comp.RequiresPower && !_power.IsPowered(ent.Owner))
return;
if (!ProtoMan.HasIndex<RadioChannelPrototype>(args.Channel) ||
!ent.Comp.SupportedChannels.Contains(args.Channel))
return;
SetIntercomChannel(ent, args.Channel);
}
private void SetIntercomChannel(Entity<IntercomComponent> ent, ProtoId<RadioChannelPrototype>? channel)
{
ent.Comp.CurrentChannel = channel;
if (TryComp<RadioMicrophoneComponent>(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<RadioSpeakerComponent>(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);
}
}

View file

@ -0,0 +1,41 @@
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
namespace Content.Shared.Radio.EntitySystems;
/// <summary>
/// This system handles intrinsic radios and the general process of converting radio messages into chat messages.
/// </summary>
public abstract partial class SharedRadioSystem : EntitySystem
{
/// <summary>
/// Send radio message to all active radio listeners.
/// </summary>
[PublicAPI]
public void SendRadioMessage(EntityUid messageSource,
string message,
ProtoId<RadioChannelPrototype> channel,
EntityUid radioSource,
bool escapeMarkup = true)
{
SendRadioMessage(messageSource, message, ProtoMan.Index(channel), radioSource, escapeMarkup: escapeMarkup);
}
/// <summary>
/// Sends a radio message to all active radio listeners.
/// </summary>
/// <param name="messageSource">Entity that spoke the message.</param>
/// <param name="message">Message to send over the radio.</param>
/// <param name="channel">Radio channel to send the message on.</param>
/// <param name="radioSource">Entity transmitting the message.</param>
/// <param name="escapeMarkup">Whether markup in the message should be escaped.</param>
[PublicAPI]
public virtual void SendRadioMessage(EntityUid messageSource,
string message,
RadioChannelPrototype channel,
EntityUid radioSource,
bool escapeMarkup = true)
{
}
}

View file

@ -1,14 +1,21 @@
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Shared.Radio;
/// <summary>
/// Defines a radio channel and its transmission properties.
/// </summary>
[Prototype]
public sealed partial class RadioChannelPrototype : IPrototype
{
[IdDataField, ViewVariables]
public string ID { get; private set; } = default!;
/// <summary>
/// Human-readable name for the channel.
/// </summary>
[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!;
/// <summary>
/// Frequency used by the channel.
/// </summary>
[DataField]
public FixedPoint2 Frequency { get; private set; } = 0;
/// <summary>
/// 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.
/// </summary>
[DataField("longRange"), ViewVariables]
public bool LongRange = false;
[DataField]
public Color Color { get; private set; } = Color.Lime;
/// <summary>
/// Whether the channel can transmit across different stations without a telecommunications server.
/// </summary>
[DataField]
public bool LongRange;
}

View file

@ -1,19 +1,21 @@
using Content.Shared.Chat;
using Content.Shared.Radio;
namespace Content.Server.Radio;
namespace Content.Shared.Radio;
/// <summary>
/// Event raised when a radio message is received.
/// </summary>
[ByRefEvent]
public readonly record struct RadioReceiveEvent(string Message, EntityUid MessageSource, RadioChannelPrototype Channel, EntityUid RadioSource, MsgChatMessage ChatMsg);
/// <summary>
/// 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.
/// </summary>
[ByRefEvent]
public readonly record struct HeadsetRadioReceiveRelayEvent(RadioReceiveEvent RelayedEvent);
/// <summary>
/// Use this event to cancel sending message per receiver
/// Use this event to cancel sending message per receiver.
/// </summary>
[ByRefEvent]
public record struct RadioReceiveAttemptEvent(RadioChannelPrototype Channel, EntityUid RadioSource, EntityUid RadioReceiver)
@ -25,7 +27,7 @@ public record struct RadioReceiveAttemptEvent(RadioChannelPrototype Channel, Ent
}
/// <summary>
/// Use this event to cancel sending message to every receiver
/// Use this event to cancel sending message to every receiver.
/// </summary>
[ByRefEvent]
public record struct RadioSendAttemptEvent(RadioChannelPrototype Channel, EntityUid RadioSource)

View file

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

View file

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

View file

@ -0,0 +1 @@
radio-microphone-component-examine = The current channel is [color={$color}]{$channel} ({$frequency})[/color].

View file

@ -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
longRange: true