[Entity] Brig Timers (#15285)

* brigtimer

* ok

* TextScreen w timer implementation

* second commit

* working brig timer

* signal timers near completion

* soon done

* removed licenses, fixes noRotation on screens, minor edits

* no message

* no message

* removed my last todos

* removed csproj.rej??

* missed a thing with .yml and tests

* fix tests

* Update base_structureairlocks.yml

* timespan type serialize

* activation turned into comp

* sloth review

* Update timer.yml

* small changes

---------

Co-authored-by: CommieFlowers <rasmus.cedergren@hotmail.com>
Co-authored-by: rolfero <45628623+rolfero@users.noreply.github.com>
This commit is contained in:
Nemanja
2023-04-19 03:47:01 -04:00
committed by GitHub
parent 7fe07fb01d
commit 31851e5468
67 changed files with 1256 additions and 1 deletions

View File

@@ -0,0 +1,81 @@
using Content.Shared.MachineLinking;
using Robust.Client.GameObjects;
using Robust.Shared.Timing;
namespace Content.Client.MachineLinking.UI;
public sealed class SignalTimerBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
private SignalTimerWindow? _window;
public SignalTimerBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = new SignalTimerWindow(this);
if (State != null)
UpdateState(State);
_window.OpenCentered();
_window.OnClose += Close;
_window.OnCurrentTextChanged += OnTextChanged;
_window.OnCurrentDelayMinutesChanged += OnDelayChanged;
_window.OnCurrentDelaySecondsChanged += OnDelayChanged;
}
public void OnStartTimer()
{
SendMessage(new SignalTimerStartMessage());
}
private void OnTextChanged(string newText)
{
SendMessage(new SignalTimerTextChangedMessage(newText));
}
private void OnDelayChanged(string newDelay)
{
if (_window == null)
return;
SendMessage(new SignalTimerDelayChangedMessage(_window.GetDelay()));
}
public TimeSpan GetCurrentTime()
{
return _gameTiming.CurTime;
}
/// <summary>
/// Update the UI state based on server-sent info
/// </summary>
/// <param name="state"></param>
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_window == null || state is not SignalTimerBoundUserInterfaceState cast)
return;
_window.SetCurrentText(cast.CurrentText);
_window.SetCurrentDelayMinutes(cast.CurrentDelayMinutes);
_window.SetCurrentDelaySeconds(cast.CurrentDelaySeconds);
_window.SetShowText(cast.ShowText);
_window.SetTriggerTime(cast.TriggerTime);
_window.SetTimerStarted(cast.TimerStarted);
_window.SetHasAccess(cast.HasAccess);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing) return;
_window?.Dispose();
}
}

View File

@@ -0,0 +1,17 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc signal-timer-menu-title}">
<BoxContainer Orientation="Vertical" SeparationOverride="4" MinWidth="150">
<BoxContainer Name="TextEdit" Orientation="Horizontal">
<Label Name="CurrentLabel" Text="{Loc signal-timer-menu-label}" />
<LineEdit Name="CurrentTextEdit" MinWidth="80" />
</BoxContainer>
<BoxContainer Name="DelayEdit" Orientation="Horizontal">
<Label Name="CurrentDelay" Text="{Loc signal-timer-menu-delay}" />
<LineEdit Name="CurrentDelayEditMinutes" MinWidth="32" />
<Label Name="Colon" Text=":" />
<LineEdit Name="CurrentDelayEditSeconds" MinWidth="32" />
<Label Name="DelayInfo" Text=" (mm:ss)" />
</BoxContainer>
<Button Name="StartTimer" Text="{Loc signal-timer-menu-start}" />
</BoxContainer>
</DefaultWindow>

View File

