diff --git a/Content.Client/MachineLinking/UI/SignalTimerBoundUserInterface.cs b/Content.Client/MachineLinking/UI/SignalTimerBoundUserInterface.cs
new file mode 100644
index 0000000000..e9cfba697f
--- /dev/null
+++ b/Content.Client/MachineLinking/UI/SignalTimerBoundUserInterface.cs
@@ -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;
+ }
+
+ ///
+ /// Update the UI state based on server-sent info
+ ///
+ ///
+ 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
index 0000000000..b30bd1c5f2
--- /dev/null
+++ b/Content.Client/MachineLinking/UI/SignalTimerWindow.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/MachineLinking/UI/SignalTimerWindow.xaml.cs b/Content.Client/MachineLinking/UI/SignalTimerWindow.xaml.cs
new file mode 100644
index 0000000000..b62595595e
--- /dev/null
+++ b/Content.Client/MachineLinking/UI/SignalTimerWindow.xaml.cs
@@ -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? OnCurrentTextChanged;
+ public event Action? OnCurrentDelayMinutesChanged;
+ public event Action? 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 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 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");
+ }
+
+ ///
+ /// Disables fields and buttons if you don't have the access.
+ ///
+ public void SetHasAccess(bool hasAccess)
+ {
+ CurrentTextEdit.Editable = hasAccess;
+ CurrentDelayEditMinutes.Editable = hasAccess;
+ CurrentDelayEditSeconds.Editable = hasAccess;
+ StartTimer.Disabled = !hasAccess;
+ }
+
+ ///
+ /// Returns a TimeSpan from the currently entered delay.
+ ///
+ 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
index 0000000000..ce3928b98b
--- /dev/null
+++ b/Content.Client/TextScreen/TextScreenSystem.cs
@@ -0,0 +1,295 @@
+using Content.Shared.TextScreen;
+using Robust.Client.GameObjects;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Client.TextScreen;
+
+///
+/// The TextScreenSystem draws text in the game world using 3x5 sprite states for each character.
+///
+public sealed class TextScreenSystem : VisualizerSystem
+{
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+
+ ///
+ /// Contains char/state Key/Value pairs.
+ /// The states in Textures/Effects/text.rsi that special character should be replaced with.
+ ///
+ private static readonly Dictionary CharStatePairs = new()
+ {
+ { ':', "colon" },
+ { '!', "exclamation" },
+ { '?', "question" },
+ { '*', "star" },
+ { '+', "plus" },
+ { '-', "dash" },
+ { ' ', "blank" }
+ };
+
+ private const string DefaultState = "blank";
+
+ ///
+ /// A string prefix for all text layers.
+ ///
+ private const string TextScreenLayerMapKey = "textScreenLayerMapKey";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(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);
+ }
+
+ ///
+ /// Resets all TextScreenComponent sprite layers, through removing them and then creating new ones.
+ ///
+ 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);
+ }
+
+ ///
+ /// Sets , adding or removing sprite layers if necessary.
+ ///
+ 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;
+ }
+
+ ///
+ /// Updates the layers offsets based on the text length, so it is drawn correctly.
+ ///
+ 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(uid);
+ else
+ RemComp(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);
+ }
+
+ ///
+ /// If currently in mode:
+ /// Sets to the value of
+ ///
+ public static void UpdateText(TextScreenVisualsComponent component)
+ {
+ if (component.CurrentMode == TextScreenMode.Text)
+ component.TextToDraw = component.Text;
+ }
+
+ ///
+ /// Sets visibility of text to .
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Sets the states in the to match the component string.
+ ///
+ ///
+ /// Remember to set to a string first.
+ ///
+ 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]);
+ }
+ }
+
+ ///
+ /// Iterates through , setting sprite states to the appropriate layers.
+ ///
+ 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();
+ 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);
+ }
+ }
+
+ ///
+ /// Returns the converted to a string in either HH:MM, MM:SS or potentially SS:mm format.
+ ///
+ /// TimeSpan to convert into string.
+ /// Should the string be ss:ms if minutes are less than 1?
+ 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;
+ }
+
+ ///
+ /// Returns the Effects/text.rsi state string based on , or null if none available.
+ ///
+ 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
index 0000000000..c87f103519
--- /dev/null
+++ b/Content.Client/TextScreen/TextScreenTimerComponent.cs
@@ -0,0 +1,10 @@
+namespace Content.Client.TextScreen;
+
+///
+/// This is an active component for tracking
+///
+[RegisterComponent]
+public sealed class TextScreenTimerComponent : Component
+{
+
+}
diff --git a/Content.Client/TextScreen/TextScreenVisualsComponent.cs b/Content.Client/TextScreen/TextScreenVisualsComponent.cs
new file mode 100644
index 0000000000..b49f874829
--- /dev/null
+++ b/Content.Client/TextScreen/TextScreenVisualsComponent.cs
@@ -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
+{
+ ///
+ /// 1/32 - the size of a pixel
+ ///
+ public const float PixelSize = 1f / EyeManager.PixelsPerMeter;
+
+ ///
+ /// The color of the text drawn.
+ ///
+ [DataField("color")]
+ public Color Color { get; set; } = Color.FloralWhite;
+
+ ///
+ /// Whether the screen is on.
+ ///
+ [DataField("activated")]
+ public bool Activated;
+
+ ///
+ /// The current mode of the screen - is it showing text, or currently counting?
+ ///
+ [DataField("currentMode")]
+ public TextScreenMode CurrentMode = TextScreenMode.Text;
+
+ ///
+ /// The time it is counting to or from.
+ ///
+ [DataField("targetTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
+ public TimeSpan TargetTime = TimeSpan.Zero;
+
+ ///
+ /// Offset for drawing the text.
+ /// (0, 8) pixels is the default for the Structures\Wallmounts\textscreen.rsi
+ ///
+ [DataField("textOffset"), ViewVariables(VVAccess.ReadWrite)]
+ public Vector2 TextOffset = new(0f, 8f * PixelSize);
+
+ ///
+ /// The amount of characters this component can show.
+ ///
+ [DataField("textLength")]
+ public int TextLength = 5;
+
+ ///
+ /// Text the screen should show when it's not counting.
+ ///
+ [DataField("text"), ViewVariables(VVAccess.ReadWrite)]
+ public string Text = "";
+
+ public string TextToDraw = "";
+
+ ///
+ /// The different layers for each character - this is the currently drawn states.
+ ///
+ [DataField("layerStatesToDraw")]
+ public Dictionary LayerStatesToDraw = new();
+}
+
diff --git a/Content.Server/Doors/Systems/AirlockSystem.cs b/Content.Server/Doors/Systems/AirlockSystem.cs
index e5e73204fc..4dc8a3dc99 100644
--- a/Content.Server/Doors/Systems/AirlockSystem.cs
+++ b/Content.Server/Doors/Systems/AirlockSystem.cs
@@ -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(OnAirlockInit);
+ SubscribeLocalEvent(OnSignalReceived);
+
SubscribeLocalEvent(OnPowerChanged);
SubscribeLocalEvent(OnStateChanged);
SubscribeLocalEvent(OnBeforeDoorOpened);
@@ -28,6 +33,7 @@ namespace Content.Server.Doors.Systems
SubscribeLocalEvent(OnActivate, before: new [] {typeof(DoorSystem)});
SubscribeLocalEvent(OnGetPryMod);
SubscribeLocalEvent(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(uid, out var appearanceComponent))
diff --git a/Content.Server/MachineLinking/Components/ActiveSignalTimerComponent.cs b/Content.Server/MachineLinking/Components/ActiveSignalTimerComponent.cs
new file mode 100644
index 0000000000..823dc469e4
--- /dev/null
+++ b/Content.Server/MachineLinking/Components/ActiveSignalTimerComponent.cs
@@ -0,0 +1,15 @@
+
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Server.MachineLinking.Components
+{
+ [RegisterComponent]
+ public sealed class ActiveSignalTimerComponent : Component
+ {
+ ///
+ /// The time the timer triggers.
+ ///
+ [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
index 0000000000..99eeb76880
--- /dev/null
+++ b/Content.Server/MachineLinking/Components/SignalTimerComponent.cs
@@ -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;
+
+ ///
+ /// This shows the Label: text box in the UI.
+ ///
+ [DataField("canEditLabel"), ViewVariables(VVAccess.ReadWrite)]
+ public bool CanEditLabel = true;
+
+ ///
+ /// The label, used for TextScreen visuals currently.
+ ///
+ [DataField("label"), ViewVariables(VVAccess.ReadWrite)]
+ public string Label = "";
+
+ ///
+ /// The port that gets signaled when the timer triggers.
+ ///
+ [DataField("triggerPort", customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)]
+ public string TriggerPort = "Timer";
+
+ ///
+ /// The port that gets signaled when the timer starts.
+ ///
+ [DataField("startPort", customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)]
+ public string StartPort = "Start";
+
+ ///
+ /// If not null, this timer will play this sound when done.
+ ///
+ [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
index 0000000000..0230a72b2b
--- /dev/null
+++ b/Content.Server/MachineLinking/System/SignalTimerSystem.cs
@@ -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(OnInit);
+ SubscribeLocalEvent(OnAfterActivatableUIOpen);
+
+ SubscribeLocalEvent(OnTextChangedMessage);
+ SubscribeLocalEvent(OnDelayChangedMessage);
+ SubscribeLocalEvent(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(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(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();
+ 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);
+ }
+ }
+
+ ///
+ /// Checks if a UI is allowed to be sent by the user.
+ ///
+ /// The entity that is interacted with.
+ ///
+ private bool IsMessageValid(EntityUid uid, BoundUserInterfaceMessage message)
+ {
+ if (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(uid, out var appearance);
+
+ if (!HasComp(uid))
+ {
+ var activeTimer = EnsureComp(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(uid);
+
+ if (appearance != null)
+ {
+ _appearanceSystem.SetData(uid, TextScreenVisuals.Mode, TextScreenMode.Text, appearance);
+ _appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, component.Label, appearance);
+ }
+ }
+ }
+}
diff --git a/Content.Shared/Doors/Components/AirlockComponent.cs b/Content.Shared/Doors/Components/AirlockComponent.cs
index d122acce11..6152a4ae14 100644
--- a/Content.Shared/Doors/Components/AirlockComponent.cs
+++ b/Content.Shared/Doors/Components/AirlockComponent.cs
@@ -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
///
[ViewVariables(VVAccess.ReadWrite)]
public float AutoCloseDelayModifier = 1.0f;
+
+ ///
+ /// The receiver port for turning off automatic closing.
+ ///
+ [DataField("autoClosePort", customTypeSerializer: typeof(PrototypeIdSerializer))]
+ public string AutoClosePort = "AutoClose";
}
[Serializable, NetSerializable]
diff --git a/Content.Shared/MachineLinking/SharedSignalTimerComponent.cs b/Content.Shared/MachineLinking/SharedSignalTimerComponent.cs
new file mode 100644
index 0000000000..57be5978f9
--- /dev/null
+++ b/Content.Shared/MachineLinking/SharedSignalTimerComponent.cs
@@ -0,0 +1,68 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.MachineLinking;
+
+[Serializable, NetSerializable]
+public enum SignalTimerUiKey : byte
+{
+ Key
+}
+
+///
+/// Represents a SignalTimerComponent state that can be sent to the client
+///
+[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
index 0000000000..76880d6c3f
--- /dev/null
+++ b/Content.Shared/TextScreen/TextScreenVisuals.cs
@@ -0,0 +1,35 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.TextScreen;
+
+[Serializable, NetSerializable]
+public enum TextScreenVisuals : byte
+{
+ ///
+ /// Should this show any text?
+ /// Expects a .
+ ///
+ On,
+ ///
+ /// Is this a timer or a text-screen?
+ /// Expects a .
+ ///
+ Mode,
+ ///
+ /// What text to show?
+ /// Expects a .
+ ///
+ ScreenText,
+ ///
+ /// What is the target time?
+ /// Expects a .
+ ///
+ 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
index 0000000000..807ac2669d
--- /dev/null
+++ b/Resources/Locale/en-US/machine-linking/components/signal-timer-component.ftl
@@ -0,0 +1,4 @@
+signal-timer-menu-title = Timer
+signal-timer-menu-label = Label:
+signal-timer-menu-delay = Delay:
+signal-timer-menu-start = Start
diff --git a/Resources/Locale/en-US/machine-linking/receiver_ports.ftl b/Resources/Locale/en-US/machine-linking/receiver_ports.ftl
index 5380ac84d7..b86e711b17 100644
--- a/Resources/Locale/en-US/machine-linking/receiver_ports.ftl
+++ b/Resources/Locale/en-US/machine-linking/receiver_ports.ftl
@@ -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
diff --git a/Resources/Locale/en-US/machine-linking/transmitter_ports.ftl b/Resources/Locale/en-US/machine-linking/transmitter_ports.ftl
index db0599cb77..03fae00159 100644
--- a/Resources/Locale/en-US/machine-linking/transmitter_ports.ftl
+++ b/Resources/Locale/en-US/machine-linking/transmitter_ports.ftl
@@ -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.
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
index 97a4023a01..490c44ac3d 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Airlocks/base_structureairlocks.yml
@@ -83,6 +83,7 @@
Open: []
Close: []
Toggle: []
+ AutoClose: []
- type: UserInterface
interfaces:
- key: enum.WiresUiKey.Key
diff --git a/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml b/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
index f870ba56b6..f58dd89ef0 100644
--- a/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
+++ b/Resources/Prototypes/Entities/Structures/Doors/Windoors/base_structurewindoors.yml
@@ -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
index 0000000000..f40fd0a27a
--- /dev/null
+++ b/Resources/Prototypes/Entities/Structures/Wallmounts/timer.yml
@@ -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"]]
diff --git a/Resources/Prototypes/MachineLinking/receiver_ports.yml b/Resources/Prototypes/MachineLinking/receiver_ports.yml
index 16cb34e9bd..923ab4d49b 100644
--- a/Resources/Prototypes/MachineLinking/receiver_ports.yml
+++ b/Resources/Prototypes/MachineLinking/receiver_ports.yml
@@ -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
diff --git a/Resources/Prototypes/MachineLinking/transmitter_ports.yml b/Resources/Prototypes/MachineLinking/transmitter_ports.yml
index 7282253347..c4d25c6461 100644
--- a/Resources/Prototypes/MachineLinking/transmitter_ports.yml
+++ b/Resources/Prototypes/MachineLinking/transmitter_ports.yml
@@ -49,9 +49,21 @@
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
index 0000000000..c24a782c85
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
index 0000000000..49b4c0e07b
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
index 0000000000..1d1dc74685
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
index 0000000000..b88c315e75
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
index 0000000000..dd71e01ea7
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
index 0000000000..0da60a2a76
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
index 0000000000..1f04acbfde
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
index 0000000000..db521b610a
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
index 0000000000..99c048414e
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
index 0000000000..7fea856ae1
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
index 0000000000..2d232254e1
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
index 0000000000..bedd457c2c
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
index 0000000000..9eb5b578fc
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
index 0000000000..b1fe5370fd
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
index 0000000000..e6115f14c8
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
index 0000000000..c110efc7b2
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
index 0000000000..e72d53e5f0
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
index 0000000000..6e967e52e2
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
index 0000000000..a3552f9885
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
index 0000000000..a302325b1d
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
index 0000000000..1ad7d5a9a2
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
index 0000000000..3e32d1c517
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
index 0000000000..433903630a
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
index 0000000000..109fd3b975
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
index 0000000000..0ea2c1f75a
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
index 0000000000..88fc088db3
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
index 0000000000..007309da72
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
index 0000000000..961d7cc6f1
--- /dev/null
+++ b/Resources/Textures/Effects/text.rsi/meta.json
@@ -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
index 0000000000..16b459624e
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
index 0000000000..c6e771304a
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
index 0000000000..d99111eca2
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
index 0000000000..4df0ce6785
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
index 0000000000..d3980f6408
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
index 0000000000..807e58c9ce
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
index 0000000000..79924fb80e
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
index 0000000000..4650fc1ca1
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
index 0000000000..256fc313dc
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
index 0000000000..6674475be3
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
index 0000000000..01617bfa4f
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
index 0000000000..96113cb5ed
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
index 0000000000..6da83f87ae
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
index 0000000000..86db44f545
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
index 0000000000..104f89e645
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
index 0000000000..562c5cd5b6
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
index 0000000000..8fb2e2d80f
--- /dev/null
+++ b/Resources/Textures/Structures/Wallmounts/textscreen.rsi/meta.json
@@ -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
index 0000000000..e1496f2ca8
Binary files /dev/null and b/Resources/Textures/Structures/Wallmounts/textscreen.rsi/textscreen.png differ