using Content.Server.DeviceLinking.Components;
using Content.Shared.UserInterface;
using Content.Shared.Access.Systems;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.MachineLinking;
using Content.Shared.TextScreen;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing;
namespace Content.Server.DeviceLinking.Systems;
public sealed class SignalTimerSystem : EntitySystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
///
/// Per-tick timer cache.
///
private List> _timers = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent(OnInit);
SubscribeLocalEvent(OnAfterActivatableUIOpen);
SubscribeLocalEvent(OnTextChangedMessage);
SubscribeLocalEvent(OnDelayChangedMessage);
SubscribeLocalEvent(OnTimerStartMessage);
SubscribeLocalEvent(OnSignalReceived);
}
private void OnInit(EntityUid uid, SignalTimerComponent component, ComponentInit args)
{
_appearanceSystem.SetData(uid, TextScreenVisuals.DefaultText, component.Label);
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, component.Label);
_signalSystem.EnsureSinkPorts(uid, component.Trigger);
}
private void OnAfterActivatableUIOpen(EntityUid uid, SignalTimerComponent component, AfterActivatableUIOpenEvent args)
{
var time = TryComp(uid, out var active) ? active.TriggerTime : TimeSpan.Zero;
if (_ui.HasUi(uid, SignalTimerUiKey.Key))
{
_ui.SetUiState(uid, SignalTimerUiKey.Key, new SignalTimerBoundUserInterfaceState(component.Label,
TimeSpan.FromSeconds(component.Delay).Minutes.ToString("D2"),
TimeSpan.FromSeconds(component.Delay).Seconds.ToString("D2"),
component.CanEditLabel,
time,
active != null,
_accessReader.IsAllowed(args.User, uid)));
}
}
///
/// Finishes a timer, triggering its main port, and removing its .
///
public void Trigger(EntityUid uid, SignalTimerComponent signalTimer)
{
RemComp(uid);
_audio.PlayPvs(signalTimer.DoneSound, uid);
_signalSystem.InvokePort(uid, signalTimer.TriggerPort);
if (_ui.HasUi(uid, SignalTimerUiKey.Key))
{
_ui.SetUiState(uid, SignalTimerUiKey.Key, new SignalTimerBoundUserInterfaceState(signalTimer.Label,
TimeSpan.FromSeconds(signalTimer.Delay).Minutes.ToString("D2"),
TimeSpan.FromSeconds(signalTimer.Delay).Seconds.ToString("D2"),
signalTimer.CanEditLabel,
TimeSpan.Zero,
false,
true));
}
}
public override void Update(float frameTime)
{
base.Update(frameTime);
UpdateTimer();
}
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.Owner, timer.Comp);
}
}
///
/// Checks if a UI is allowed to be sent by the user.
///
/// The entity that is interacted with.
private bool IsMessageValid(EntityUid uid, BoundUserInterfaceMessage message)
{
if (!_accessReader.IsAllowed(message.Actor, uid))
return false;
return true;
}
///
/// Called by to both
/// change the default component label, and propagate that change to the TextScreen.
///
private void OnTextChangedMessage(EntityUid uid, SignalTimerComponent component, SignalTimerTextChangedMessage args)
{
if (!IsMessageValid(uid, args))
return;
component.Label = args.Text[..Math.Min(component.MaxLength, args.Text.Length)];
if (!HasComp(uid))
{
// could maybe move the defaulttext update out of this block,
// if you delved deep into appearance update batching
_appearanceSystem.SetData(uid, TextScreenVisuals.DefaultText, component.Label);
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, component.Label);
}
}
///
/// Called by to change the
/// delay, and propagate that change to a textscreen.
///
private void OnDelayChangedMessage(EntityUid uid, SignalTimerComponent component, SignalTimerDelayChangedMessage args)
{
if (!IsMessageValid(uid, args))
return;
component.Delay = Math.Min(args.Delay.TotalSeconds, component.MaxDuration);
_appearanceSystem.SetData(uid, TextScreenVisuals.TargetTime, component.Delay);
}
///
/// Called by to instantiate an ,
/// clear , propagate those changes, and invoke the start port.
///
private void OnTimerStartMessage(EntityUid uid, SignalTimerComponent component, SignalTimerStartMessage args)
{
if (!IsMessageValid(uid, args))
return;
// feedback received: pressing the timer button while a timer is running should cancel the timer.
if (HasComp(uid))
{
_appearanceSystem.SetData(uid, TextScreenVisuals.TargetTime, _gameTiming.CurTime);
Trigger(uid, component);
}
else
OnStartTimer(uid, component);
}
private void OnSignalReceived(EntityUid uid, SignalTimerComponent component, ref SignalReceivedEvent args)
{
if (args.Port == component.Trigger)
{
OnStartTimer(uid, component);
}
}
public void OnStartTimer(EntityUid uid, SignalTimerComponent component)
{
TryComp(uid, out var appearance);
var timer = EnsureComp(uid);
timer.TriggerTime = _gameTiming.CurTime + TimeSpan.FromSeconds(component.Delay);
if (appearance != null)
{
_appearanceSystem.SetData(uid, TextScreenVisuals.TargetTime, timer.TriggerTime, appearance);
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, string.Empty, appearance);
}
_signalSystem.InvokePort(uid, component.StartPort);
}
}