@@ -0,0 +1,192 @@
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
using Content.Client.TextScreen;
namespace Content.Client.MachineLinking.UI;
[GenerateTypedNameReferences]
public sealed partial class SignalTimerWindow : DefaultWindow
{
private const int MaxTextLength = 5;
public event Action<string>? OnCurrentTextChanged;
public event Action<string>? OnCurrentDelayMinutesChanged;
public event Action<string>? OnCurrentDelaySecondsChanged;
private readonly SignalTimerBoundUserInterface _owner;
private TimeSpan? _triggerTime;
private bool _timerStarted;
public SignalTimerWindow(SignalTimerBoundUserInterface owner)
{
RobustXamlLoader.Load(this);
_owner = owner;
CurrentTextEdit.OnTextChanged += e => OnCurrentTextChange(e.Text);
CurrentDelayEditMinutes.OnTextChanged += e => OnCurrentDelayMinutesChange(e.Text);
CurrentDelayEditSeconds.OnTextChanged += e => OnCurrentDelaySecondsChange(e.Text);
StartTimer.OnPressed += _ => OnStartTimer();
}
public void OnStartTimer()
{
if (!_timerStarted)
{
_timerStarted = true;
_triggerTime = _owner.GetCurrentTime() + GetDelay();
}
else
{
SetTimerStarted(false);
}
_owner.OnStartTimer();
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!_timerStarted || _triggerTime == null)
return;
if (_owner.GetCurrentTime() < _triggerTime.Value)
{
StartTimer.Text = TextScreenSystem.TimeToString(_triggerTime.Value - _owner.GetCurrentTime());
}
else
{
SetTimerStarted(false);
}
}
public void OnCurrentTextChange(string text)
{
if (CurrentTextEdit.Text.Length > MaxTextLength)
{
CurrentTextEdit.Text = CurrentTextEdit.Text.Remove(MaxTextLength);
CurrentTextEdit.CursorPosition = MaxTextLength;
}
OnCurrentTextChanged?.Invoke(text);
}
public void OnCurrentDelayMinutesChange(string text)
{
List<char> toRemove = new();
foreach (var a in text)
{
if (!char.IsDigit(a))
toRemove.Add(a);
}
foreach (var a in toRemove)
{
CurrentDelayEditMinutes.Text = text.Replace(a.ToString(),"");
}
if (CurrentDelayEditMinutes.Text == "")
return;
while (CurrentDelayEditMinutes.Text[0] == '0' && CurrentDelayEditMinutes.Text.Length > 2)
{
CurrentDelayEditMinutes.Text = CurrentDelayEditMinutes.Text.Remove(0, 1);
}
if (CurrentDelayEditMinutes.Text.Length > 2)
{
CurrentDelayEditMinutes.Text = CurrentDelayEditMinutes.Text.Remove(2);
}
OnCurrentDelayMinutesChanged?.Invoke(CurrentDelayEditMinutes.Text);
}
public void OnCurrentDelaySecondsChange(string text)
{
List<char> toRemove = new();
foreach (var a in text)
{
if (!char.IsDigit(a))
toRemove.Add(a);
}
foreach (var a in toRemove)
{
CurrentDelayEditSeconds.Text = text.Replace(a.ToString(), "");
}
if (CurrentDelayEditSeconds.Text == "")
return;
while (CurrentDelayEditSeconds.Text[0] == '0' && CurrentDelayEditSeconds.Text.Length > 2)
{
CurrentDelayEditSeconds.Text = CurrentDelayEditSeconds.Text.Remove(0, 1);
}
if (CurrentDelayEditSeconds.Text.Length > 2)
{
CurrentDelayEditSeconds.Text = CurrentDelayEditSeconds.Text.Remove(2);
}
OnCurrentDelaySecondsChanged?.Invoke(CurrentDelayEditSeconds.Text);
}
public void SetCurrentText(string text)
{
CurrentTextEdit.Text = text;
}
public void SetCurrentDelayMinutes(string delay)
{
CurrentDelayEditMinutes.Text = delay;
}
public void SetCurrentDelaySeconds(string delay)
{
CurrentDelayEditSeconds.Text = delay;
}
public void SetShowText(bool showTime)
{
TextEdit.Visible = showTime;
}
public void SetTriggerTime(TimeSpan timeSpan)
{
_triggerTime = timeSpan;
}
public void SetTimerStarted(bool timerStarted)
{
_timerStarted = timerStarted;
if (!timerStarted)
StartTimer.Text = Loc.GetString("signal-timer-menu-start");
}
/// <summary>
/// Disables fields and buttons if you don't have the access.
/// </summary>
public void SetHasAccess(bool hasAccess)
{
CurrentTextEdit.Editable = hasAccess;
CurrentDelayEditMinutes.Editable = hasAccess;
CurrentDelayEditSeconds.Editable = hasAccess;
StartTimer.Disabled = !hasAccess;
}
/// <summary>
/// Returns a TimeSpan from the currently entered delay.
/// </summary>
public TimeSpan GetDelay()
{
if (!double.TryParse(CurrentDelayEditMinutes.Text, out var minutes))
minutes = 0;
if (!double.TryParse(CurrentDelayEditSeconds.Text, out var seconds))
seconds = 0;
return TimeSpan.FromMinutes(minutes) + TimeSpan.FromSeconds(seconds);
}
}

View File

