diff --git a/Content.Server/GameTicking/Rules/Configurations/SolarFlareEventRuleConfiguration.cs b/Content.Server/GameTicking/Rules/Configurations/SolarFlareEventRuleConfiguration.cs new file mode 100644 index 0000000000..7c1d1e35e8 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Configurations/SolarFlareEventRuleConfiguration.cs @@ -0,0 +1,40 @@ +using Content.Shared.Radio; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set; + +namespace Content.Server.GameTicking.Rules.Configurations; + +/// +/// Solar Flare event specific configuration +/// +public sealed class SolarFlareEventRuleConfiguration : StationEventRuleConfiguration +{ + /// + /// In seconds, most early moment event can end + /// + [DataField("minEndAfter")] + public int MinEndAfter; + + /// + /// In seconds, most late moment event can end + /// + [DataField("maxEndAfter")] + public int MaxEndAfter; + + /// + /// If true, only headsets affected, but e.g. handheld radio will still work + /// + [DataField("onlyJamHeadsets")] + public bool OnlyJamHeadsets; + + /// + /// Channels that will be disabled for a duration of event + /// + [DataField("affectedChannels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))] + public readonly HashSet AffectedChannels = new(); + + /// + /// Chance any given light bulb breaks due to event + /// + [DataField("lightBreakChance")] + public float LightBreakChance; +} \ No newline at end of file diff --git a/Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs b/Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs index 5fa0b28fe0..84689dc225 100644 --- a/Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs +++ b/Content.Server/GameTicking/Rules/Configurations/StationEventRuleConfiguration.cs @@ -8,7 +8,7 @@ namespace Content.Server.GameTicking.Rules.Configurations; /// game rules. /// [UsedImplicitly] -public sealed class StationEventRuleConfiguration : GameRuleConfiguration +public class StationEventRuleConfiguration : GameRuleConfiguration { [DataField("id", required: true)] private string _id = default!; diff --git a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs index de1fd66338..2a66f985d3 100644 --- a/Content.Server/Radio/EntitySystems/HeadsetSystem.cs +++ b/Content.Server/Radio/EntitySystems/HeadsetSystem.cs @@ -45,7 +45,7 @@ public sealed class HeadsetSystem : EntitySystem && TryComp(component.Headset, out HeadsetComponent? headset) && headset.Channels.Contains(args.Channel.ID)) { - _radio.SendRadioMessage(uid, args.Message, args.Channel); + _radio.SendRadioMessage(uid, args.Message, args.Channel, component.Headset); args.Channel = null; // prevent duplicate messages from other listeners. } } diff --git a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs index b98e1427aa..fc2d89ddbb 100644 --- a/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioDeviceSystem.cs @@ -194,7 +194,7 @@ public sealed class RadioDeviceSystem : EntitySystem return; // no feedback loops please. if (_recentlySent.Add((args.Message, args.Source))) - _radio.SendRadioMessage(args.Source, args.Message, _protoMan.Index(component.BroadcastChannel)); + _radio.SendRadioMessage(args.Source, args.Message, _protoMan.Index(component.BroadcastChannel), uid); } private void OnAttemptListen(EntityUid uid, RadioMicrophoneComponent component, ListenAttemptEvent args) diff --git a/Content.Server/Radio/EntitySystems/RadioSystem.cs b/Content.Server/Radio/EntitySystems/RadioSystem.cs index de6131bb9d..e85014f1d9 100644 --- a/Content.Server/Radio/EntitySystems/RadioSystem.cs +++ b/Content.Server/Radio/EntitySystems/RadioSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Administration.Logs; using Content.Server.Chat.Systems; using Content.Server.Radio.Components; using Content.Server.VoiceMask; +using Content.Server.Popups; using Content.Shared.Chat; using Content.Shared.Database; using Content.Shared.Radio; @@ -9,6 +10,7 @@ using Robust.Server.GameObjects; using Robust.Shared.Network; using Robust.Shared.Replays; using Robust.Shared.Utility; +using Content.Shared.Popups; namespace Content.Server.Radio.EntitySystems; @@ -20,6 +22,7 @@ public sealed class RadioSystem : EntitySystem [Dependency] private readonly INetManager _netMan = default!; [Dependency] private readonly IReplayRecordingManager _replay = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; // set used to prevent radio feedback loops. private readonly HashSet _messages = new(); @@ -46,7 +49,7 @@ public sealed class RadioSystem : EntitySystem _netMan.ServerSendMessage(args.ChatMsg, actor.PlayerSession.ConnectedClient); } - public void SendRadioMessage(EntityUid source, string message, RadioChannelPrototype channel) + public void SendRadioMessage(EntityUid source, string message, RadioChannelPrototype channel, EntityUid? radioSource = null) { // TODO if radios ever garble / modify messages, feedback-prevention needs to be handled better than this. if (!_messages.Add(message)) @@ -66,8 +69,9 @@ public sealed class RadioSystem : EntitySystem EntityUid.Invalid); var chatMsg = new MsgChatMessage { Message = chat }; - var ev = new RadioReceiveEvent(message, source, channel, chatMsg); - var attemptEv = new RadioReceiveAttemptEvent(message, source, channel); + var ev = new RadioReceiveEvent(message, source, channel, chatMsg, radioSource); + var attemptEv = new RadioReceiveAttemptEvent(message, source, channel, radioSource); + bool sentAtLeastOnce = false; foreach (var radio in EntityQuery()) { @@ -82,9 +86,11 @@ public sealed class RadioSystem : EntitySystem attemptEv.Uncancel(); continue; } - + sentAtLeastOnce = true; RaiseLocalEvent(radio.Owner, ev); } + if (!sentAtLeastOnce) + _popupSystem.PopupEntity(Loc.GetString("failed-to-send-message"), source, PopupType.MediumCaution); if (name != Name(source)) _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Radio message from {ToPrettyString(source):user} as {name} on {channel.LocalizedName}: {message}"); diff --git a/Content.Server/Radio/RadioReceiveEvent.cs b/Content.Server/Radio/RadioReceiveEvent.cs index f1b4e3d6cd..f8d8240b8d 100644 --- a/Content.Server/Radio/RadioReceiveEvent.cs +++ b/Content.Server/Radio/RadioReceiveEvent.cs @@ -9,13 +9,15 @@ public sealed class RadioReceiveEvent : EntityEventArgs public readonly EntityUid Source; public readonly RadioChannelPrototype Channel; public readonly MsgChatMessage ChatMsg; + public readonly EntityUid? RadioSource; - public RadioReceiveEvent(string message, EntityUid source, RadioChannelPrototype channel, MsgChatMessage chatMsg) + public RadioReceiveEvent(string message, EntityUid source, RadioChannelPrototype channel, MsgChatMessage chatMsg, EntityUid? radioSource) { Message = message; Source = source; Channel = channel; ChatMsg = chatMsg; + RadioSource = radioSource; } } @@ -24,11 +26,13 @@ public sealed class RadioReceiveAttemptEvent : CancellableEntityEventArgs public readonly string Message; public readonly EntityUid Source; public readonly RadioChannelPrototype Channel; + public readonly EntityUid? RadioSource; - public RadioReceiveAttemptEvent(string message, EntityUid source, RadioChannelPrototype channel) + public RadioReceiveAttemptEvent(string message, EntityUid source, RadioChannelPrototype channel, EntityUid? radioSource) { Message = message; Source = source; Channel = channel; + RadioSource = radioSource; } } diff --git a/Content.Server/StationEvents/Events/SolarFlare.cs b/Content.Server/StationEvents/Events/SolarFlare.cs new file mode 100644 index 0000000000..2b4d6d182a --- /dev/null +++ b/Content.Server/StationEvents/Events/SolarFlare.cs @@ -0,0 +1,73 @@ +using Content.Server.GameTicking.Rules.Configurations; +using Content.Server.Radio.Components; +using Content.Server.Radio; +using Robust.Shared.Random; +using Content.Server.Light.EntitySystems; +using Content.Server.Light.Components; + +namespace Content.Server.StationEvents.Events; + +public sealed class SolarFlare : StationEventSystem +{ + [Dependency] private readonly PoweredLightSystem _poweredLight = default!; + + public override string Prototype => "SolarFlare"; + + private SolarFlareEventRuleConfiguration _event = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnRadioSendAttempt); + } + + public override void Added() + { + base.Added(); + + if (Configuration is not SolarFlareEventRuleConfiguration ev) + return; + + _event = ev; + _event.EndAfter = RobustRandom.Next(ev.MinEndAfter, ev.MaxEndAfter); + } + + public override void Started() + { + base.Started(); + MessLights(); + } + + private void MessLights() + { + foreach (var comp in EntityQuery()) + { + if (RobustRandom.Prob(_event.LightBreakChance)) + { + var uid = comp.Owner; + _poweredLight.TryDestroyBulb(uid, comp); + } + } + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (!RuleStarted) + return; + + if (Elapsed > _event.EndAfter) + { + ForceEndSelf(); + return; + } + } + + private void OnRadioSendAttempt(EntityUid uid, ActiveRadioComponent component, RadioReceiveAttemptEvent args) + { + if (RuleStarted && _event.AffectedChannels.Contains(args.Channel.ID)) + if (!_event.OnlyJamHeadsets || (HasComp(uid) || HasComp(args.RadioSource))) + args.Cancel(); + } +} diff --git a/Resources/Locale/en-US/radio/radio-event.ftl b/Resources/Locale/en-US/radio/radio-event.ftl new file mode 100644 index 0000000000..63c264b0ab --- /dev/null +++ b/Resources/Locale/en-US/radio/radio-event.ftl @@ -0,0 +1 @@ +failed-to-send-message = Failed to send message! diff --git a/Resources/Locale/en-US/station-events/events/solar-flare.ftl b/Resources/Locale/en-US/station-events/events/solar-flare.ftl new file mode 100644 index 0000000000..5c88f82ded --- /dev/null +++ b/Resources/Locale/en-US/station-events/events/solar-flare.ftl @@ -0,0 +1,2 @@ +station-event-solar-flare-start-announcement = A solar flare has been detected near the station. Some communication channels may not function. +station-event-solar-flare-end-announcement = The solar flare ended. Communication channels no longer affected. diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index 2597006dfa..9986cedd9b 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -162,6 +162,23 @@ startAudio: path: /Audio/Announcements/attention.ogg +- type: gameRule + id: SolarFlare + config: !type:SolarFlareEventRuleConfiguration + id: SolarFlare + weight: 10 + startAnnouncement: station-event-solar-flare-start-announcement + endAnnouncement: station-event-solar-flare-end-announcement + startAudio: + path: /Audio/Announcements/attention.ogg + minEndAfter: 120 + maxEndAfter: 240 + onlyJamHeadsets: true + affectedChannels: + - Common + - Service + lightBreakChance: 0.05 + - type: gameRule id: VentClog config: @@ -194,7 +211,7 @@ earliestStart: 20 minimumPlayers: 15 weight: 5 - endAfter: 60 + endAfter: 60 - type: gameRule id: ZombieOutbreak