]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Hallway textscreens (#24189)
authoravery <51971268+graevy@users.noreply.github.com>
Sat, 27 Jan 2024 13:51:24 +0000 (05:51 -0800)
committerGitHub <noreply@github.com>
Sat, 27 Jan 2024 13:51:24 +0000 (08:51 -0500)
* hallway screen refactor pending comms console support

* comms console broadcasts

* screen and timer localization

23 files changed:
Content.Client/Communications/UI/CommunicationsConsoleBoundUserInterface.cs
Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml
Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml.cs
Content.Client/TextScreen/TextScreenSystem.cs
Content.Client/TextScreen/TextScreenVisualsComponent.cs
Content.Server/Communications/CommunicationsConsoleComponent.cs
Content.Server/Communications/CommunicationsConsoleSystem.cs
Content.Server/RoundEnd/RoundEndSystem.cs
Content.Server/Screens/Components/ScreenComponent.cs [new file with mode: 0644]
Content.Server/Screens/Systems/ScreenSystem.cs [new file with mode: 0644]
Content.Server/Shuttles/Components/ShuttleTimerComponent.cs [deleted file]
Content.Server/Shuttles/Systems/ArrivalsSystem.cs
Content.Server/Shuttles/Systems/EmergencyShuttleSystem.Console.cs
Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs
Content.Server/Shuttles/Systems/ShuttleTimerSystem.cs [deleted file]
Content.Shared/Communications/SharedCommunicationsConsoleComponent.cs
Content.Shared/TextScreen/TextScreenVisuals.cs
Resources/Locale/en-US/communications/communications-console-component.ftl
Resources/Locale/en-US/shuttles/screens.ftl [new file with mode: 0644]
Resources/Locale/en-US/shuttles/timer.ftl
Resources/Prototypes/Entities/Structures/Machines/Computers/computers.yml
Resources/Prototypes/Entities/Structures/Wallmounts/screen.yml
Resources/Prototypes/Entities/Structures/Wallmounts/timer.yml

index 07492b310f3d70090a25ad461f62ee67070017fd..1c94d32bf8d17d142c56e93b426d524705474b5d 100644 (file)
@@ -16,6 +16,8 @@ namespace Content.Client.Communications.UI
 
         [ViewVariables]
         public bool CanAnnounce { get; private set; }
+        [ViewVariables]
+        public bool CanBroadcast { get; private set; }
 
         [ViewVariables]
         public bool CanCall { get; private set; }
@@ -71,6 +73,11 @@ namespace Content.Client.Communications.UI
             SendMessage(new CommunicationsConsoleAnnounceMessage(msg));
         }
 
+        public void BroadcastButtonPressed(string message)
+        {
+            SendMessage(new CommunicationsConsoleBroadcastMessage(message));
+        }
+
         public void CallShuttle()
         {
             SendMessage(new CommunicationsConsoleCallEmergencyShuttleMessage());
@@ -89,6 +96,7 @@ namespace Content.Client.Communications.UI
                 return;
 
             CanAnnounce = commsState.CanAnnounce;
+            CanBroadcast = commsState.CanBroadcast;
             CanCall = commsState.CanCall;
             _expectedCountdownTime = commsState.ExpectedCountdownEnd;
             CountdownStarted = commsState.CountdownStarted;
@@ -102,6 +110,7 @@ namespace Content.Client.Communications.UI
                 _menu.AlertLevelButton.Disabled = !AlertLevelSelectable;
                 _menu.EmergencyShuttleButton.Disabled = !CanCall;
                 _menu.AnnounceButton.Disabled = !CanAnnounce;
+                _menu.BroadcastButton.Disabled = !CanBroadcast;
             }
         }
 
index 86c0b4e2d5337b74027343e5fc1761af62b299cd..ea2f77d457d7817408ac407da0ee236963d2ca03 100644 (file)
@@ -5,6 +5,7 @@
     <BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="5">
         <TextEdit Name="MessageInput" HorizontalExpand="True" VerticalExpand="True" Margin="0 0 0 5" MinHeight="100" />
         <Button Name="AnnounceButton" Text="{Loc 'comms-console-menu-announcement-button'}" StyleClasses="OpenLeft" Access="Public" />
+        <Button Name="BroadcastButton" Text="{Loc 'comms-console-menu-broadcast-button'}" StyleClasses="OpenLeft" Access="Public" />
 
         <OptionButton Name="AlertLevelButton" StyleClasses="OpenRight" Access="Public" />
 
index 37fcdd5e29c62cae9ae2285877a2299d061557b7..90643e45cf7ff4e6dd612dee6520f46cd31ee363 100644 (file)
@@ -26,6 +26,9 @@ namespace Content.Client.Communications.UI
             AnnounceButton.OnPressed += (_) => Owner.AnnounceButtonPressed(Rope.Collapse(MessageInput.TextRope));
             AnnounceButton.Disabled = !owner.CanAnnounce;
 