@@ -0,0 +1,295 @@
using Content.Shared.TextScreen;
using Robust.Client.GameObjects;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.TextScreen;
/// <summary>
/// The TextScreenSystem draws text in the game world using 3x5 sprite states for each character.
/// </summary>
public sealed class TextScreenSystem : VisualizerSystem<TextScreenVisualsComponent>
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
/// <summary>
/// Contains char/state Key/Value pairs. <br/>
/// The states in Textures/Effects/text.rsi that special character should be replaced with.
/// </summary>
private static readonly Dictionary<char, string> CharStatePairs = new()
{
{ ':', "colon" },
{ '!', "exclamation" },
{ '?', "question" },
{ '*', "star" },
{ '+', "plus" },
{ '-', "dash" },
{ ' ', "blank" }
};
private const string DefaultState = "blank";
/// <summary>
/// A string prefix for all text layers.
/// </summary>
private const string TextScreenLayerMapKey = "textScreenLayerMapKey";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TextScreenVisualsComponent, ComponentInit>(OnInit);
}
private void OnInit(EntityUid uid, TextScreenVisualsComponent component, ComponentInit args)
{
if (!TryComp(uid, out SpriteComponent? sprite))
return;
ResetTextLength(uid, component, sprite);
PrepareLayerStatesToDraw(uid, component, sprite);
UpdateLayersToDraw(uid, component, sprite);
}
/// <summary>
/// Resets all TextScreenComponent sprite layers, through removing them and then creating new ones.
/// </summary>
public void ResetTextLength(EntityUid uid, TextScreenVisualsComponent component, SpriteComponent? sprite = null)
{
if (!Resolve(uid, ref sprite))
return;
foreach (var (key, _) in component.LayerStatesToDraw)
{
sprite.RemoveLayer(key);
}
component.LayerStatesToDraw.Clear();
var length = component.TextLength;
component.TextLength = 0;
SetTextLength(uid, component, length, sprite);
}
/// <summary>
/// Sets <see cref="TextScreenVisualsComponent.TextLength"/>, adding or removing sprite layers if necessary.
/// </summary>
public void SetTextLength(EntityUid uid, TextScreenVisualsComponent component, int newLength, SpriteComponent? sprite = null)
{
if (newLength == component.TextLength)
return;
if (!Resolve(uid, ref sprite))
return;
if (newLength > component.TextLength)
{
for (var i = component.TextLength; i < newLength; i++)
{
sprite.LayerMapReserveBlank(TextScreenLayerMapKey + i);
component.LayerStatesToDraw.Add(TextScreenLayerMapKey + i, null);
sprite.LayerSetRSI(TextScreenLayerMapKey + i, new ResourcePath("Effects/text.rsi"));
sprite.LayerSetColor(TextScreenLayerMapKey + i, component.Color);
sprite.LayerSetState(TextScreenLayerMapKey + i, DefaultState);
}
}
else
{
for (var i = component.TextLength; i > newLength; i--)
{
sprite.LayerMapGet(TextScreenLayerMapKey + (i - 1));
component.LayerStatesToDraw.Remove(TextScreenLayerMapKey + (i - 1));
sprite.RemoveLayer(TextScreenLayerMapKey + (i - 1));
}
}
UpdateOffsets(uid, component, sprite);
component.TextLength = newLength;
}
/// <summary>
/// Updates the layers offsets based on the text length, so it is drawn correctly.
/// </summary>
public void UpdateOffsets(EntityUid uid, TextScreenVisualsComponent component, SpriteComponent? sprite = null)
{
if (!Resolve(uid, ref sprite))
return;
for (var i = 0; i < component.LayerStatesToDraw.Count; i++)
{
var offset = i - (component.LayerStatesToDraw.Count - 1) / 2.0f;
sprite.LayerSetOffset(TextScreenLayerMapKey + i, new Vector2(offset * TextScreenVisualsComponent.PixelSize * 4f, 0.0f) + component.TextOffset);
}
}
protected override void OnAppearanceChange(EntityUid uid, TextScreenVisualsComponent component, ref AppearanceChangeEvent args)
{
UpdateAppearance(uid, component, args.Component, args.Sprite);
}
public void UpdateAppearance(EntityUid uid, TextScreenVisualsComponent component, AppearanceComponent? appearance = null, SpriteComponent? sprite = null)
{
if (!Resolve(uid, ref appearance, ref sprite))
return;
if (AppearanceSystem.TryGetData(uid, TextScreenVisuals.On, out bool on, appearance))
{
component.Activated = on;
UpdateVisibility(uid, component, sprite);
}
if (AppearanceSystem.TryGetData(uid, TextScreenVisuals.Mode, out TextScreenMode mode, appearance))
{
component.CurrentMode = mode;
if (component.CurrentMode == TextScreenMode.Timer)
EnsureComp<TextScreenTimerComponent>(uid);
else
RemComp<TextScreenTimerComponent>(uid);
UpdateText(component);
}
if (AppearanceSystem.TryGetData(uid, TextScreenVisuals.TargetTime, out TimeSpan time, appearance))
{
component.TargetTime = time;
}
if (AppearanceSystem.TryGetData(uid, TextScreenVisuals.ScreenText, out string text, appearance))
{
component.Text = text;
}
UpdateText(component);
PrepareLayerStatesToDraw(uid, component, sprite);
UpdateLayersToDraw(uid, component, sprite);
}
/// <summary>
/// If currently in <see cref="TextScreenMode.Text"/> mode: <br/>
/// Sets <see cref="TextScreenVisualsComponent.TextToDraw"/> to the value of <see cref="TextScreenVisualsComponent.Text"/>
/// </summary>
public static void UpdateText(TextScreenVisualsComponent component)
{
if (component.CurrentMode == TextScreenMode.Text)
component.TextToDraw = component.Text;
}
/// <summary>
/// Sets visibility of text to <see cref="TextScreenVisualsComponent.Activated"/>.
/// </summary>
public void UpdateVisibility(EntityUid uid, TextScreenVisualsComponent component, SpriteComponent? sprite = null)
{
if (!Resolve(uid, ref sprite))
return;
foreach (var (key, _) in component.LayerStatesToDraw)
{
sprite.LayerSetVisible(key, component.Activated);
}
}
/// <summary>
/// Sets the states in the <see cref="TextScreenVisualsComponent.LayerStatesToDraw"/> to match the component <see cref="TextScreenVisualsComponent.TextToDraw"/> string.
/// </summary>
/// <remarks>
/// Remember to set <see cref="TextScreenVisualsComponent.TextToDraw"/> to a string first.
/// </remarks>
public void PrepareLayerStatesToDraw(EntityUid uid, TextScreenVisualsComponent component, SpriteComponent? sprite = null)
{
if (!Resolve(uid, ref sprite))
return;
for (var i = 0; i < component.TextLength; i++)
{
if (i >= component.TextToDraw.Length)
{
component.LayerStatesToDraw[TextScreenLayerMapKey + i] = DefaultState;
continue;
}
component.LayerStatesToDraw[TextScreenLayerMapKey + i] = GetStateFromChar(component.TextToDraw[i]);
}
}
/// <summary>
/// Iterates through <see cref="TextScreenVisualsComponent.LayerStatesToDraw"/>, setting sprite states to the appropriate layers.
/// </summary>
public void UpdateLayersToDraw(EntityUid uid, TextScreenVisualsComponent component, SpriteComponent? sprite = null)
{
if (!Resolve(uid, ref sprite))
return;
foreach (var (key, state) in component.LayerStatesToDraw)
{
if (state == null)
continue;
sprite.LayerSetState(key, state);
}
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<TextScreenVisualsComponent, TextScreenTimerComponent>();
while (query.MoveNext(out var uid, out var comp, out _))
{
// Basically Abs(TimeSpan, TimeSpan) -> Gives the difference between the current time and the target time.
var timeToShow = _gameTiming.CurTime > comp.TargetTime ? _gameTiming.CurTime - comp.TargetTime : comp.TargetTime - _gameTiming.CurTime;
comp.TextToDraw = TimeToString(timeToShow, false);
PrepareLayerStatesToDraw(uid, comp);
UpdateLayersToDraw(uid, comp);
}
}
/// <summary>
/// Returns the <paramref name="timeSpan"/> converted to a string in either HH:MM, MM:SS or potentially SS:mm format.
/// </summary>
/// <param name="timeSpan">TimeSpan to convert into string.</param>
/// <param name="getMilliseconds">Should the string be ss:ms if minutes are less than 1?</param>
public static string TimeToString(TimeSpan timeSpan, bool getMilliseconds = true)
{
string firstString;
string lastString;
if (timeSpan.TotalHours >= 1)
{
firstString = timeSpan.Hours.ToString("D2");
lastString = timeSpan.Minutes.ToString("D2");
}
else if (timeSpan.TotalMinutes >= 1 || !getMilliseconds)
{
firstString = timeSpan.Minutes.ToString("D2");
// It's nicer to see a timer set at 5 seconds actually start at 00:05 instead of 00:04.
var seconds = timeSpan.Seconds + (timeSpan.Milliseconds > 500 ? 1 : 0);
lastString = seconds.ToString("D2");
}
else
{
firstString = timeSpan.Seconds.ToString("D2");
var centiseconds = timeSpan.Milliseconds / 10;
lastString = centiseconds.ToString("D2");
}
return firstString + ':' + lastString;
}
/// <summary>
/// Returns the Effects/text.rsi state string based on <paramref name="character"/>, or null if none available.
/// </summary>
public static string? GetStateFromChar(char? character)
{
if (character == null)
return null;
// First checks if its one of our special characters
if (CharStatePairs.ContainsKey(character.Value))
return CharStatePairs[character.Value];
// Or else it checks if its a normal letter or digit
if (char.IsLetterOrDigit(character.Value))
return character.Value.ToString().ToLower();
return null;
}
}

