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
}