]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
[Entity] Brig Timers (#15285)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Wed, 19 Apr 2023 07:47:01 +0000 (03:47 -0400)
committerGitHub <noreply@github.com>
Wed, 19 Apr 2023 07:47:01 +0000 (01:47 -0600)
* 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>
67 files changed:
Content.Client/MachineLinking/UI/SignalTimerBoundUserInterface.cs [new file with mode: 0644]
Content.Client/MachineLinking/UI/SignalTimerWindow.xaml [new file with mode: 0644]
Content.Client/MachineLinking/UI/SignalTimerWindow.xaml.cs [new file with mode: 0644]
Content.Client/TextScreen/TextScreenSystem.cs [new file with mode: 0644]
Content.Client/TextScreen/TextScreenTimerComponent.cs [new file with mode: 0644]
Content.Client/TextScreen/TextScreenVisualsComponent.cs [new file with mode: 0644]
Content.Server/Doors/Systems/AirlockSystem.cs
Content.Server/MachineLinking/Components/ActiveSignalTimerComponent.cs [new file with mode: 0644]
Content.Server/MachineLinking/Components/SignalTimerComponent.cs [new file with mode: 0644]
Content.Server/MachineLinking/System/SignalTimerSystem.cs [new file with mode: 0644]
Content.Shared/Doors/Components/AirlockComponent.cs
Content.Shared/MachineLinking/SharedSignalTimerComponent.cs [new file with mode: 0644]
Content.Shared/TextScreen/TextScreenVisuals.cs [new file with mode: 0644]
Resources/Locale/en-US/machine-linking/components/signal-timer-component.ftl [new file with mode: 0644]
Resources/Locale/en-US/machine-linking/receiver_ports.ftl
Resources/Locale/en-US/machine-linking/transmitter_ports.ftl
Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
Resources/Prototypes/Entities/Structures/Wallmounts/timer.yml [new file with mode: 0644]
Resources/Prototypes/MachineLinking/receiver_ports.yml
Resources/Prototypes/MachineLinking/transmitter_ports.yml
Resources/Textures/Effects/text.rsi/0.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/1.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/2.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/3.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/4.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/5.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/6.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/7.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/8.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/9.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/a.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/b.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/blank.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/c.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/colon.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/d.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/dash.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/e.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/exclamation.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/f.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/g.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/h.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/i.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/j.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/k.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/l.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/m.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/n.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/o.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/p.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/plus.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/q.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/question.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/r.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/s.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/star.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/t.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/u.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/v.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/w.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/x.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/y.png [new file with mode: 0644]
Resources/Textures/Effects/text.rsi/z.png [new file with mode: 0644]
Resources/Textures/Structures/Wallmounts/textscreen.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Structures/Wallmounts/textscreen.rsi/textscreen.png [new file with mode: 0644]

diff --git a/Content.Client/MachineLinking/UI/SignalTimerBoundUserInterface.cs b/Content.Client/MachineLinking/UI/SignalTimerBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..e9cfba6
--- /dev/null
@@ -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();
+    }
+}
diff --git a/Content.Client/MachineLinking/UI/SignalTimerWindow.xaml b/Content.Client/MachineLinking/UI/SignalTimerWindow.xaml
new file mode 100644 (file)
index 0000000..b30bd1c
--- /dev/null
@@ -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>
diff --git a/Content.Client/MachineLinking/UI/SignalTimerWindow.xaml.cs b/Content.Client/MachineLinking/UI/SignalTimerWindow.xaml.cs
new file mode 100644 (file)
index 0000000..b625955
--- /dev/null
@@ -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);
+    }
+}
diff --git a/Content.Client/TextScreen/TextScreenSystem.cs b/Content.Client/TextScreen/TextScreenSystem.cs
new file mode 100644 (file)
index 0000000..ce3928b
--- /dev/null
@@ -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;
+    }
+}
diff --git a/Content.Client/TextScreen/TextScreenTimerComponent.cs b/Content.Client/TextScreen/TextScreenTimerComponent.cs
new file mode 100644 (file)
index 0000000..c87f103
--- /dev/null
@@ -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
+{
+
+}
diff --git a/Content.Client/TextScreen/TextScreenVisualsComponent.cs b/Content.Client/TextScreen/TextScreenVisualsComponent.cs
new file mode 100644 (file)
index 0000000..b49f874
--- /dev/null
@@ -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();
+}
+
index e5e73204fc25f5256156e266b758e685bfcbe5d3..4dc8a3dc99dc5ccd418fcd4a9b31239e52458ac3 100644 (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))
diff --git a/Content.Server/MachineLinking/Components/ActiveSignalTimerComponent.cs b/Content.Server/MachineLinking/Components/ActiveSignalTimerComponent.cs
new file mode 100644 (file)
index 0000000..823dc46
--- /dev/null
@@ -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;
+    }
+}
diff --git a/Content.Server/MachineLinking/Components/SignalTimerComponent.cs b/Content.Server/MachineLinking/Components/SignalTimerComponent.cs
new file mode 100644 (file)
index 0000000..99eeb76
--- /dev/null
@@ -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;
+}
diff --git a/Content.Server/MachineLinking/System/SignalTimerSystem.cs b/Content.Server/MachineLinking/System/SignalTimerSystem.cs
new file mode 100644 (file)
index 0000000..0230a72
--- /dev/null
@@ -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);
+            }
+        }
+    }
+}
index d122acce117ce4e34697dcb84fbb3ccd5b087127..6152a4ae1431afa60cd12d1a268724a0d66c42e1 100644 (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]
diff --git a/Content.Shared/MachineLinking/SharedSignalTimerComponent.cs b/Content.Shared/MachineLinking/SharedSignalTimerComponent.cs
new file mode 100644 (file)
index 0000000..57be597
--- /dev/null
@@ -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
+{
+
+}
diff --git a/Content.Shared/TextScreen/TextScreenVisuals.cs b/Content.Shared/TextScreen/TextScreenVisuals.cs
new file mode 100644 (file)
index 0000000..76880d6
--- /dev/null
@@ -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
+}
diff --git a/Resources/Locale/en-US/machine-linking/components/signal-timer-component.ftl b/Resources/Locale/en-US/machine-linking/components/signal-timer-component.ftl
new file mode 100644 (file)
index 0000000..807ac26
--- /dev/null
@@ -0,0 +1,4 @@
+signal-timer-menu-title = Timer
+signal-timer-menu-label = Label: 
+signal-timer-menu-delay = Delay: 
+signal-timer-menu-start = Start
index 5380ac84d7ba00a38cbfa8f44468bede879d3632..b86e711b17028759a82221d84f0d751b81201709 100644 (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
 
index db0599cb77f130e33e04a8431aab80951f4fa466..03fae00159976c4d0f1f96183e92ef017e19ce6b 100644 (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.
index 97a4023a01d0efe52ae8889dcc2bd652bba10c28..490c44ac3d26cf0d119dee0b3fa97a830a390b14 100644 (file)
@@ -83,6 +83,7 @@
       Open: []
       Close: []
       Toggle: []
+      AutoClose: []
   - type: UserInterface
     interfaces:
     - key: enum.WiresUiKey.Key
index f870ba56b6f6bd0ae5258f8e971e416e844ba89f..f58dd89ef087a1545f7e38b84a1e192920808656 100644 (file)
@@ -49,6 +49,7 @@
       Open: []
       Close: []
       Toggle: []
+      AutoClose: []
   - type: Damageable
     damageContainer: Inorganic
     damageModifierSet: Glass
diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/timer.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/timer.yml
new file mode 100644 (file)
index 0000000..f40fd0a
--- /dev/null
@@ -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"]]
index 16cb34e9bd24422b9e766cf0837e517d188bea70..923ab4d49b03b49f0dd8e3dd00a316ddeae9442c 100644 (file)
   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
index 7282253347a279cc1a29cc1111892f595c56f9e0..c4d25c6461227100ed95315a1520c690a1b4490b 100644 (file)
   id: MedicalScannerSender
   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
   description: signal-port-description-artifact-analyzer-sender
-  defaultLinks: [ ArtifactAnalyzerReceiver ]
\ No newline at end of file
+  defaultLinks: [ ArtifactAnalyzerReceiver ]
diff --git a/Resources/Textures/Effects/text.rsi/0.png b/Resources/Textures/Effects/text.rsi/0.png
new file mode 100644 (file)
index 0000000..c24a782
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/0.png differ
diff --git a/Resources/Textures/Effects/text.rsi/1.png b/Resources/Textures/Effects/text.rsi/1.png
new file mode 100644 (file)
index 0000000..49b4c0e
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/1.png differ
diff --git a/Resources/Textures/Effects/text.rsi/2.png b/Resources/Textures/Effects/text.rsi/2.png
new file mode 100644 (file)
index 0000000..1d1dc74
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/2.png differ
diff --git a/Resources/Textures/Effects/text.rsi/3.png b/Resources/Textures/Effects/text.rsi/3.png
new file mode 100644 (file)
index 0000000..b88c315
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/3.png differ
diff --git a/Resources/Textures/Effects/text.rsi/4.png b/Resources/Textures/Effects/text.rsi/4.png
new file mode 100644 (file)
index 0000000..dd71e01
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/4.png differ
diff --git a/Resources/Textures/Effects/text.rsi/5.png b/Resources/Textures/Effects/text.rsi/5.png
new file mode 100644 (file)
index 0000000..0da60a2
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/5.png differ
diff --git a/Resources/Textures/Effects/text.rsi/6.png b/Resources/Textures/Effects/text.rsi/6.png
new file mode 100644 (file)
index 0000000..1f04acb
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/6.png differ
diff --git a/Resources/Textures/Effects/text.rsi/7.png b/Resources/Textures/Effects/text.rsi/7.png
new file mode 100644 (file)
index 0000000..db521b6
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/7.png differ
diff --git a/Resources/Textures/Effects/text.rsi/8.png b/Resources/Textures/Effects/text.rsi/8.png
new file mode 100644 (file)
index 0000000..99c0484
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/8.png differ
diff --git a/Resources/Textures/Effects/text.rsi/9.png b/Resources/Textures/Effects/text.rsi/9.png
new file mode 100644 (file)
index 0000000..7fea856
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/9.png differ
diff --git a/Resources/Textures/Effects/text.rsi/a.png b/Resources/Textures/Effects/text.rsi/a.png
new file mode 100644 (file)
index 0000000..2d23225
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/a.png differ
diff --git a/Resources/Textures/Effects/text.rsi/b.png b/Resources/Textures/Effects/text.rsi/b.png
new file mode 100644 (file)
index 0000000..bedd457
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/b.png differ
diff --git a/Resources/Textures/Effects/text.rsi/blank.png b/Resources/Textures/Effects/text.rsi/blank.png
new file mode 100644 (file)
index 0000000..9eb5b57
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/blank.png differ
diff --git a/Resources/Textures/Effects/text.rsi/c.png b/Resources/Textures/Effects/text.rsi/c.png
new file mode 100644 (file)
index 0000000..b1fe537
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/c.png differ
diff --git a/Resources/Textures/Effects/text.rsi/colon.png b/Resources/Textures/Effects/text.rsi/colon.png
new file mode 100644 (file)
index 0000000..e6115f1
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/colon.png differ
diff --git a/Resources/Textures/Effects/text.rsi/d.png b/Resources/Textures/Effects/text.rsi/d.png
new file mode 100644 (file)
index 0000000..c110efc
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/d.png differ
diff --git a/Resources/Textures/Effects/text.rsi/dash.png b/Resources/Textures/Effects/text.rsi/dash.png
new file mode 100644 (file)
index 0000000..e72d53e
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/dash.png differ
diff --git a/Resources/Textures/Effects/text.rsi/e.png b/Resources/Textures/Effects/text.rsi/e.png
new file mode 100644 (file)
index 0000000..6e967e5
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/e.png differ
diff --git a/Resources/Textures/Effects/text.rsi/exclamation.png b/Resources/Textures/Effects/text.rsi/exclamation.png
new file mode 100644 (file)
index 0000000..a3552f9
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/exclamation.png differ
diff --git a/Resources/Textures/Effects/text.rsi/f.png b/Resources/Textures/Effects/text.rsi/f.png
new file mode 100644 (file)
index 0000000..a302325
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/f.png differ
diff --git a/Resources/Textures/Effects/text.rsi/g.png b/Resources/Textures/Effects/text.rsi/g.png
new file mode 100644 (file)
index 0000000..1ad7d5a
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/g.png differ
diff --git a/Resources/Textures/Effects/text.rsi/h.png b/Resources/Textures/Effects/text.rsi/h.png
new file mode 100644 (file)
index 0000000..3e32d1c
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/h.png differ
diff --git a/Resources/Textures/Effects/text.rsi/i.png b/Resources/Textures/Effects/text.rsi/i.png
new file mode 100644 (file)
index 0000000..4339036
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/i.png differ
diff --git a/Resources/Textures/Effects/text.rsi/j.png b/Resources/Textures/Effects/text.rsi/j.png
new file mode 100644 (file)
index 0000000..109fd3b
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/j.png differ
diff --git a/Resources/Textures/Effects/text.rsi/k.png b/Resources/Textures/Effects/text.rsi/k.png
new file mode 100644 (file)
index 0000000..0ea2c1f
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/k.png differ
diff --git a/Resources/Textures/Effects/text.rsi/l.png b/Resources/Textures/Effects/text.rsi/l.png
new file mode 100644 (file)
index 0000000..88fc088
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/l.png differ
diff --git a/Resources/Textures/Effects/text.rsi/m.png b/Resources/Textures/Effects/text.rsi/m.png
new file mode 100644 (file)
index 0000000..007309d
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/m.png differ
diff --git a/Resources/Textures/Effects/text.rsi/meta.json b/Resources/Textures/Effects/text.rsi/meta.json
new file mode 100644 (file)
index 0000000..961d7cc
--- /dev/null
@@ -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"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Effects/text.rsi/n.png b/Resources/Textures/Effects/text.rsi/n.png
new file mode 100644 (file)
index 0000000..16b4596
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/n.png differ
diff --git a/Resources/Textures/Effects/text.rsi/o.png b/Resources/Textures/Effects/text.rsi/o.png
new file mode 100644 (file)
index 0000000..c6e7713
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/o.png differ
diff --git a/Resources/Textures/Effects/text.rsi/p.png b/Resources/Textures/Effects/text.rsi/p.png
new file mode 100644 (file)
index 0000000..d99111e
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/p.png differ
diff --git a/Resources/Textures/Effects/text.rsi/plus.png b/Resources/Textures/Effects/text.rsi/plus.png
new file mode 100644 (file)
index 0000000..4df0ce6
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/plus.png differ
diff --git a/Resources/Textures/Effects/text.rsi/q.png b/Resources/Textures/Effects/text.rsi/q.png
new file mode 100644 (file)
index 0000000..d3980f6
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/q.png differ
diff --git a/Resources/Textures/Effects/text.rsi/question.png b/Resources/Textures/Effects/text.rsi/question.png
new file mode 100644 (file)
index 0000000..807e58c
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/question.png differ
diff --git a/Resources/Textures/Effects/text.rsi/r.png b/Resources/Textures/Effects/text.rsi/r.png
new file mode 100644 (file)
index 0000000..79924fb
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/r.png differ
diff --git a/Resources/Textures/Effects/text.rsi/s.png b/Resources/Textures/Effects/text.rsi/s.png
new file mode 100644 (file)
index 0000000..4650fc1
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/s.png differ
diff --git a/Resources/Textures/Effects/text.rsi/star.png b/Resources/Textures/Effects/text.rsi/star.png
new file mode 100644 (file)
index 0000000..256fc31
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/star.png differ
diff --git a/Resources/Textures/Effects/text.rsi/t.png b/Resources/Textures/Effects/text.rsi/t.png
new file mode 100644 (file)
index 0000000..6674475
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/t.png differ
diff --git a/Resources/Textures/Effects/text.rsi/u.png b/Resources/Textures/Effects/text.rsi/u.png
new file mode 100644 (file)
index 0000000..01617bf
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/u.png differ
diff --git a/Resources/Textures/Effects/text.rsi/v.png b/Resources/Textures/Effects/text.rsi/v.png
new file mode 100644 (file)
index 0000000..96113cb
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/v.png differ
diff --git a/Resources/Textures/Effects/text.rsi/w.png b/Resources/Textures/Effects/text.rsi/w.png
new file mode 100644 (file)
index 0000000..6da83f8
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/w.png differ
diff --git a/Resources/Textures/Effects/text.rsi/x.png b/Resources/Textures/Effects/text.rsi/x.png
new file mode 100644 (file)
index 0000000..86db44f
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/x.png differ
diff --git a/Resources/Textures/Effects/text.rsi/y.png b/Resources/Textures/Effects/text.rsi/y.png
new file mode 100644 (file)
index 0000000..104f89e
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/y.png differ
diff --git a/Resources/Textures/Effects/text.rsi/z.png b/Resources/Textures/Effects/text.rsi/z.png
new file mode 100644 (file)
index 0000000..562c5cd
Binary files /dev/null and b/Resources/Textures/Effects/text.rsi/z.png differ
diff --git a/Resources/Textures/Structures/Wallmounts/textscreen.rsi/meta.json b/Resources/Textures/Structures/Wallmounts/textscreen.rsi/meta.json
new file mode 100644 (file)
index 0000000..8fb2e2d
--- /dev/null
@@ -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"
+    }
+  ]
+}
diff --git a/Resources/Textures/Structures/Wallmounts/textscreen.rsi/textscreen.png b/Resources/Textures/Structures/Wallmounts/textscreen.rsi/textscreen.png
new file mode 100644 (file)
index 0000000..e1496f2
Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/textscreen.rsi/textscreen.png differ