View File

@@ -0,0 +1,10 @@
namespace Content.Client.TextScreen;
/// <summary>
/// This is an active component for tracking <see cref="TextScreenVisualsComponent"/>
/// </summary>
[RegisterComponent]
public sealed class TextScreenTimerComponent : Component
{
}

View File

@@ -0,0 +1,66 @@
using Content.Shared.TextScreen;
using Robust.Client.Graphics;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Client.TextScreen;
[RegisterComponent]
public sealed class TextScreenVisualsComponent : Component
{
/// <summary>
/// 1/32 - the size of a pixel
/// </summary>
public const float PixelSize = 1f / EyeManager.PixelsPerMeter;
/// <summary>
/// The color of the text drawn.
/// </summary>
[DataField("color")]
public Color Color { get; set; } = Color.FloralWhite;
/// <summary>
/// Whether the screen is on.
/// </summary>
[DataField("activated")]
public bool Activated;
/// <summary>
/// The current mode of the screen - is it showing text, or currently counting?
/// </summary>
[DataField("currentMode")]
public TextScreenMode CurrentMode = TextScreenMode.Text;
/// <summary>
/// The time it is counting to or from.
/// </summary>
[DataField("targetTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan TargetTime = TimeSpan.Zero;
/// <summary>
/// Offset for drawing the text. <br/>
/// (0, 8) pixels is the default for the Structures\Wallmounts\textscreen.rsi
/// </summary>
[DataField("textOffset"), ViewVariables(VVAccess.ReadWrite)]
public Vector2 TextOffset = new(0f, 8f * PixelSize);
/// <summary>
/// The amount of characters this component can show.
/// </summary>
[DataField("textLength")]
public int TextLength = 5;
/// <summary>
/// Text the screen should show when it's not counting.
/// </summary>
[DataField("text"), ViewVariables(VVAccess.ReadWrite)]
public string Text = "";
public string TextToDraw = "";
/// <summary>
/// The different layers for each character - this is the currently drawn states.
/// </summary>
[DataField("layerStatesToDraw")]
public Dictionary<string, string?> LayerStatesToDraw = new();
}

View File

@@ -8,6 +8,8 @@ using Content.Shared.Doors.Systems;
using Content.Shared.Interaction;
using Robust.Server.GameObjects;
using Content.Shared.Wires;
using Content.Server.MachineLinking.Events;
using Content.Server.MachineLinking.System;
namespace Content.Server.Doors.Systems
{
@@ -15,12 +17,15 @@ namespace Content.Server.Doors.Systems
{
[Dependency] private readonly WiresSystem _wiresSystem = default!;
[Dependency] private readonly PowerReceiverSystem _power = default!;
[Dependency] private readonly SignalLinkerSystem _signalSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AirlockComponent, ComponentInit>(OnAirlockInit);
SubscribeLocalEvent<AirlockComponent, SignalReceivedEvent>(OnSignalReceived);
SubscribeLocalEvent<AirlockComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<AirlockComponent, DoorStateChangedEvent>(OnStateChanged);
SubscribeLocalEvent<AirlockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
@@ -28,6 +33,7 @@ namespace Content.Server.Doors.Systems
SubscribeLocalEvent<AirlockComponent, ActivateInWorldEvent>(OnActivate, before: new [] {typeof(DoorSystem)});
SubscribeLocalEvent<AirlockComponent, DoorGetPryTimeModifierEvent>(OnGetPryMod);
SubscribeLocalEvent<AirlockComponent, BeforeDoorPryEvent>(OnDoorPry);
}
private void OnAirlockInit(EntityUid uid, AirlockComponent component, ComponentInit args)
@@ -38,6 +44,14 @@ namespace Content.Server.Doors.Systems
}
}
private void OnSignalReceived(EntityUid uid, AirlockComponent component, SignalReceivedEvent args)
{
if (args.Port == component.AutoClosePort)
{
component.AutoClose = false;
}
}
private void OnPowerChanged(EntityUid uid, AirlockComponent component, ref PowerChangedEvent args)
{
if (TryComp<AppearanceComponent>(uid, out var appearanceComponent))

View File

@@ -0,0 +1,15 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.MachineLinking.Components
{
[RegisterComponent]
public sealed class ActiveSignalTimerComponent : Component
{
/// <summary>
/// The time the timer triggers.
/// </summary>
[DataField("triggerTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan TriggerTime;
}
}

View File

@@ -0,0 +1,42 @@
using Content.Shared.MachineLinking;
using Robust.Shared.Audio;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.MachineLinking.Components;
[RegisterComponent]
public sealed class SignalTimerComponent : Component
{
[DataField("delay"), ViewVariables(VVAccess.ReadWrite)]
public double Delay = 5;
/// <summary>
/// This shows the Label: text box in the UI.
/// </summary>
[DataField("canEditLabel"), ViewVariables(VVAccess.ReadWrite)]
public bool CanEditLabel = true;
/// <summary>
/// The label, used for TextScreen visuals currently.
/// </summary>
[DataField("label"), ViewVariables(VVAccess.ReadWrite)]
public string Label = "";
/// <summary>
/// The port that gets signaled when the timer triggers.
/// </summary>
[DataField("triggerPort", customTypeSerializer: typeof(PrototypeIdSerializer<TransmitterPortPrototype>)), ViewVariables(VVAccess.ReadWrite)]
public string TriggerPort = "Timer";
/// <summary>
/// The port that gets signaled when the timer starts.
/// </summary>
[DataField("startPort", customTypeSerializer: typeof(PrototypeIdSerializer<TransmitterPortPrototype>)), ViewVariables(VVAccess.ReadWrite)]
public string StartPort = "Start";
/// <summary>
/// If not null, this timer will play this sound when done.
/// </summary>
[DataField("doneSound"), ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier? DoneSound;
}

View File

@@ -0,0 +1,162 @@
using Robust.Shared.Timing;
using Content.Server.MachineLinking.Components;
using Content.Shared.TextScreen;
using Robust.Server.GameObjects;
using Content.Shared.MachineLinking;
using Content.Server.UserInterface;
using Content.Shared.Access.Systems;
using Content.Server.Interaction;
namespace Content.Server.MachineLinking.System;
public sealed class SignalTimerSystem : EntitySystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SignalLinkerSystem _signalSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly InteractionSystem _interaction = default!;
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);
}
private void OnInit(EntityUid uid, SignalTimerComponent component, ComponentInit args)
{
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, component.Label);
}
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)));
}
}
public void Trigger(EntityUid uid, SignalTimerComponent signalTimer)
{
RemComp<ActiveSignalTimerComponent>(uid);
_signalSystem.InvokePort(uid, signalTimer.TriggerPort);
_appearanceSystem.SetData(uid, TextScreenVisuals.Mode, TextScreenMode.Text);
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()
{
var query = EntityQueryEnumerator<ActiveSignalTimerComponent, SignalTimerComponent>();
while (query.MoveNext(out var uid, out var active, out var timer))
{
if (active.TriggerTime > _gameTiming.CurTime)
continue;
Trigger(uid, timer);
if (timer.DoneSound == null)
continue;
_audio.PlayPvs(timer.DoneSound, uid);
}
}
/// <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>
/// <param name="message"></param>
private bool IsMessageValid(EntityUid uid, BoundUserInterfaceMessage message)
{
if (message.Session.AttachedEntity is not { Valid: true } mob)
return false;
if (!_accessReader.IsAllowed(mob, uid))
return false;
return true;
}
private void OnTextChangedMessage(EntityUid uid, SignalTimerComponent component, SignalTimerTextChangedMessage args)
{
if (!IsMessageValid(uid, args))
return;
component.Label = args.Text[..Math.Min(5,args.Text.Length)];
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, component.Label);
}
private void OnDelayChangedMessage(EntityUid uid, SignalTimerComponent component, SignalTimerDelayChangedMessage args)
{
if (!IsMessageValid(uid, args))
return;
component.Delay = args.Delay.TotalSeconds;
}
private void OnTimerStartMessage(EntityUid uid, SignalTimerComponent component, SignalTimerStartMessage args)
{
if (!IsMessageValid(uid, args))
return;
TryComp<AppearanceComponent>(uid, out var appearance);
if (!HasComp<ActiveSignalTimerComponent>(uid))
{
var activeTimer = EnsureComp<ActiveSignalTimerComponent>(uid);
activeTimer.TriggerTime = _gameTiming.CurTime + TimeSpan.FromSeconds(component.Delay);
if (appearance != null)
{
_appearanceSystem.SetData(uid, TextScreenVisuals.Mode, TextScreenMode.Timer, appearance);
_appearanceSystem.SetData(uid, TextScreenVisuals.TargetTime, activeTimer.TriggerTime, appearance);
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, component.Label, appearance);
}
_signalSystem.InvokePort(uid, component.StartPort);
}
else
{
RemComp<ActiveSignalTimerComponent>(uid);
if (appearance != null)
{
_appearanceSystem.SetData(uid, TextScreenVisuals.Mode, TextScreenMode.Text, appearance);
_appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, component.Label, appearance);
}
}
}
}