+            BroadcastButton.OnPressed += (_) => Owner.BroadcastButtonPressed(Rope.Collapse(MessageInput.TextRope));
+            BroadcastButton.Disabled = !owner.CanBroadcast;
+
             AlertLevelButton.OnItemSelected += args =>
             {
                 var metadata = AlertLevelButton.GetItemMetadata(args.Id);
index c14bce740a6e1fc05327204f8c0dcd85338481f4..a736933d0fae2edb5eb3241a7766225eac2c72f0 100644 (file)
@@ -99,19 +99,43 @@ public sealed class TextScreenSystem : VisualizerSystem<TextScreenVisualsCompone
     ///     Called by <see cref="SharedAppearanceSystem.SetData"/> to handle text updates,
     ///     and spawn a <see cref="TextScreenTimerComponent"/> if necessary
     /// </summary>
+    /// <remarks>
+    ///     The appearance updates are batched; order matters for both sender and receiver.
+    /// </remarks>
     protected override void OnAppearanceChange(EntityUid uid, TextScreenVisualsComponent component, ref AppearanceChangeEvent args)
     {
         if (!Resolve(uid, ref args.Sprite))
             return;
 
-        var appearance = args.Component;
+        if (args.AppearanceData.TryGetValue(TextScreenVisuals.Color, out var color) && color is Color)
+            component.Color = (Color) color;
+
+        // DefaultText: broadcast updates from comms consoles
+        // ScreenText: the text accompanying shuttle timers e.g. "ETA"
+        if (args.AppearanceData.TryGetValue(TextScreenVisuals.DefaultText, out var newDefault) && newDefault is string)
+        {
+            string?[] defaultText = SegmentText((string) newDefault, component);
+            component.Text = defaultText;
+            component.TextToDraw = defaultText;
+            ResetText(uid, component);
+            BuildTextLayers(uid, component, args.Sprite);
+            DrawLayers(uid, component.LayerStatesToDraw);
+        }
+        if (args.AppearanceData.TryGetValue(TextScreenVisuals.ScreenText, out var text) && text is string)
+        {
+            component.TextToDraw = SegmentText((string) text, component);
+            ResetText(uid, component);
+            BuildTextLayers(uid, component, args.Sprite);
+            DrawLayers(uid, component.LayerStatesToDraw);
+        }
 
-        if (AppearanceSystem.TryGetData(uid, TextScreenVisuals.TargetTime, out TimeSpan time, appearance))
+        if (args.AppearanceData.TryGetValue(TextScreenVisuals.TargetTime, out var time) && time is TimeSpan)
         {
-            if (time > _gameTiming.CurTime)
+            var target = (TimeSpan) time;
+            if (target > _gameTiming.CurTime)
             {
                 var timer = EnsureComp<TextScreenTimerComponent>(uid);
-                timer.Target = time;
+                timer.Target = target;
                 BuildTimerLayers(uid, timer, component);
                 DrawLayers(uid, timer.LayerStatesToDraw);
             }
@@ -120,14 +144,6 @@ public sealed class TextScreenSystem : VisualizerSystem<TextScreenVisualsCompone
                 OnTimerFinish(uid, component);
             }
         }
-
-        if (AppearanceSystem.TryGetData(uid, TextScreenVisuals.ScreenText, out string?[] text, appearance))
-        {
-            component.TextToDraw = text;
-            ResetText(uid, component);
-            BuildTextLayers(uid, component, args.Sprite);
-            DrawLayers(uid, component.LayerStatesToDraw);
-        }
     }
 
     /// <summary>
@@ -151,10 +167,28 @@ public sealed class TextScreenSystem : VisualizerSystem<TextScreenVisualsCompone
         DrawLayers(uid, screen.LayerStatesToDraw);
     }
 
+    /// <summary>
+    ///     Converts string to string?[] based on
+    ///     <see cref="TextScreenVisualsComponent.RowLength"/> and <see cref="TextScreenVisualsComponent.Rows"/>.
+    /// </summary>
+    private string?[] SegmentText(string text, TextScreenVisualsComponent component)
+    {
+        int segment = component.RowLength;
+        var segmented = new string?[Math.Min(component.Rows, (text.Length - 1) / segment + 1)];
+
+        // populate segmented with a string sliding window using Substring.
+        // (Substring(5, 5) will return the 5 characters starting from 5th index)
+        // the Mins are for the very short string case, the very long string case, and to not OOB the end of the string.
+        for (int i = 0; i < Math.Min(text.Length, segment * component.Rows); i += segment)
+            segmented[i / segment] = text.Substring(i, Math.Min(text.Length - i, segment)).Trim();
+
+        return segmented;
+    }
+
     /// <summary>
     ///     Clears <see cref="TextScreenVisualsComponent.LayerStatesToDraw"/>, and instantiates new blank defaults.
     /// </summary>
