# Conflicts: # Content.Client/Access/AccessOverlay.cs # Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs # Content.Client/Access/UI/IdCardConsoleWindow.xaml # Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs # Content.Client/Chemistry/UI/InjectorStatusControl.cs # Content.Client/StatusIcon/StatusIconOverlay.cs # Content.Client/Stylesheets/StyleNano.cs # Content.Client/UserInterface/Systems/Chat/ChatUIController.cs # Content.Client/UserInterface/Systems/Chat/Widgets/ChatBox.xaml # Content.Server/Access/Systems/IdCardConsoleSystem.cs # Content.Server/Administration/Commands/AGhost.cs # Content.Server/Chemistry/EntitySystems/ReagentDispenserSystem.cs # Content.Server/Connection/ConnectionManager.cs # Content.Server/DeviceLinking/Systems/SignalTimerSystem.cs # Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs # Content.Server/GameTicking/GameTicker.RoundFlow.cs # Content.Server/GameTicking/GameTicker.Spawning.cs # Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs # Content.Server/Resist/EscapeInventorySystem.cs # Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs # Content.Shared/Access/Components/IdCardConsoleComponent.cs # Content.Shared/Anomaly/SharedAnomalySystem.cs # Content.Shared/Bed/Sleep/SharedSleepingSystem.cs # Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs # Content.Shared/Lock/LockSystem.cs # Content.Shared/RCD/Systems/RCDSystem.cs # Content.Shared/Roles/JobPrototype.cs # Content.Shared/StatusIcon/StatusIconPrototype.cs # Content.Shared/Weapons/Melee/SharedMeleeWeaponSystem.cs # Resources/Audio/Machines/attributions.yml # Resources/Locale/en-US/rcd/components/rcd-component.ftl # Resources/Maps/reach.yml # Resources/Prototypes/Catalog/Cargo/cargo_vending.yml # Resources/Prototypes/Catalog/Fills/Lockers/heads.yml # Resources/Prototypes/Catalog/Fills/Lockers/security.yml # Resources/Prototypes/Catalog/ReagentDispensers/beverage.yml # Resources/Prototypes/Catalog/VendingMachines/Inventories/boozeomat.yml # Resources/Prototypes/Catalog/VendingMachines/Inventories/cola.yml # Resources/Prototypes/Catalog/VendingMachines/Inventories/lawdrobe.yml # Resources/Prototypes/Catalog/VendingMachines/Inventories/pwrgame.yml # Resources/Prototypes/Catalog/VendingMachines/Inventories/shamblersjuice.yml # Resources/Prototypes/Catalog/VendingMachines/Inventories/soda.yml # Resources/Prototypes/Catalog/VendingMachines/Inventories/spaceup.yml # Resources/Prototypes/Catalog/VendingMachines/Inventories/starkist.yml # Resources/Prototypes/Catalog/VendingMachines/Inventories/theater.yml # Resources/Prototypes/DeviceLinking/sink_ports.yml # Resources/Prototypes/Entities/Clothing/Back/duffel.yml # Resources/Prototypes/Entities/Clothing/Belt/base_clothingbelt.yml # Resources/Prototypes/Entities/Clothing/Neck/misc.yml # Resources/Prototypes/Entities/Clothing/OuterClothing/base_clothingouter.yml # Resources/Prototypes/Entities/Clothing/OuterClothing/wintercoats.yml # Resources/Prototypes/Entities/Mobs/Customization/Markings/gauze.yml # Resources/Prototypes/Entities/Objects/Devices/Electronics/door.yml # Resources/Prototypes/Entities/Objects/Magic/books.yml # Resources/Prototypes/Entities/Objects/Materials/Sheets/glass.yml # Resources/Prototypes/Entities/Objects/Materials/Sheets/metal.yml # Resources/Prototypes/Entities/Objects/Materials/Sheets/other.yml # Resources/Prototypes/Entities/Objects/Misc/tiles.yml # Resources/Prototypes/Entities/Objects/Specific/Medical/morgue.yml # Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml # Resources/Prototypes/Entities/Objects/Weapons/Melee/e_sword.yml # Resources/Prototypes/Entities/Structures/Doors/Airlocks/airlocks.yml # Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_assembly.yml # Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml # Resources/Prototypes/Entities/Structures/Doors/Airlocks/highsec.yml # Resources/Prototypes/Entities/Structures/Doors/Firelocks/firelock.yml # Resources/Prototypes/Entities/Structures/Doors/Firelocks/frame.yml # Resources/Prototypes/Entities/Structures/Doors/MaterialDoors/material_doors.yml # Resources/Prototypes/Entities/Structures/Doors/SecretDoor/secret_door.yml # Resources/Prototypes/Entities/Structures/Doors/Windoors/assembly.yml # Resources/Prototypes/Entities/Structures/Lighting/base_lighting.yml # Resources/Prototypes/Entities/Structures/Machines/lathe.yml # Resources/Prototypes/Entities/Structures/Power/cable_terminal.yml # Resources/Prototypes/Entities/Structures/Storage/Tanks/base_structuretanks.yml # Resources/Prototypes/Entities/Structures/Walls/grille.yml # Resources/Prototypes/Recipes/Construction/Graphs/structures/shutter.yml # Resources/Prototypes/Recipes/Crafting/Graphs/improvised/flowercrown.yml # Resources/Prototypes/Recipes/Crafting/improvised.yml # Resources/Prototypes/Roles/Jobs/Security/detective.yml # Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml # Resources/Prototypes/Roles/Jobs/Security/security_officer.yml # Resources/Prototypes/Roles/Jobs/Security/warden.yml # Resources/Prototypes/StatusEffects/health.yml # Resources/Prototypes/Voice/speech_emotes.yml # Resources/Prototypes/lobbyscreens.yml # Resources/Textures/Clothing/OuterClothing/Hardsuits/ERTSuits/ertchaplain.rsi/equipped-OUTERCLOTHING-body-slim.png # Resources/Textures/Decals/bricktile.rsi/white_box.png # Resources/Textures/Objects/Misc/books.rsi/meta.json # Resources/migration.yml
241 lines
9.6 KiB
C#
241 lines
9.6 KiB
C#
using Content.Server.DeviceLinking.Components;
|
||
using Content.Server.DeviceLinking.Events;
|
||
using Content.Server.Radio.EntitySystems;
|
||
using Content.Shared.UserInterface;
|
||
using Content.Shared.Access.Systems;
|
||
using Content.Shared.MachineLinking;
|
||
using Content.Shared.Radio;
|
||
using Content.Shared.TextScreen;
|
||
using Robust.Server.GameObjects;
|
||
using Robust.Shared.Audio.Systems;
|
||
using Robust.Shared.Prototypes;
|
||
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!;
|
||
[Dependency] private readonly RadioSystem _radioSystem = default!;
|
||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||
|
||
/// <summary>
|
||
/// Per-tick timer cache.
|
||
/// </summary>
|
||
private readonly List<Entity<SignalTimerComponent>> _timers = new();
|
||
|
||
public override void Initialize()
|
||
{
|
||
base.Initialize();
|
||
|
||
SubscribeLocalEvent<SignalTimerComponent, ComponentInit>(OnInit);
|
||
SubscribeLocalEvent<SignalTimerComponent, AfterActivatableUIOpenEvent>(OnAfterActivatableUIOpen);
|
||
|
||
SubscribeLocalEvent<SignalTimerComponent, SignalTimerTextChangedMessage>(OnTextChangedMessage);
|
||
SubscribeLocalEvent<SignalTimerComponent, SignalTimerDelayChangedMessage>(OnDelayChangedMessage);
|
||
SubscribeLocalEvent<SignalTimerComponent, SignalTimerStartMessage>(OnTimerStartMessage);
|
||
SubscribeLocalEvent<SignalTimerComponent, SignalReceivedEvent>(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<ActiveSignalTimerComponent>(uid, out var active) ? active.TriggerTime : TimeSpan.Zero;
|
||
|
||
if (_ui.TryGetUi(uid, SignalTimerUiKey.Key, out var bui))
|
||
{
|
||
_ui.SetUiState(bui, 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)));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Finishes a timer, triggering its main port, and removing its <see cref="ActiveSignalTimerComponent"/>.
|
||
/// </summary>
|
||
public void Trigger(EntityUid uid, SignalTimerComponent signalTimer)
|
||
{
|
||
RemComp<ActiveSignalTimerComponent>(uid);
|
||
|
||
var announceMessage = signalTimer.Label;
|
||
if (string.IsNullOrWhiteSpace(announceMessage)) { announceMessage = Loc.GetString("label-none"); }
|
||
|
||
if (signalTimer.TimerCanAnnounce)
|
||
{
|
||
Report(uid, SignalTimerComponent.SecChannel, "timer-end-announcement", ("Label", announceMessage));
|
||
}
|
||
|
||
_audio.PlayPvs(signalTimer.DoneSound, uid);
|
||
_signalSystem.InvokePort(uid, signalTimer.TriggerPort);
|
||
|
||
if (_ui.TryGetUi(uid, SignalTimerUiKey.Key, out var bui))
|
||
{
|
||
_ui.SetUiState(bui, 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<ActiveSignalTimerComponent, SignalTimerComponent>();
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Checks if a UI <paramref name="message"/> is allowed to be sent by the user.
|
||
/// </summary>
|
||
/// <param name="uid">The entity that is interacted with.</param>
|
||
private bool IsMessageValid(EntityUid uid, BaseBoundUserInterfaceEvent message)
|
||
{
|
||
if (message.Session.AttachedEntity is not { Valid: true } mob)
|
||
return false;
|
||
|
||
return _accessReader.IsAllowed(mob, uid);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Called by <see cref="SignalTimerTextChangedMessage"/> to both
|
||
/// change the default component label, and propagate that change to the TextScreen.
|
||
/// </summary>
|
||
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<ActiveSignalTimerComponent>(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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Called by <see cref="SignalTimerDelayChangedMessage"/> to change the <see cref="SignalTimerComponent"/>
|
||
/// delay, and propagate that change to a textscreen.
|
||
/// </summary>
|
||
private void OnDelayChangedMessage(
|
||
EntityUid uid,
|
||
SignalTimerComponent component,
|
||
SignalTimerDelayChangedMessage args)
|
||
{
|
||
if (!IsMessageValid(uid, args))
|
||
return;
|
||
|
||
component.Delay = args.Delay.TotalSeconds;
|
||
_appearanceSystem.SetData(uid, TextScreenVisuals.TargetTime, component.Delay);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Called by <see cref="SignalTimerStartMessage"/> to instantiate an <see cref="ActiveSignalTimerComponent"/>,
|
||
/// clear <see cref="TextScreenVisuals.ScreenText"/>, propagate those changes, and invoke the start port.
|
||
/// </summary>
|
||
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<ActiveSignalTimerComponent>(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<AppearanceComponent>(uid, out var appearance);
|
||
var timer = EnsureComp<ActiveSignalTimerComponent>(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);
|
||
|
||
var announceMessage = component.Label;
|
||
if (string.IsNullOrWhiteSpace(announceMessage)) { announceMessage = Loc.GetString("label-none"); }
|
||
|
||
//sorry, skill issue
|
||
var time = TimeSpan.FromSeconds(component.Delay);
|
||
var timeFormatted =
|
||
$"{(time.Duration().Hours > 0 ? $"{time.Hours:0} час;{(time.Hours == 1 ? string.Empty : "ов")} " : string.Empty)}{(time.Duration().Minutes > 0 ? $"{time.Minutes:0} минут;{(time.Minutes != 1 ? string.Empty : "а")} " : string.Empty)}{(time.Duration().Seconds > 0 ? $"{time.Seconds:0} секунд{(time.Seconds != 1 ? string.Empty : "а")} " : string.Empty)}";
|
||
|
||
if (component.TimerCanAnnounce)
|
||
{
|
||
Report(uid, SignalTimerComponent.SecChannel, "timer-start-announcement", ("Label", announceMessage),
|
||
("Time", timeFormatted));
|
||
}
|
||
}
|
||
|
||
private void Report(EntityUid source, string channelName, string messageKey, params (string, object)[] args)
|
||
{
|
||
var message = args.Length == 0 ? Loc.GetString(messageKey) : Loc.GetString(messageKey, args);
|
||
var channel = _prototypeManager.Index<RadioChannelPrototype>(channelName);
|
||
_radioSystem.SendRadioMessage(source, message, channel, source);
|
||
}
|
||
}
|