View File

@@ -1,8 +1,10 @@
using System.Threading;
using Content.Shared.Doors.Systems;
using Content.Shared.MachineLinking;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Doors.Components;
@@ -82,6 +84,12 @@ public sealed class AirlockComponent : Component
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
public float AutoCloseDelayModifier = 1.0f;
/// <summary>
/// The receiver port for turning off automatic closing.
/// </summary>
[DataField("autoClosePort", customTypeSerializer: typeof(PrototypeIdSerializer<ReceiverPortPrototype>))]
public string AutoClosePort = "AutoClose";
}
[Serializable, NetSerializable]

View File

@@ -0,0 +1,68 @@
using Robust.Shared.Serialization;
namespace Content.Shared.MachineLinking;
[Serializable, NetSerializable]
public enum SignalTimerUiKey : byte
{
Key
}
/// <summary>
/// Represents a SignalTimerComponent state that can be sent to the client
/// </summary>
[Serializable, NetSerializable]
public sealed class SignalTimerBoundUserInterfaceState : BoundUserInterfaceState
{
public string CurrentText;
public string CurrentDelayMinutes;
public string CurrentDelaySeconds;
public bool ShowText;
public TimeSpan TriggerTime;
public bool TimerStarted;
public bool HasAccess;
public SignalTimerBoundUserInterfaceState(string currentText,
string currentDelayMinutes,
string currentDelaySeconds,
bool showText,
TimeSpan triggerTime,
bool timerStarted,
bool hasAccess)
{
CurrentText = currentText;
CurrentDelayMinutes = currentDelayMinutes;
CurrentDelaySeconds = currentDelaySeconds;
ShowText = showText;
TriggerTime = triggerTime;
TimerStarted = timerStarted;
HasAccess = hasAccess;
}
}
[Serializable, NetSerializable]
public sealed class SignalTimerTextChangedMessage : BoundUserInterfaceMessage
{
public string Text { get; }
public SignalTimerTextChangedMessage(string text)
{
Text = text;
}
}
[Serializable, NetSerializable]
public sealed class SignalTimerDelayChangedMessage : BoundUserInterfaceMessage
{
public TimeSpan Delay { get; }
public SignalTimerDelayChangedMessage(TimeSpan delay)
{
Delay = delay;
}
}
[Serializable, NetSerializable]
public sealed class SignalTimerStartMessage : BoundUserInterfaceMessage
{
}