-    public void ResetText(EntityUid uid, TextScreenVisualsComponent component, SpriteComponent? sprite = null)
+    private void ResetText(EntityUid uid, TextScreenVisualsComponent component, SpriteComponent? sprite = null)
     {
         if (!Resolve(uid, ref sprite))
             return;
@@ -167,11 +201,12 @@ public sealed class TextScreenSystem : VisualizerSystem<TextScreenVisualsCompone
         for (var row = 0; row < component.Rows; row++)
             for (var i = 0; i < component.RowLength; i++)
             {
-                sprite.LayerMapReserveBlank(TextMapKey + row + i);
-                component.LayerStatesToDraw.Add(TextMapKey + row + i, null);
-                sprite.LayerSetRSI(TextMapKey + row + i, new ResPath(TextPath));
-                sprite.LayerSetColor(TextMapKey + row + i, component.Color);
-                sprite.LayerSetState(TextMapKey + row + i, DefaultState);
+                var key = TextMapKey + row + i;
+                sprite.LayerMapReserveBlank(key);
+                component.LayerStatesToDraw.Add(key, null);
+                sprite.LayerSetRSI(key, new ResPath(TextPath));
+                sprite.LayerSetColor(key, component.Color);
+                sprite.LayerSetState(key, DefaultState);
             }
     }
 
@@ -182,7 +217,7 @@ public sealed class TextScreenSystem : VisualizerSystem<TextScreenVisualsCompone
     /// <remarks>
     ///     Remember to set <see cref="TextScreenVisualsComponent.TextToDraw"/> to a string?[] first.
     /// </remarks>
