using Content.Server.DeviceLinking.Components; using Content.Shared.Access.Systems; using Content.Shared.DeviceLinking.Events; using Content.Shared.MachineLinking; using Content.Shared.TextScreen; using Content.Shared.UserInterface; using Robust.Server.GameObjects; using Robust.Shared.Audio.Systems; using Robust.Shared.Timing; namespace Content.Server.DeviceLinking.Systems; /// /// A system for signallable timers. Sets timer AppearanceData when triggered to start/stop. /// /// /// public sealed partial class SignalTimerSystem : EntitySystem { [Dependency] private IGameTiming _gameTiming = default!; [Dependency] private AccessReaderSystem _accessReader = default!; [Dependency] private DeviceLinkSystem _deviceLink = default!; [Dependency] private SharedAppearanceSystem _appearance = default!; [Dependency] private SharedAudioSystem _audio = default!; [Dependency] private UserInterfaceSystem _ui = default!; [Dependency] private EntityQuery _activeTimerQuery; [Dependency] private EntityQuery _appearanceQuery; /// /// Per-tick timer cache. /// private readonly List> _timers = new(); #region Event Handlers [SubscribeLocalEvent] private void OnInit(Entity ent, ref ComponentInit args) { if (_appearanceQuery.TryComp(ent, out var appearance)) { _appearance.SetData(ent, TextScreenVisuals.DefaultText, ent.Comp.Label, appearance); _appearance.SetData(ent, TextScreenVisuals.ScreenText, ent.Comp.Label, appearance); _appearance.SetData(ent, TextScreenVisuals.ScreenTextTime, _gameTiming.CurTime, appearance); } _deviceLink.EnsureSinkPorts(ent, ent.Comp.Trigger); } [SubscribeLocalEvent] private void OnAfterActivatableUIOpen(Entity ent, ref AfterActivatableUIOpenEvent args) { var time = _activeTimerQuery.TryComp(ent, out var active) ? active.TriggerTime : TimeSpan.Zero; if (!_ui.HasUi(ent, SignalTimerUiKey.Key)) return; var timeSpan = TimeSpan.FromSeconds(ent.Comp.Delay); _ui.SetUiState(ent.Owner, SignalTimerUiKey.Key, new SignalTimerBoundUserInterfaceState(ent.Comp.Label, timeSpan.Minutes.ToString("D2"), timeSpan.Seconds.ToString("D2"), ent.Comp.CanEditLabel, time, active != null, _accessReader.IsAllowed(args.User, ent))); } /// /// Called by to both /// change the default component label, and propagate that change to the TextScreen. /// [SubscribeLocalEvent] private void OnTextChangedMessage(Entity ent, ref SignalTimerTextChangedMessage args) { if (!IsMessageValid(ent, args)) return; ent.Comp.Label = args.Text[..Math.Min(ent.Comp.MaxLength, args.Text.Length)]; if (!_appearanceQuery.TryComp(ent, out var appearance)) return; // could maybe move the defaulttext update out of this block, // if you delved deep into appearance update batching _appearance.SetData(ent, TextScreenVisuals.DefaultText, ent.Comp.Label, appearance); _appearance.SetData(ent, TextScreenVisuals.ScreenText, ent.Comp.Label, appearance); _appearance.SetData(ent, TextScreenVisuals.ScreenTextTime, _gameTiming.CurTime, appearance); } /// /// Called by to change the /// delay, and propagate that change to a textscreen. /// [SubscribeLocalEvent] private void OnDelayChangedMessage(Entity ent, ref SignalTimerDelayChangedMessage args) { if (!IsMessageValid(ent, args)) return; ent.Comp.Delay = Math.Min(args.Delay.TotalSeconds, ent.Comp.MaxDuration); } /// /// Called by to instantiate an , /// clear , propagate those changes, and invoke the start port. /// [SubscribeLocalEvent] private void OnTimerStartMessage(Entity ent, ref SignalTimerStartMessage args) { if (!IsMessageValid(ent, args)) return; // feedback received: pressing the timer button while a timer is running should cancel the timer. if (_activeTimerQuery.HasComp(ent)) { _appearance.SetData(ent, TextScreenVisuals.TargetTime, _gameTiming.CurTime); Trigger(ent); } else StartTimer(ent); } [SubscribeLocalEvent] private void OnSignalReceived(Entity ent, ref SignalReceivedEvent args) { if (args.Port == ent.Comp.Trigger) StartTimer(ent); } #endregion Event Handlers #region Public API /// /// Finishes a timer, triggering its main port, and removing its . /// public void Trigger(Entity ent) { RemComp(ent); _audio.PlayPvs(ent.Comp.DoneSound, ent); _deviceLink.InvokePort(ent.Owner, ent.Comp.TriggerPort); if (_ui.HasUi(ent, SignalTimerUiKey.Key)) { _ui.SetUiState(ent.Owner, SignalTimerUiKey.Key, new SignalTimerBoundUserInterfaceState(ent.Comp.Label, TimeSpan.FromSeconds(ent.Comp.Delay).Minutes.ToString("D2"), TimeSpan.FromSeconds(ent.Comp.Delay).Seconds.ToString("D2"), ent.Comp.CanEditLabel, TimeSpan.Zero, false, true)); } } public override void Update(float frameTime) { base.Update(frameTime); UpdateTimer(); } public void StartTimer(Entity ent) { var timer = EnsureComp(ent); timer.TriggerTime = _gameTiming.CurTime + TimeSpan.FromSeconds(ent.Comp.Delay); if (_appearanceQuery.TryComp(ent, out var appearance)) { _appearance.SetData(ent, TextScreenVisuals.TargetTime, timer.TriggerTime, appearance); _appearance.SetData(ent, TextScreenVisuals.ScreenText, string.Empty, appearance); } _deviceLink.InvokePort(ent.Owner, ent.Comp.StartPort); } #endregion Public API #region Internal private void UpdateTimer() { _timers.Clear(); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var active, out var timer)) { if (active.TriggerTime > _gameTiming.CurTime) continue; _timers.Add((uid, timer)); } foreach (var timer in _timers) { // Exploded or the likes. if (!Exists(timer.Owner)) continue; Trigger(timer); } } /// /// Checks if a UI is allowed to be sent by the user. /// /// The entity that is interacted with. /// The message to check. private bool IsMessageValid(EntityUid uid, BoundUserInterfaceMessage message) { if (!_accessReader.IsAllowed(message.Actor, uid)) return false; return true; } #endregion Internal }