View File

@@ -0,0 +1,35 @@
using Robust.Shared.Serialization;
namespace Content.Shared.TextScreen;
[Serializable, NetSerializable]
public enum TextScreenVisuals : byte
{
/// <summary>
/// Should this show any text? <br/>
/// Expects a <see cref="bool"/>.
/// </summary>
On,
/// <summary>
/// Is this a timer or a text-screen? <br/>
/// Expects a <see cref="TextScreenMode"/>.
/// </summary>
Mode,
/// <summary>
/// What text to show? <br/>
/// Expects a <see cref="string"/>.
/// </summary>
ScreenText,
/// <summary>
/// What is the target time? <br/>
/// Expects a <see cref="TimeSpan"/>.
/// </summary>
TargetTime
}
[Serializable, NetSerializable]
public enum TextScreenMode : byte
{
Text,
Timer
}

View File

@@ -0,0 +1,4 @@
signal-timer-menu-title = Timer
signal-timer-menu-label = Label:
signal-timer-menu-delay = Delay:
signal-timer-menu-start = Start

View File

@@ -46,6 +46,9 @@ signal-port-description-med-scanner-sender = Medical scanner signal sender
signal-port-name-med-scanner-receiver = Medical scanner
signal-port-description-med-scanner-receiver = Medical scanner signal receiver
signal-port-name-hold-open = Hold
signal-port-description-hold-open = Turns off automatic closing.
signal-port-name-artifact-analyzer-sender = Console
signal-port-description-artifact-analyzer-sender = Analysis console signal sender