-    public void BuildTextLayers(EntityUid uid, TextScreenVisualsComponent component, SpriteComponent? sprite = null)
+    private void BuildTextLayers(EntityUid uid, TextScreenVisualsComponent component, SpriteComponent? sprite = null)
     {
         if (!Resolve(uid, ref sprite))
             return;
@@ -211,7 +246,7 @@ public sealed class TextScreenSystem : VisualizerSystem<TextScreenVisualsCompone
     /// <summary>
     ///     Populates timer.LayerStatesToDraw & the sprite component's layer dict with calculated offsets.
     /// </summary>
-    public void BuildTimerLayers(EntityUid uid, TextScreenTimerComponent timer, TextScreenVisualsComponent screen)
+    private void BuildTimerLayers(EntityUid uid, TextScreenTimerComponent timer, TextScreenVisualsComponent screen)
     {
         if (!TryComp<SpriteComponent>(uid, out var sprite))
             return;
index cf3619024c606045eafec7f01edb03ddc4785dfd..e74f38f4320e8cebf1dcf73e20145f01030daaed 100644 (file)
@@ -1,5 +1,4 @@
 using System.Numerics;
-using Content.Shared.TextScreen;
 using Robust.Client.Graphics;
 
 namespace Content.Client.TextScreen;
@@ -37,7 +36,7 @@ public sealed partial class TextScreenVisualsComponent : Component
     ///     Number of rows of text this screen can render.
     /// </summary>
     [DataField("rows")]
-    public int Rows = 1;
+    public int Rows = 2;
 
     /// <summary>
     ///     Spacing between each text row
index 52f3087b65809baa4de5c881ce44e146dca2f442..56c8e9504b1b07033679c6bc72f1f8fb7ed5ad4a 100644 (file)
@@ -12,10 +12,14 @@ namespace Content.Server.Communications
         /// <summary>
         /// Remaining cooldown between making announcements.
         /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
+        [ViewVariables]
         [DataField]
         public float AnnouncementCooldownRemaining;
 
+        [ViewVariables]
+        [DataField]
+        public float BroadcastCooldownRemaining;
+
         /// <summary>
         /// Fluent ID for the announcement title
         /// If a Fluent ID isn't found, just uses the raw string
@@ -27,28 +31,28 @@ namespace Content.Server.Communications
         /// <summary>
         /// Announcement color
         /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
+        [ViewVariables]
         [DataField]
         public Color Color = Color.Gold;
 
         /// <summary>
         /// Time in seconds between announcement delays on a per-console basis
         /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
+        [ViewVariables]
         [DataField]
         public int Delay = 90;
 
         /// <summary>
         /// Time in seconds of announcement cooldown when a new console is created on a per-console basis
         /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
+        [ViewVariables]
         [DataField]
         public int InitialDelay = 30;
 
         /// <summary>
         /// Can call or recall the shuttle
         /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
+        [ViewVariables]
         [DataField]
         public bool CanShuttle = true;
 
index 6a4cd23ba12b4e6ee54b57ee7e4db94328418059..4fa60563bddba80fba76da718a30c2ecbf3cfc3d 100644 (file)
@@ -3,9 +3,14 @@ using Content.Server.Access.Systems;
 using Content.Server.Administration.Logs;
 using Content.Server.AlertLevel;
 using Content.Server.Chat.Systems;
+using Content.Server.DeviceNetwork;
+using Content.Server.DeviceNetwork.Components;
+using Content.Server.DeviceNetwork.Systems;
 using Content.Server.Interaction;
 using Content.Server.Popups;
 using Content.Server.RoundEnd;
+using Content.Server.Screens;
+using Content.Server.Screens.Components;
 using Content.Server.Shuttles.Systems;
 using Content.Server.Station.Systems;
 using Content.Shared.Access.Components;
@@ -27,6 +32,7 @@ namespace Content.Server.Communications
         [Dependency] private readonly InteractionSystem _interaction = default!;
         [Dependency] private readonly AlertLevelSystem _alertLevelSystem = default!;
         [Dependency] private readonly ChatSystem _chatSystem = default!;
+        [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
         [Dependency] private readonly EmergencyShuttleSystem _emergency = default!;
         [Dependency] private readonly IdCardSystem _idCardSystem = default!;
         [Dependency] private readonly PopupSystem _popupSystem = default!;
@@ -49,6 +55,7 @@ namespace Content.Server.Communications
             // Messages from the BUI
             SubscribeLocalEvent<CommunicationsConsoleComponent, CommunicationsConsoleSelectAlertLevelMessage>(OnSelectAlertLevelMessage);
             SubscribeLocalEvent<CommunicationsConsoleComponent, CommunicationsConsoleAnnounceMessage>(OnAnnounceMessage);
+            SubscribeLocalEvent<CommunicationsConsoleComponent, CommunicationsConsoleBroadcastMessage>(OnBroadcastMessage);
             SubscribeLocalEvent<CommunicationsConsoleComponent, CommunicationsConsoleCallEmergencyShuttleMessage>(OnCallShuttleMessage);
             SubscribeLocalEvent<CommunicationsConsoleComponent, CommunicationsConsoleRecallEmergencyShuttleMessage>(OnRecallShuttleMessage);
 
@@ -162,6 +169,7 @@ namespace Content.Server.Communications
 
             _uiSystem.SetUiState(ui, new CommunicationsConsoleInterfaceState(
                 CanAnnounce(comp),
+                CanBroadcast(comp),
                 CanCallOrRecall(comp),
                 levels,
                 currentLevel,
@@ -175,6 +183,11 @@ namespace Content.Server.Communications
             return comp.AnnouncementCooldownRemaining <= 0f;
         }
 
+        private static bool CanBroadcast(CommunicationsConsoleComponent comp)
+        {
+            return comp.AnnouncementCooldownRemaining <= 0f;
+        }
+
         private bool CanUse(EntityUid user, EntityUid console)
         {
             // This shouldn't technically be possible because of BUI but don't trust client.
@@ -278,6 +291,19 @@ namespace Content.Server.Communications
                 _adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(message.Session.AttachedEntity.Value):player} has sent the following station announcement: {msg}");
         }
 
+        private void OnBroadcastMessage(EntityUid uid, CommunicationsConsoleComponent component, CommunicationsConsoleBroadcastMessage message)
+        {
+            if (!TryComp<DeviceNetworkComponent>(uid, out var net))
+                return;
+
+            var payload = new NetworkPayload
+            {
+                [ScreenMasks.Text] = message.Message
+            };
+
+            _deviceNetworkSystem.QueuePacket(uid, null, payload, net.TransmitFrequency);
+        }
+
         private void OnCallShuttleMessage(EntityUid uid, CommunicationsConsoleComponent comp, CommunicationsConsoleCallEmergencyShuttleMessage message)
         {
             if (!CanCallOrRecall(comp))
index 88c55154a320e99c9101bbec8696f9f54861d02f..10d4bea8b540537ffe663aeb0eb03280eb7bf161 100644 (file)
@@ -8,6 +8,7 @@ using Content.Server.DeviceNetwork;
 using Content.Server.DeviceNetwork.Components;
 using Content.Server.DeviceNetwork.Systems;
 using Content.Server.GameTicking;
+using Content.Server.Screens.Components;
 using Content.Server.Shuttles.Components;
 using Content.Server.Shuttles.Systems;
 using Content.Server.Station.Components;
@@ -16,7 +17,6 @@ using Content.Shared.Database;
 using Content.Shared.GameTicking;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Configuration;
-using Robust.Shared.Map;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
@@ -34,14 +34,11 @@ namespace Content.Server.RoundEnd
         [Dependency] private readonly IConfigurationManager _cfg = default!;
         [Dependency] private readonly IChatManager _chatManager = default!;
         [Dependency] private readonly IGameTiming _gameTiming = default!;
-        [Dependency] private readonly IMapManager _mapManager = default!;
-
         [Dependency] private readonly IPrototypeManager _protoManager = default!;
         [Dependency] private readonly ChatSystem _chatSystem = default!;
         [Dependency] private readonly GameTicker _gameTicker = default!;
         [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
         [Dependency] private readonly EmergencyShuttleSystem _shuttle = default!;
-        [Dependency] private readonly ShuttleTimerSystem _shuttleTimerSystem = default!;
         [Dependency] private readonly SharedAudioSystem _audio = default!;
         [Dependency] private readonly StationSystem _stationSystem = default!;
 
@@ -112,13 +109,9 @@ namespace Content.Server.RoundEnd
         /// </summary>
         public EntityUid? GetCentcomm()
         {
-            if (AllEntityQuery<StationCentcommComponent, TransformComponent>()
-                .MoveNext(out var centcomm, out var xform))
-            {
-                return xform.MapUid;
-            }
+            AllEntityQuery<StationCentcommComponent>().MoveNext(out var centcomm);
 
-            return null;
+            return centcomm == null ? null : centcomm.MapEntity;
         }
 
         public bool CanCallOrRecall()
@@ -243,7 +236,7 @@ namespace Content.Server.RoundEnd
             ActivateCooldown();
             RaiseLocalEvent(RoundEndSystemChangedEvent.Default);
 
-            // remove all active shuttle timers
+            // remove active clientside evac shuttle timers by zeroing the target time
             var zero = TimeSpan.Zero;
             var shuttle = _shuttle.GetShuttle();
             if (shuttle != null && TryComp<DeviceNetworkComponent>(shuttle, out var net))
@@ -256,7 +249,6 @@ namespace Content.Server.RoundEnd
                     [ShuttleTimerMasks.ShuttleTime] = zero,
                     [ShuttleTimerMasks.SourceTime] = zero,
                     [ShuttleTimerMasks.DestTime] = zero,
-                    [ShuttleTimerMasks.Text] = new string?[] { string.Empty, string.Empty }
                 };
                 _deviceNetworkSystem.QueuePacket(shuttle.Value, null, payload, net.TransmitFrequency);
             }
diff --git a/Content.Server/Screens/Components/ScreenComponent.cs b/Content.Server/Screens/Components/ScreenComponent.cs
new file mode 100644 (file)
index 0000000..1ed7608
--- /dev/null
@@ -0,0 +1,35 @@
+namespace Content.Server.Screens.Components;
+
+[RegisterComponent]
+public sealed partial class ScreenComponent : Component
+{
+
+}
+
+/// <summary>
+///     Player-facing hashable string consts for NetworkPayload
+/// </summary>
+public sealed class ScreenMasks
+{
+    public static readonly string Text = Loc.GetString("screen-text");
+    public static readonly string Color = Loc.GetString("screen-color");
+}
+
+/// <summary>
+///     Player-facing hashable string consts for NetworkPayload
+/// </summary>
+public sealed class ShuttleTimerMasks
+{
+    public static readonly string ShuttleTime = Loc.GetString("shuttle-timer-shuttle-time");
+    public static readonly string DestTime = Loc.GetString("shuttle-timer-dest-time");
+    public static readonly string SourceTime = Loc.GetString("shuttle-timer-source-time");
+    public static readonly string ShuttleMap = Loc.GetString("shuttle-timer-shuttle-map");
+    public static readonly string SourceMap = Loc.GetString("shuttle-timer-source-map");
+    public static readonly string DestMap = Loc.GetString("shuttle-timer-dest-map");
+    public static readonly string Docked = Loc.GetString("shuttle-timer-docked");
+    public static readonly string ETA = Loc.GetString("shuttle-timer-eta");
+    public static readonly string ETD = Loc.GetString("shuttle-timer-etd");
+    public static readonly string Bye = Loc.GetString("shuttle-timer-bye");
+    public static readonly string Kill = Loc.GetString("shuttle-timer-kill");
+}
+
diff --git a/Content.Server/Screens/Systems/ScreenSystem.cs b/Content.Server/Screens/Systems/ScreenSystem.cs
new file mode 100644 (file)
index 0000000..19790f6
--- /dev/null
@@ -0,0 +1,114 @@
+using Content.Shared.TextScreen;
+using Content.Server.Screens.Components;
+using Content.Server.DeviceNetwork.Components;
+using Content.Server.DeviceNetwork.Systems;
+using Robust.Shared.Timing;
+
+
+namespace Content.Server.Screens.Systems;
+
+/// <summary>
+/// Controls the wallmounted screens on stations and shuttles displaying e.g. FTL duration, ETA
+/// </summary>
+public sealed class ScreenSystem : EntitySystem
+{
+    [Dependency] private readonly IGameTiming _gameTiming = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<ScreenComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
+    }
+
+    /// <summary>
+    ///     Calls either a normal screen text update or shuttle timer update based on the presence of
+    ///     <see cref="ShuttleTimerMasks.ShuttleMap"/> in <see cref="args.Data"/>
+    /// </summary>
+    private void OnPacketReceived(EntityUid uid, ScreenComponent component, DeviceNetworkPacketEvent args)
+    {
+        if (args.Data.TryGetValue(ShuttleTimerMasks.ShuttleMap, out _))
+            ShuttleTimer(uid, component, args);
+        else
+            ScreenText(uid, component, args);
+    }
+
+    /// <summary>
+    ///     Send a text update to every screen on the same MapUid as the originating comms console.
+    /// </summary>
+    private void ScreenText(EntityUid uid, ScreenComponent component, DeviceNetworkPacketEvent args)
+    {
+        // don't allow text updates if there's an active timer
+        // (and just check here so the server doesn't have to track them)
+        if (_appearanceSystem.TryGetData(uid, TextScreenVisuals.TargetTime, out TimeSpan target)
+            && target > _gameTiming.CurTime)
+            return;
+
+        var screenMap = Transform(uid).MapUid;
+        var argsMap = Transform(args.Sender).MapUid;
+
+        if (screenMap != null
+            && argsMap != null
+            && screenMap == argsMap
+            && args.Data.TryGetValue(ScreenMasks.Text, out string? text)
+            && text != null
+            )
+        {
+            _appearanceSystem.SetData(uid, TextScreenVisuals.DefaultText, text);
+        }
+    }
+
+    /// <summary>
+    /// Determines if/how a timer packet affects this screen.
+    /// Currently there are 2 broadcast domains: Arrivals, and every other screen.
+    /// Domain is determined by the <see cref="DeviceNetworkComponent.TransmitFrequencyId"/> on each timer.
+    /// Each broadcast domain is divided into subnets. Screen MapUid determines subnet.
+    /// Subnets are the shuttle, source, and dest. Source/dest change each jump.
+    /// This is required to send different timers to the shuttle/terminal/station.
+    /// </summary>
+    private void ShuttleTimer(EntityUid uid, ScreenComponent component, DeviceNetworkPacketEvent args)
+    {
+        var timerXform = Transform(uid);
+
+        // no false positives.
+        if (timerXform.MapUid == null)
+            return;
+
+        string key;
+        args.Data.TryGetValue(ShuttleTimerMasks.ShuttleMap, out EntityUid? shuttleMap);
+        args.Data.TryGetValue(ShuttleTimerMasks.SourceMap, out EntityUid? source);
+        args.Data.TryGetValue(ShuttleTimerMasks.DestMap, out EntityUid? dest);
+        args.Data.TryGetValue(ShuttleTimerMasks.Docked, out bool docked);
+        string text = docked ? ShuttleTimerMasks.ETD : ShuttleTimerMasks.ETA;
+
+        switch (timerXform.MapUid)
+        {
+            // sometimes the timer transforms on FTL shuttles have a hyperspace mapuid, so matching by grid works as a fallback.
+            case var local when local == shuttleMap || timerXform.GridUid == shuttleMap:
+                key = ShuttleTimerMasks.ShuttleTime;
+                break;
+            case var origin when origin == source:
+                key = ShuttleTimerMasks.SourceTime;
+                break;
+            case var remote when remote == dest:
+                key = ShuttleTimerMasks.DestTime;
+                text = ShuttleTimerMasks.ETA;
+                break;
+            default:
+                return;
+        }
+
+        if (!args.Data.TryGetValue(key, out TimeSpan duration))
+            return;
+
+        if (args.Data.TryGetValue(ScreenMasks.Text, out string? label) && label != null)
+            text = label;
+
+        _appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, text);
+        _appearanceSystem.SetData(uid, TextScreenVisuals.TargetTime, _gameTiming.CurTime + duration);
+
+        if (args.Data.TryGetValue(ScreenMasks.Color, out Color color))
+            _appearanceSystem.SetData(uid, TextScreenVisuals.Color, color);
+    }
+}
diff --git a/Content.Server/Shuttles/Components/ShuttleTimerComponent.cs b/Content.Server/Shuttles/Components/ShuttleTimerComponent.cs
deleted file mode 100644 (file)
index 03b4f05..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-namespace Content.Server.Shuttles.Components;
-
-[RegisterComponent]
-public sealed partial class ShuttleTimerComponent : Component
-{
-
-}
-
-/// <summary>
-///     Awkward hashable string consts because NetworkPayload requires string keys
-///     TODO: Refactor NetworkPayload to accept bytes from enums?
-/// </summary>
-public sealed class ShuttleTimerMasks
-{
-    public static readonly string ShuttleTime = "ShuttleTime";
-    public static readonly string DestTime = "DestTime";
-    public static readonly string SourceTime = "SourceTime";
-    public static readonly string ShuttleMap = "ShuttleMap";
-    public static readonly string SourceMap = "SourceMap";
-    public static readonly string DestMap = "DestMap";
-    public static readonly string Docked = "Docked";
-    public static readonly string Text = "Text";
-}
-
index 41f455cea1fa843f025e1932347b8373fb9f33d3..f1f52c224152da3537954fec1a11f55e59ddee6b 100644 (file)
@@ -8,6 +8,7 @@ using Content.Server.DeviceNetwork;
 using Content.Server.DeviceNetwork.Components;
 using Content.Server.DeviceNetwork.Systems;
 using Content.Server.Salvage;
+using Content.Server.Screens.Components;
 using Content.Server.Shuttles.Components;
 using Content.Server.Shuttles.Events;
 using Content.Server.Spawners.Components;
index d14c5be43887f65e95c3333230c367c2ec0a9632..3b9a12a3310ca5bc71096eb372467c19630b5f43 100644 (file)
@@ -1,6 +1,7 @@
 using System.Threading;
 using Content.Server.DeviceNetwork;
 using Content.Server.DeviceNetwork.Components;
+using Content.Server.Screens.Components;
 using Content.Server.Shuttles.Components;
 using Content.Server.Shuttles.Events;
 using Content.Server.UserInterface;
index eb205cae63381a7c681599a3cf06148eeca2fa3e..d984c727376dd641022486f8c69bb3c647a496cd 100644 (file)
@@ -11,6 +11,7 @@ using Content.Server.DeviceNetwork.Systems;
 using Content.Server.GameTicking.Events;
 using Content.Server.Popups;
 using Content.Server.RoundEnd;
+using Content.Server.Screens.Components;
 using Content.Server.Shuttles.Components;
 using Content.Server.Shuttles.Events;
 using Content.Server.Station.Components;
@@ -235,8 +236,18 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem
                 [ShuttleTimerMasks.ShuttleTime] = countdownTime,
                 [ShuttleTimerMasks.SourceTime] = countdownTime,
                 [ShuttleTimerMasks.DestTime] = countdownTime,
-                [ShuttleTimerMasks.Text] = new string?[] { "BYE!" }
             };
