diff --git a/Content.Client/GameObjects/Components/ComputerBoundUserInterface.cs b/Content.Client/GameObjects/Components/ComputerBoundUserInterface.cs
new file mode 100644
index 0000000000..76d8bf0995
--- /dev/null
+++ b/Content.Client/GameObjects/Components/ComputerBoundUserInterface.cs
@@ -0,0 +1,78 @@
+using System;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Maths;
+using Robust.Shared.Timing;
+
+namespace Content.Client.GameObjects.Components
+{
+ ///
+ /// ComputerBoundUserInterface shunts all sorts of responsibilities that are in the BoundUserInterface for architectural reasons into the Window.
+ /// NOTE: Despite the name, ComputerBoundUserInterface does not and will not care about things like power.
+ ///
+ public class ComputerBoundUserInterface : ComputerBoundUserInterfaceBase where W : BaseWindow, IComputerWindow, new() where S : BoundUserInterfaceState
+ {
+ [Dependency] private readonly IDynamicTypeFactory _dynamicTypeFactory = default!;
+ private W? _window;
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = (W) _dynamicTypeFactory.CreateInstance(typeof(W));
+ _window.SetupComputerWindow(this);
+ _window.OnClose += Close;
+ _window.OpenCentered();
+ }
+
+ // Alas, this constructor has to be copied to the subclass. :(
+ public ComputerBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (_window == null)
+ {
+ return;
+ }
+
+ _window.UpdateState((S) state);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (disposing)
+ {
+ _window?.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// This class is to avoid a lot of <> being written when we just want to refer to SendMessage.
+ /// We could instead qualify a lot of generics even further, but that is a waste of time.
+ ///
+ public class ComputerBoundUserInterfaceBase : BoundUserInterface
+ {
+ public ComputerBoundUserInterfaceBase(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
+ public void SendMessage(BoundUserInterfaceMessage msg)
+ {
+ base.SendMessage(msg);
+ }
+ }
+
+ public interface IComputerWindow
+ {
+ void SetupComputerWindow(ComputerBoundUserInterfaceBase cb) {}
+ void UpdateState(S state) {}
+ }
+}
+
diff --git a/Content.Client/GameObjects/Components/Power/SolarControlWindow.xaml b/Content.Client/GameObjects/Components/Power/SolarControlWindow.xaml
new file mode 100644
index 0000000000..a51f57154c
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Power/SolarControlWindow.xaml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/GameObjects/Components/Power/SolarControlWindow.xaml.cs b/Content.Client/GameObjects/Components/Power/SolarControlWindow.xaml.cs
new file mode 100644
index 0000000000..5ced2ae49d
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Power/SolarControlWindow.xaml.cs
@@ -0,0 +1,164 @@
+using System;
+using JetBrains.Annotations;
+using Content.Shared.Solar;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Shared.GameObjects;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Localization;
+using Robust.Shared.Maths;
+using Robust.Shared.Timing;
+
+namespace Content.Client.GameObjects.Components.Power
+{
+ [GenerateTypedNameReferences]
+ public sealed partial class SolarControlWindow : SS14Window, IComputerWindow
+ {
+ private SolarControlConsoleBoundInterfaceState _lastState = new(0, 0, 0, 0);
+
+ public SolarControlWindow()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ public void SetupComputerWindow(ComputerBoundUserInterfaceBase cb)
+ {
+ PanelRotation.OnTextEntered += (text) => {
+ double value;
+ if (double.TryParse(text.Text, out value))
+ {
+ SolarControlConsoleAdjustMessage msg = new SolarControlConsoleAdjustMessage();
+ msg.Rotation = Angle.FromDegrees(value);
+ msg.AngularVelocity = _lastState.AngularVelocity;
+ cb.SendMessage(msg);
+ // Predict this...
+ _lastState.Rotation = msg.Rotation;
+ NotARadar.UpdateState(_lastState);
+ }
+ };
+ PanelVelocity.OnTextEntered += (text) => {
+ double value;
+ if (double.TryParse(text.Text, out value))
+ {
+ SolarControlConsoleAdjustMessage msg = new SolarControlConsoleAdjustMessage();
+ msg.Rotation = NotARadar.PredictedPanelRotation;
+ msg.AngularVelocity = Angle.FromDegrees(value / 60);
+ cb.SendMessage(msg);
+ // Predict this...
+ _lastState.Rotation = NotARadar.PredictedPanelRotation;
+ _lastState.AngularVelocity = msg.AngularVelocity;
+ NotARadar.UpdateState(_lastState);
+ }
+ };
+ }
+
+ private string FormatAngle(Angle d)
+ {
+ return d.Degrees.ToString("F1");
+ }
+
+ // The idea behind this is to prevent every update from the server
+ // breaking the textfield.
+ private void UpdateField(LineEdit field, string newValue)
+ {
+ if (!field.HasKeyboardFocus())
+ {
+ field.Text = newValue;
+ }
+ }
+
+ public void UpdateState(SolarControlConsoleBoundInterfaceState scc)
+ {
+ _lastState = scc;
+ NotARadar.UpdateState(scc);
+ OutputPower.Text = ((int) MathF.Floor(scc.OutputPower)).ToString();
+ SunAngle.Text = FormatAngle(scc.TowardsSun);
+ UpdateField(PanelRotation, FormatAngle(scc.Rotation));
+ UpdateField(PanelVelocity, FormatAngle(scc.AngularVelocity * 60));
+ }
+
+ }
+
+ public sealed class SolarControlNotARadar : Control
+ {
+ // This is used for client-side prediction of the panel rotation.
+ // This makes the display feel a lot smoother.
+ private IGameTiming _gameTiming = IoCManager.Resolve();
+
+ private SolarControlConsoleBoundInterfaceState _lastState = new(0, 0, 0, 0);
+
+ private TimeSpan _lastStateTime = TimeSpan.Zero;
+
+ public const int StandardSizeFull = 290;
+ public const int StandardRadiusCircle = 140;
+ public int SizeFull => (int) (StandardSizeFull * UIScale);
+ public int RadiusCircle => (int) (StandardRadiusCircle * UIScale);
+
+ public SolarControlNotARadar()
+ {
+ MinSize = (SizeFull, SizeFull);
+ }
+
+ public void UpdateState(SolarControlConsoleBoundInterfaceState ls)
+ {
+ _lastState = ls;
+ _lastStateTime = _gameTiming.CurTime;
+ }
+
+ public Angle PredictedPanelRotation => _lastState.Rotation + (_lastState.AngularVelocity * ((_gameTiming.CurTime - _lastStateTime).TotalSeconds));
+
+ protected override void Draw(DrawingHandleScreen handle)
+ {
+ var point = SizeFull / 2;
+ var fakeAA = new Color(0.08f, 0.08f, 0.08f);
+ var gridLines = new Color(0.08f, 0.08f, 0.08f);
+ var panelExtentCutback = 4;
+ var gridLinesRadial = 8;
+ var gridLinesEquatorial = 8;
+
+ // Draw base
+ handle.DrawCircle((point, point), RadiusCircle + 1, fakeAA);
+ handle.DrawCircle((point, point), RadiusCircle, Color.Black);
+
+ // Draw grid lines
+ for (var i = 0; i < gridLinesEquatorial; i++)
+ {
+ handle.DrawCircle((point, point), (RadiusCircle / gridLinesEquatorial) * i, gridLines, false);
+ }
+
+ for (var i = 0; i < gridLinesRadial; i++)
+ {
+ Angle angle = (Math.PI / gridLinesRadial) * i;
+ var aExtent = angle.ToVec() * RadiusCircle;
+ handle.DrawLine((point, point) - aExtent, (point, point) + aExtent, gridLines);
+ }
+
+ // The rotations need to be adjusted because Y is inverted in Robust (like BYOND)
+ Vector2 rotMul = (1, -1);
+ // Hotfix corrections I don't understand
+ Angle rotOfs = new Angle(Math.PI * -0.5);
+
+ Angle predictedPanelRotation = PredictedPanelRotation;
+
+ var extent = (predictedPanelRotation + rotOfs).ToVec() * rotMul * RadiusCircle;
+ Vector2 extentOrtho = (extent.Y, -extent.X);
+ handle.DrawLine((point, point) - extentOrtho, (point, point) + extentOrtho, Color.White);
+ handle.DrawLine((point, point) + (extent / panelExtentCutback), (point, point) + extent - (extent / panelExtentCutback), Color.DarkGray);
+
+ var sunExtent = (_lastState.TowardsSun + rotOfs).ToVec() * rotMul * RadiusCircle;
+ handle.DrawLine((point, point) + sunExtent, (point, point), Color.Yellow);
+ }
+ }
+
+ [UsedImplicitly]
+ public class SolarControlConsoleBoundUserInterface : ComputerBoundUserInterface
+ {
+ public SolarControlConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
+ }
+}
diff --git a/Content.Client/Solar/SolarControlConsoleBoundUserInterface.cs b/Content.Client/Solar/SolarControlConsoleBoundUserInterface.cs
deleted file mode 100644
index ed457181ec..0000000000
--- a/Content.Client/Solar/SolarControlConsoleBoundUserInterface.cs
+++ /dev/null
@@ -1,227 +0,0 @@
-using System;
-using Content.Shared.Solar;
-using JetBrains.Annotations;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Maths;
-using Robust.Shared.Timing;
-
-namespace Content.Client.Solar
-{
- [UsedImplicitly]
- public class SolarControlConsoleBoundUserInterface : BoundUserInterface
- {
- [Dependency] private readonly IGameTiming _gameTiming = default!;
-
- private SolarControlWindow? _window;
- private SolarControlConsoleBoundInterfaceState _lastState = new(0, 0, 0, 0);
-
- protected override void Open()
- {
- base.Open();
-
- _window = new SolarControlWindow(_gameTiming);
- _window.OnClose += Close;
- _window.PanelRotation.OnTextEntered += (text) => {
- double value;
- if (double.TryParse(text.Text, out value))
- {
- SolarControlConsoleAdjustMessage msg = new SolarControlConsoleAdjustMessage();
- msg.Rotation = Angle.FromDegrees(value);
- msg.AngularVelocity = _lastState.AngularVelocity;
- SendMessage(msg);
- }
- };
- _window.PanelVelocity.OnTextEntered += (text) => {
- double value;
- if (double.TryParse(text.Text, out value))
- {
- SolarControlConsoleAdjustMessage msg = new SolarControlConsoleAdjustMessage();
- msg.Rotation = _lastState.Rotation;
- msg.AngularVelocity = Angle.FromDegrees(value / 60);
- SendMessage(msg);
- }
- };
- _window.OpenCentered();
- }
-
- public SolarControlConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
- {
- }
-
- private string FormatAngle(Angle d)
- {
- return d.Degrees.ToString("F1");
- }
-
- // The idea behind this is to prevent every update from the server
- // breaking the textfield.
- private void UpdateField(LineEdit field, string newValue)
- {
- if (!field.HasKeyboardFocus())
- {
- field.Text = newValue;
- }
- }
-
- protected override void UpdateState(BoundUserInterfaceState state)
- {
- base.UpdateState(state);
-
- if (_window == null)
- {
- return;
- }
-
- SolarControlConsoleBoundInterfaceState scc = (SolarControlConsoleBoundInterfaceState) state;
- _lastState = scc;
- _window.NotARadar.UpdateState(scc);
- _window.OutputPower.Text = ((int) MathF.Floor(scc.OutputPower)).ToString();
- _window.SunAngle.Text = FormatAngle(scc.TowardsSun);
- UpdateField(_window.PanelRotation, FormatAngle(scc.Rotation));
- UpdateField(_window.PanelVelocity, FormatAngle(scc.AngularVelocity * 60));
- }
-
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
-
- if (disposing)
- {
- _window?.Dispose();
- }
- }
-
- private sealed class SolarControlWindow : SS14Window
- {
- public readonly Label OutputPower;
- public readonly Label SunAngle;
-
- public readonly SolarControlNotARadar NotARadar;
-
- public readonly LineEdit PanelRotation;
- public readonly LineEdit PanelVelocity;
-
- public SolarControlWindow(IGameTiming igt)
- {
- Title = "Solar Control Window";
-
- var rows = new GridContainer();
- rows.Columns = 2;
-
- // little secret: the reason I put the values
- // in the first column is because otherwise the UI
- // layouter autoresizes the window to be too small
- rows.AddChild(new Label {Text = "Output Power:"});
- rows.AddChild(new Label {Text = ""});
-
- rows.AddChild(OutputPower = new Label {Text = "?"});
- rows.AddChild(new Label {Text = "W"});
-
- rows.AddChild(new Label {Text = "Sun Angle:"});
- rows.AddChild(new Label {Text = ""});
-
- rows.AddChild(SunAngle = new Label {Text = "?"});
- rows.AddChild(new Label {Text = "°"});
-
- rows.AddChild(new Label {Text = "Panel Angle:"});
- rows.AddChild(new Label {Text = ""});
-
- rows.AddChild(PanelRotation = new LineEdit());
- rows.AddChild(new Label {Text = "°"});
-
- rows.AddChild(new Label {Text = "Panel Angular Velocity:"});
- rows.AddChild(new Label {Text = ""});
-
- rows.AddChild(PanelVelocity = new LineEdit());
- rows.AddChild(new Label {Text = "°/min."});
-
- rows.AddChild(new Label {Text = "Press Enter to confirm."});
- rows.AddChild(new Label {Text = ""});
-
- PanelRotation.HorizontalExpand = true;
- PanelVelocity.HorizontalExpand = true;
-
- NotARadar = new SolarControlNotARadar(igt);
-
- var outerColumns = new HBoxContainer();
- outerColumns.AddChild(rows);
- outerColumns.AddChild(NotARadar);
- Contents.AddChild(outerColumns);
- Resizable = false;
- }
- }
-
- private sealed class SolarControlNotARadar : Control
- {
- // IoC doesn't apply here, so it's propagated from the parent class.
- // This is used for client-side prediction of the panel rotation.
- // This makes the display feel a lot smoother.
- private IGameTiming _gameTiming;
-
- private SolarControlConsoleBoundInterfaceState _lastState = new(0, 0, 0, 0);
-
- private TimeSpan _lastStateTime = TimeSpan.Zero;
-
- public const int SizeFull = 290;
- public const int RadiusCircle = 140;
-
- public SolarControlNotARadar(IGameTiming igt)
- {
- _gameTiming = igt;
- MinSize = (SizeFull, SizeFull);
- }
-
- public void UpdateState(SolarControlConsoleBoundInterfaceState ls)
- {
- _lastState = ls;
- _lastStateTime = _gameTiming.CurTime;
- }
-
- protected override void Draw(DrawingHandleScreen handle)
- {
- var point = SizeFull / 2;
- var fakeAA = new Color(0.08f, 0.08f, 0.08f);
- var gridLines = new Color(0.08f, 0.08f, 0.08f);
- var panelExtentCutback = 4;
- var gridLinesRadial = 8;
- var gridLinesEquatorial = 8;
-
- // Draw base
- handle.DrawCircle((point, point), RadiusCircle + 1, fakeAA);
- handle.DrawCircle((point, point), RadiusCircle, Color.Black);
-
- // Draw grid lines
- for (var i = 0; i < gridLinesEquatorial; i++)
- {
- handle.DrawCircle((point, point), (RadiusCircle / gridLinesEquatorial) * i, gridLines, false);
- }
-
- for (var i = 0; i < gridLinesRadial; i++)
- {
- Angle angle = (Math.PI / gridLinesRadial) * i;
- var aExtent = angle.ToVec() * RadiusCircle;
- handle.DrawLine((point, point) - aExtent, (point, point) + aExtent, gridLines);
- }
-
- // The rotations need to be adjusted because Y is inverted in Robust (like BYOND)
- Vector2 rotMul = (1, -1);
-
- Angle predictedPanelRotation = _lastState.Rotation + (_lastState.AngularVelocity * ((_gameTiming.CurTime - _lastStateTime).TotalSeconds));
-
- var extent = predictedPanelRotation.ToVec() * rotMul * RadiusCircle;
- Vector2 extentOrtho = (extent.Y, -extent.X);
- handle.DrawLine((point, point) - extentOrtho, (point, point) + extentOrtho, Color.White);
- handle.DrawLine((point, point) + (extent / panelExtentCutback), (point, point) + extent - (extent / panelExtentCutback), Color.DarkGray);
-
- var sunExtent = _lastState.TowardsSun.ToVec() * rotMul * RadiusCircle;
- handle.DrawLine((point, point) + sunExtent, (point, point), Color.Yellow);
- }
- }
- }
-}
diff --git a/Content.Server/GameObjects/Components/BaseComputerUserInterfaceComponent.cs b/Content.Server/GameObjects/Components/BaseComputerUserInterfaceComponent.cs
new file mode 100644
index 0000000000..bb2b23e297
--- /dev/null
+++ b/Content.Server/GameObjects/Components/BaseComputerUserInterfaceComponent.cs
@@ -0,0 +1,141 @@
+using Content.Server.Power.Components;
+using Content.Server.UserInterface;
+using Content.Shared.ActionBlocker;
+using Content.Shared.Notification;
+using Content.Shared.Interaction;
+using Content.Shared.GameObjects.Components;
+using Content.Shared.GameObjects.EntitySystems;
+using Robust.Server.GameObjects;
+using Robust.Shared.Containers;
+using Robust.Shared.GameObjects;
+using Robust.Shared.ViewVariables;
+using Robust.Shared.Localization;
+
+namespace Content.Server.GameObjects.Components
+{
+ ///
+ /// This component is used as a base class for classes like SolarControlConsoleComponent.
+ /// These components operate the server-side logic for the "primary UI" of a computer.
+ /// That means showing the UI when a user activates it, for example.
+ ///
+ public abstract class BaseComputerUserInterfaceComponent : Component
+ {
+ protected readonly object UserInterfaceKey;
+
+ [ViewVariables] protected BoundUserInterface? UserInterface => Owner.GetUIOrNull(UserInterfaceKey);
+ [ViewVariables] public bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
+
+ public BaseComputerUserInterfaceComponent(object key)
+ {
+ UserInterfaceKey = key;
+ }
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ if (UserInterface != null)
+ UserInterface.OnReceiveMessage += OnReceiveUIMessageCallback;
+ }
+
+ ///
+ /// Internal callback used to grab session and session attached entity before any more work is done.
+ /// This is so that sessionEntity is always available to checks up and down the line.
+ ///
+ private void OnReceiveUIMessageCallback(ServerBoundUserInterfaceMessage obj)
+ {
+ var session = obj.Session;
+ var sessionEntity = session.AttachedEntity;
+ if (sessionEntity == null)
+ return; // No session entity, so we're probably not able to touch this.
+ OnReceiveUnfilteredUserInterfaceMessage(obj, sessionEntity);
+ }
+
+ ///
+ /// Override this to handle messages from the UI before filtering them.
+ /// Calling base is necessary if you want this class to have any meaning.
+ ///
+ protected void OnReceiveUnfilteredUserInterfaceMessage(ServerBoundUserInterfaceMessage obj, IEntity sessionEntity)
+ {
+ // "Across all computers" "anti-cheats" ought to be put here or at some parent level (BaseDeviceUserInterfaceComponent?)
+ // Determine some facts about the session.
+ // Powered?
+ if (!Powered)
+ {
+ sessionEntity.PopupMessageCursor(Loc.GetString("base-computer-ui-component-not-powered"));
+ return; // Not powered, so this computer should probably do nothing.
+ }
+ // Can we interact?
+ if (!ActionBlockerSystem.CanInteract(sessionEntity))
+ {
+ sessionEntity.PopupMessageCursor(Loc.GetString("base-computer-ui-component-cannot-interact"));
+ return;
+ }
+ // Good to go!
+ OnReceiveUserInterfaceMessage(obj);
+ }
+
+ ///
+ /// Override this to handle messages from the UI.
+ /// Calling base is unnecessary.
+ /// These messages will automatically be blocked if the user shouldn't be able to access this computer, or if the computer has lost power.
+ ///
+ protected virtual void OnReceiveUserInterfaceMessage(ServerBoundUserInterfaceMessage obj)
+ {
+ // Nothing!
+ }
+
+ public override void HandleMessage(ComponentMessage message, IComponent? component)
+ {
+ base.HandleMessage(message, component);
+ switch (message)
+ {
+ case PowerChangedMessage powerChanged:
+ PowerReceiverOnOnPowerStateChanged(powerChanged);
+ break;
+ }
+ }
+
+ private void PowerReceiverOnOnPowerStateChanged(PowerChangedMessage e)
+ {
+ if (!e.Powered)
+ {
+ // We need to kick off users who are using it when it loses power.
+ UserInterface?.CloseAll();
+ // Now alert subclass.
+ ComputerLostPower();
+ }
+ }
+
+ ///
+ /// Override this if you want the computer to do something when it loses power (i.e. reset state)
+ /// All UIs should have been closed by the time this is called.
+ /// Calling base is unnecessary.
+ ///
+ public virtual void ComputerLostPower()
+ {
+ }
+
+ ///
+ /// This is called from ComputerUIActivatorSystem.
+ /// Override this to add additional activation conditions of some sort.
+ /// Calling base runs standard activation logic.
+ /// *This remains inside the component for overridability.*
+ ///
+ public virtual void ActivateThunk(ActivateInWorldEvent eventArgs)
+ {
+ if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
+ {
+ return;
+ }
+
+ if (!Powered)
+ {
+ Owner.PopupMessage(eventArgs.User, Loc.GetString("base-computer-ui-component-not-powered"));
+ return;
+ }
+
+ UserInterface?.Open(actor.PlayerSession);
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/EntitySystems/ComputerUIActivatorSystem.cs b/Content.Server/GameObjects/EntitySystems/ComputerUIActivatorSystem.cs
new file mode 100644
index 0000000000..f0689cfcbf
--- /dev/null
+++ b/Content.Server/GameObjects/EntitySystems/ComputerUIActivatorSystem.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using System.Linq;
+using Content.Shared.Interaction;
+using Content.Server.GameObjects.Components;
+using Content.Shared.GameTicking;
+using Robust.Server.GameObjects;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Maths;
+using JetBrains.Annotations;
+
+namespace Content.Server.GameObjects.EntitySystems
+{
+ [UsedImplicitly]
+ internal sealed class ComputerUIActivatorSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(HandleActivate);
+ }
+
+ private void HandleActivate(EntityUid uid, BaseComputerUserInterfaceComponent component, ActivateInWorldEvent args)
+ {
+ component.ActivateThunk(args);
+ }
+ }
+}
diff --git a/Content.Server/Solar/Components/SolarControlConsoleComponent.cs b/Content.Server/Solar/Components/SolarControlConsoleComponent.cs
index 48c9f6c2b4..080dc9fc48 100644
--- a/Content.Server/Solar/Components/SolarControlConsoleComponent.cs
+++ b/Content.Server/Solar/Components/SolarControlConsoleComponent.cs
@@ -1,9 +1,8 @@
-#nullable enable
-using Content.Server.Power.Components;
-using Content.Server.Solar.EntitySystems;
-using Content.Server.UserInterface;
-using Content.Shared.Interaction;
+#nullable enable
using Content.Shared.Solar;
+using Content.Server.Solar.EntitySystems;
+using Content.Server.GameObjects.Components;
+using Content.Server.GameObjects.EntitySystems;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -12,26 +11,20 @@ using Robust.Shared.ViewVariables;
namespace Content.Server.Solar.Components
{
[RegisterComponent]
- [ComponentReference(typeof(IActivate))]
- public class SolarControlConsoleComponent : SharedSolarControlConsoleComponent, IActivate
+ [ComponentReference(typeof(BaseComputerUserInterfaceComponent))]
+ public class SolarControlConsoleComponent : BaseComputerUserInterfaceComponent
{
+ public override string Name => "SolarControlConsole";
+
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
private PowerSolarSystem _powerSolarSystem = default!;
- private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered;
- [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(SolarControlConsoleUiKey.Key);
+ public SolarControlConsoleComponent() : base(SolarControlConsoleUiKey.Key) { }
public override void Initialize()
{
base.Initialize();
-
- if (UserInterface != null)
- {
- UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage;
- }
-
- Owner.EnsureComponent();
_powerSolarSystem = _entitySystemManager.GetEntitySystem();
}
@@ -40,7 +33,7 @@ namespace Content.Server.Solar.Components
UserInterface?.SetState(new SolarControlConsoleBoundInterfaceState(_powerSolarSystem.TargetPanelRotation, _powerSolarSystem.TargetPanelVelocity, _powerSolarSystem.TotalPanelPower, _powerSolarSystem.TowardsSun));
}
- private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage obj)
+ protected override void OnReceiveUserInterfaceMessage(ServerBoundUserInterfaceMessage obj)
{
switch (obj.Message)
{
@@ -56,22 +49,5 @@ namespace Content.Server.Solar.Components
break;
}
}
-
- void IActivate.Activate(ActivateEventArgs eventArgs)
- {
- if (!eventArgs.User.TryGetComponent(out ActorComponent? actor))
- {
- return;
- }
-
- if (!Powered)
- {
- return;
- }
-
- // always update the UI immediately before opening, just in case
- UpdateUIState();
- UserInterface?.Open(actor.PlayerSession);
- }
}
}
diff --git a/Content.Server/Solar/EntitySystems/PowerSolarSystem.cs b/Content.Server/Solar/EntitySystems/PowerSolarSystem.cs
index 19490c0928..32460e5cae 100644
--- a/Content.Server/Solar/EntitySystems/PowerSolarSystem.cs
+++ b/Content.Server/Solar/EntitySystems/PowerSolarSystem.cs
@@ -137,7 +137,7 @@ namespace Content.Server.Solar.EntitySystems
{
// Determine if the solar panel is occluded, and zero out coverage if so.
// FIXME: The "Opaque" collision group doesn't seem to work right now.
- var ray = new CollisionRay(entity.Transform.WorldPosition, TowardsSun.ToVec(), (int) CollisionGroup.Opaque);
+ var ray = new CollisionRay(entity.Transform.WorldPosition, TowardsSun.ToWorldVec(), (int) CollisionGroup.Opaque);
var rayCastResults = EntitySystem.Get().IntersectRay(entity.Transform.MapID, ray, SunOcclusionCheckDistance, entity);
if (rayCastResults.Any())
coverage = 0;
diff --git a/Content.Shared/Solar/SharedSolarControlConsoleComponent.cs b/Content.Shared/Solar/SharedSolarControlConsoleComponent.cs
index dd00fdb1f4..492e83f7f0 100644
--- a/Content.Shared/Solar/SharedSolarControlConsoleComponent.cs
+++ b/Content.Shared/Solar/SharedSolarControlConsoleComponent.cs
@@ -6,12 +6,6 @@ using Robust.Shared.Serialization;
namespace Content.Shared.Solar
{
- public class SharedSolarControlConsoleComponent : Component
- {
- public override string Name => "SolarControlConsole";
-
- }
-
[Serializable, NetSerializable]
public class SolarControlConsoleBoundInterfaceState : BoundUserInterfaceState
{
diff --git a/Resources/Locale/en-US/components/base-computer-ui-component.ftl b/Resources/Locale/en-US/components/base-computer-ui-component.ftl
new file mode 100644
index 0000000000..dce198278b
--- /dev/null
+++ b/Resources/Locale/en-US/components/base-computer-ui-component.ftl
@@ -0,0 +1,3 @@
+base-computer-ui-component-cannot-interact = You can't interact with a computer right now.
+base-computer-ui-component-not-powered = The computer is not powered.
+
diff --git a/Resources/Locale/en-US/ui/solar-control.ftl b/Resources/Locale/en-US/ui/solar-control.ftl
new file mode 100644
index 0000000000..eb694df84a
--- /dev/null
+++ b/Resources/Locale/en-US/ui/solar-control.ftl
@@ -0,0 +1,10 @@
+solar-control-window-title = Solar Control Console
+solar-control-window-output-power = Output Power:
+solar-control-window-watts = W
+solar-control-window-sun-angle = Sun Angle:
+solar-control-window-degrees = °
+solar-control-window-panel-angle = Panel Angle:
+solar-control-window-panel-angular-velocity = Panel Angular Velocity:
+solar-control-window-degrees-per-minute = °/min.
+solar-control-window-press-enter-to-confirm = Press Enter to confirm.
+