View File

@@ -15,3 +15,9 @@ signal-port-description-right = This port is invoked whenever the lever is moved
signal-port-name-middle = Middle
signal-port-description-middle = This port is invoked whenever the lever is moved to the neutral position.
signal-port-name-timer-trigger = Timer Trigger
signal-port-description-timer-trigger = This port is invoked whenever the timer triggers.
signal-port-name-timer-start = Timer Start
signal-port-description-timer-start = This port is invoked whenever the timer starts.

View File

@@ -83,6 +83,7 @@
Open: []
Close: []
Toggle: []
AutoClose: []
- type: UserInterface
interfaces:
- key: enum.WiresUiKey.Key

View File

@@ -49,6 +49,7 @@
Open: []
Close: []
Toggle: []
AutoClose: []
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Glass

View File

@@ -0,0 +1,64 @@
- type: entity
id: SignalTimer
name: signal timer
description: It's a timer for sending timed signals to things.
placement:
mode: SnapgridCenter
snap:
- Wallmount
components:
- type: Transform
anchored: true
- type: WallMount
arc: 360
- type: Clickable
- type: InteractionOutline
- type: Sprite
sprite: Structures/Wallmounts/switch.rsi
netsync: false
state: on
- type: Appearance
- type: Rotatable
- type: Fixtures
- type: SignalTimer
canEditLabel: false
- type: SignalTransmitter
outputs:
Start: []
Timer: []
- type: ActivatableUI
key: enum.SignalTimerUiKey.Key
- type: UserInterface
interfaces:
- key: enum.SignalTimerUiKey.Key
type: SignalTimerBoundUserInterface
- type: ApcPowerReceiver
powerLoad: 100
- type: Electrified
enabled: false
usesApcPower: true
- type: ExtensionCableReceiver
- type: ActivatableUIRequiresPower
- type: entity
id: ScreenTimer
parent: SignalTimer
name: screen timer
description: It's a timer for sending timed signals to things, with a built-in screen.
components:
- type: SignalTimer
canEditLabel: true
- type: TextScreenVisuals
- type: Sprite
sprite: Structures/Wallmounts/textscreen.rsi
state: textscreen
noRot: true
- type: entity
id: BrigTimer
parent: ScreenTimer
name: brig timer
description: It's a timer for brig cells.
components:
- type: AccessReader
access: [["Security"]]