+
+            // by popular request
+            // https://discord.com/channels/310555209753690112/770682801607278632/1189989482234126356
+            if (_random.Next(1000) == 0)
+            {
+                payload.Add(ScreenMasks.Text, ShuttleTimerMasks.Kill);
+                payload.Add(ScreenMasks.Color, Color.Red);
+            }
+            else
+                payload.Add(ScreenMasks.Text, ShuttleTimerMasks.Bye);
+
             _deviceNetworkSystem.QueuePacket(shuttle, null, payload, net.TransmitFrequency);
         }
     }
diff --git a/Content.Server/Shuttles/Systems/ShuttleTimerSystem.cs b/Content.Server/Shuttles/Systems/ShuttleTimerSystem.cs
deleted file mode 100644 (file)
index 9a098cd..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-using Content.Shared.TextScreen;
-using Content.Server.Shuttles.Components;
-using Content.Server.DeviceNetwork.Systems;
-using Robust.Shared.Timing;
-
-
-namespace Content.Server.Shuttles.Systems;
-
-/// <summary>
-/// Controls the wallmounted screens on stations and shuttles displaying e.g. FTL duration, ETA
-/// </summary>
-public sealed class ShuttleTimerSystem : EntitySystem
-{
-    [Dependency] private readonly IGameTiming _gameTiming = default!;
-    [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
-
-    public override void Initialize()
-    {
-        base.Initialize();
-
-        SubscribeLocalEvent<ShuttleTimerComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
-    }
-
-    /// <summary>
-    /// Determines if/how a broadcast packet affects this timer.
-    /// All shuttle timer packets are broadcast in their network, and subnetting is implemented by filtering timer MapUid.
-    /// </summary>
-    private void OnPacketReceived(EntityUid uid, ShuttleTimerComponent component, DeviceNetworkPacketEvent args)
-    {
-        var timerXform = Transform(uid);
-
-        // no false positives.
-        if (timerXform.MapUid == null)
-            return;
-
-        string key;
-        args.Data.TryGetValue(ShuttleTimerMasks.ShuttleMap, out EntityUid? shuttleMap);
-        args.Data.TryGetValue(ShuttleTimerMasks.SourceMap, out EntityUid? source);
-        args.Data.TryGetValue(ShuttleTimerMasks.DestMap, out EntityUid? dest);
-        args.Data.TryGetValue(ShuttleTimerMasks.Docked, out bool docked);
-        string?[] text = new string?[] { docked ? Loc.GetString("shuttle-timer-etd") : Loc.GetString("shuttle-timer-eta")};
-
-        switch (timerXform.MapUid)
-        {
-            // sometimes the timer transforms on FTL shuttles have a hyperspace mapuid, so matching by grid works as a fallback.
-            case var local when local == shuttleMap || timerXform.GridUid == shuttleMap:
-                key = ShuttleTimerMasks.ShuttleTime;
-                break;
-            case var origin when origin == source:
-                key = ShuttleTimerMasks.SourceTime;
-                break;
-            case var remote when remote == dest:
-                key = ShuttleTimerMasks.DestTime;
-                text = new string?[] { Loc.GetString("shuttle-timer-eta") };
-                break;
-            default:
-                return;
-        }
-
-        if (!args.Data.TryGetValue(key, out TimeSpan duration))
-            return;
-
-        if (args.Data.TryGetValue(ShuttleTimerMasks.Text, out string?[]? label))
-            text = label;
-
-        _appearanceSystem.SetData(uid, TextScreenVisuals.TargetTime, _gameTiming.CurTime + duration);
-        _appearanceSystem.SetData(uid, TextScreenVisuals.ScreenText, text);
-    }
-}
index 7f9094726c843dd53d0767cd92e677437e340857..bf0f5195b714fb01d1bd34a295c8d37ebbb55ed7 100644 (file)
@@ -11,6 +11,7 @@ namespace Content.Shared.Communications
     public sealed class CommunicationsConsoleInterfaceState : BoundUserInterfaceState
     {
         public readonly bool CanAnnounce;
+        public readonly bool CanBroadcast;
         public readonly bool CanCall;
         public readonly TimeSpan? ExpectedCountdownEnd;
         public readonly bool CountdownStarted;
@@ -18,9 +19,10 @@ namespace Content.Shared.Communications
         public string CurrentAlert;
         public float CurrentAlertDelay;
 
-        public CommunicationsConsoleInterfaceState(bool canAnnounce, bool canCall, List<string>? alertLevels, string currentAlert, float currentAlertDelay, TimeSpan? expectedCountdownEnd = null)
+        public CommunicationsConsoleInterfaceState(bool canAnnounce, bool canBroadcast, bool canCall, List<string>? alertLevels, string currentAlert, float currentAlertDelay, TimeSpan? expectedCountdownEnd = null)
         {
             CanAnnounce = canAnnounce;
+            CanBroadcast = canBroadcast;
             CanCall = canCall;
             ExpectedCountdownEnd = expectedCountdownEnd;
             CountdownStarted = expectedCountdownEnd != null;
@@ -52,6 +54,16 @@ namespace Content.Shared.Communications
         }
     }
 
+    [Serializable, NetSerializable]
+    public sealed class CommunicationsConsoleBroadcastMessage : BoundUserInterfaceMessage
+    {
+        public readonly string Message;
+        public CommunicationsConsoleBroadcastMessage(string message)
+        {
+            Message = message;
+        }
+    }
+
     [Serializable, NetSerializable]
     public sealed class CommunicationsConsoleCallEmergencyShuttleMessage : BoundUserInterfaceMessage
     {
index 605428632d74c5b3fd9d14fc7b6a64bdcffd5b21..102a0cec5ce582f8e0873e4c388b74cffc4b3cc4 100644 (file)
@@ -5,15 +5,15 @@ namespace Content.Shared.TextScreen;
 [Serializable, NetSerializable]
 public enum TextScreenVisuals : byte
 {
+    // TODO: support for a small image, I think. Probably want to rename textscreen to just screen then.
     /// <summary>
-    ///     Should this show any text? <br/>
-    ///     Expects a <see cref="bool"/>.
+    ///     What text to default to after timer completion?
+    ///     Expects a <see cref="string"/>.
     /// </summary>
-    On,
-
+    DefaultText,
     /// <summary>
-    ///     What text to show? <br/>
-    ///     Expects a <see cref="string?[]"/>.
+    ///     What text to render? <br/>
+    ///     Expects a <see cref="string"/>.
     /// </summary>
     ScreenText,
 
@@ -21,5 +21,11 @@ public enum TextScreenVisuals : byte
     ///     What is the target time? <br/>
     ///     Expects a <see cref="TimeSpan"/>.
     /// </summary>
-    TargetTime
+    TargetTime,
+
+    /// <summary>
+    ///     Change text color on the entire screen
+    ///     Expects a <see cref="Color"/>.
+    /// </summary>
+    Color
 }
index a3b95940dfacd49dd555f70f18321217e9c8bf19..f7cc87cb8ba1b1fa40c2f02f9f789fb40f98f275 100644 (file)
@@ -2,6 +2,7 @@
 comms-console-menu-title = Communications Console
 comms-console-menu-announcement-placeholder = Announcement text...
 comms-console-menu-announcement-button = Announce
+comms-console-menu-broadcast-button = Broadcast
 comms-console-menu-call-shuttle = Call emergency shuttle
 comms-console-menu-recall-shuttle = Recall emergency shuttle
 
diff --git a/Resources/Locale/en-US/shuttles/screens.ftl b/Resources/Locale/en-US/shuttles/screens.ftl
new file mode 100644 (file)
index 0000000..7a1a60e
--- /dev/null
@@ -0,0 +1,2 @@
+screens-text = text
+screens-color = color
index 82ca71d65402b91913b692a40c1154863fffd126..2b489707c4046e7795ee8778eba74fc198055bd1 100644 (file)
@@ -1,2 +1,11 @@
 shuttle-timer-eta = ETA
 shuttle-timer-etd = ETD
+shuttle-timer-shuttle-time = ShuttleTime
+shuttle-timer-source-time = SourceTime
+shuttle-timer-dest-time = DestTime
+shuttle-timer-shuttle-map = ShuttleMap
+shuttle-timer-source-map = SourceMap
+shuttle-timer-dest-map = DestMap
+shuttle-timer-docked = Docked
+shuttle-timer-bye = BYE!
+shuttle-timer-kill = KILL
index c0c91d57c69d3711dde844c61cef7afb07bed529..5aa1680aaeacf3febdc827a9ae1157cd30cd98ac 100644 (file)
     access: [[ "Command" ]]
   - type: CommunicationsConsole
     title: comms-console-announcement-title-station
+  - type: DeviceNetwork
+    transmitFrequencyId: ShuttleTimer
   - type: ActivatableUI
     key: enum.CommunicationsConsoleUiKey.Key
   - type: UserInterface
index c0f1b56b0d482c32c9f54441310e705733daa4b4..f3fcb5062a83f7ce1bc1bd703c4b97dc6d971b48 100644 (file)
@@ -2,12 +2,17 @@
   id: Screen
   name: screen
   description: Displays text or time.
+  placement:
+    mode: SnapgridCenter
+    snap:
+    - Wallmount
   components:
   - type: Transform
     anchored: true
   - type: WallMount
     arc: 360
   - type: InteractionOutline
+  - type: Clickable
   - type: Appearance
   - type: Rotatable
   - type: TextScreenVisuals
@@ -28,7 +33,7 @@
     enabled: false
     usesApcPower: true
   - type: ExtensionCableReceiver
-  - type: ShuttleTimer
+  - type: Screen
   - type: DeviceNetwork
     receiveFrequencyId: ShuttleTimer
 
index fbb2b7fe5668b3d9f44bd2c46fb90642e52ac191..0c284eec7afcc2516ecc901defcb26e33ee4dd98 100644 (file)
@@ -58,6 +58,7 @@
     textOffset: 0,8
     timerOffset: 0,8
     textLength: 5
+    rows: 1
   - type: Sprite
     drawdepth: WallMountedItems
     sprite: Structures/Wallmounts/signalscreen.rsi