View File

@@ -63,6 +63,11 @@
name: signal-port-name-med-scanner-receiver
description: signal-port-description-med-scanner-receiver
- type: receiverPort
id: AutoClose
name: signal-port-name-hold-open
description: signal-port-description-hold-open
- type: receiverPort
id: ArtifactAnalyzerReceiver
name: signal-port-name-artifact-analyzer-receiver

View File

@@ -50,6 +50,18 @@
name: signal-port-name-med-scanner-sender
description: signal-port-description-med-scanner-sender
- type: transmitterPort
id: Timer
name: signal-port-name-timer-trigger
description: signal-port-description-timer-trigger
defaultLinks: [ AutoClose, On, Open, Forward, Trigger ]
- type: transmitterPort
id: Start
name: signal-port-name-timer-start
description: signal-port-description-timer-start
defaultLinks: [ Close, Off ]
- type: transmitterPort
id: ArtifactAnalyzerSender
name: signal-port-name-artifact-analyzer-sender

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

View File

@@ -0,0 +1,140 @@
{
"version": 1,
"license": "CC0-1.0",
"copyright": "Created by rolfero (github) for Space Station 14",
"size": {
"x": 4,
"y": 6
},
"states": [
{
"name": "0"
},
{
"name": "1"
},
{
"name": "2"
},
{
"name": "3"
},
{
"name": "4"
},
{
"name": "5"
},
{
"name": "6"
},
{
"name": "7"
},
{
"name": "8"
},
{
"name": "9"
},
{
"name": "a"
},
{
"name": "b"
},
{
"name": "c"
},
{
"name": "d"
},
{
"name": "e"
},
{
"name": "f"
},
{
"name": "g"
},
{
"name": "h"
},
{
"name": "i"
},
{
"name": "j"
},
{
"name": "k"
},
{
"name": "l"
},
{
"name": "m"
},
{
"name": "n"
},
{
"name": "o"
},
{
"name": "p"
},
{
"name": "q"
},
{
"name": "r"
},
{
"name": "s"
},
{
"name": "t"
},
{
"name": "u"
},
{
"name": "v"
},
{
"name": "w"
},
{
"name": "x"
},
{
"name": "y"
},
{
"name": "z"
},
{
"name": "exclamation"
},
{
"name": "question"
},
{
"name": "star"
},
{
"name": "plus"
},
{
"name": "dash"
},
{
"name": "colon"
},
{
"name": "blank"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 B

View File

@@ -0,0 +1,14 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Made by brainfood1183 (Github) for Space Station 14",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "textscreen"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B