Refactoring of solar control console (#4072)
* Refactor/fix client-side of solar control computer (introduce ComputerBoundUserInterface & fix bugs) * Refactor server side of solar control computer (introduce BaseComputerUserInterfaceComponent) * If you can't interact, then messages to computers are blocked. * Add 'not powered' messages, migrate activation logic partially to an EntitySystem * Move solar control console to a XAML UI * Remove useless comment on UserInterfaceKey * BaseComputerUserInterfaceComponent: Remove EnsureComponent<PowerReceiver>, it's not necessary * Fix solar panel occlusion check direction * Solar Control Console refactors/etc. : Handle namespace renames
This commit is contained in:
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public class ComputerBoundUserInterface<W, S> : ComputerBoundUserInterfaceBase where W : BaseWindow, IComputerWindow<S>, 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public class ComputerBoundUserInterfaceBase : BoundUserInterface
|
||||||
|
{
|
||||||
|
public ComputerBoundUserInterfaceBase(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
|
||||||
|
public void SendMessage(BoundUserInterfaceMessage msg)
|
||||||
|
{
|
||||||
|
base.SendMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IComputerWindow<S>
|
||||||
|
{
|
||||||
|
void SetupComputerWindow(ComputerBoundUserInterfaceBase cb) {}
|
||||||
|
void UpdateState(S state) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<SS14Window xmlns="https://spacestation14.io"
|
||||||
|
xmlns:scc="clr-namespace:Content.Client.GameObjects.Components.Power"
|
||||||
|
Title="{Loc 'solar-control-window-title'}"
|
||||||
|
Resizable="False">
|
||||||
|
<HBoxContainer>
|
||||||
|
<GridContainer 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
|
||||||
|
-->
|
||||||
|
|
||||||
|
<Label Text="{Loc 'solar-control-window-output-power'}"/><Label Text=""/>
|
||||||
|
<Label Name="OutputPower"/><Label Text="{Loc 'solar-control-window-watts'}"/>
|
||||||
|
|
||||||
|
<Label Text="{Loc 'solar-control-window-sun-angle'}"/><Label Text=""/>
|
||||||
|
<Label Name="SunAngle"/><Label Text="{Loc 'solar-control-window-degrees'}"/>
|
||||||
|
|
||||||
|
|
||||||
|
<Label Text="{Loc 'solar-control-window-panel-angle'}"/><Label Text=""/>
|
||||||
|
<LineEdit Name="PanelRotation" HorizontalExpand="True"/><Label Text="{Loc 'solar-control-window-degrees'}"/>
|
||||||
|
|
||||||
|
<Label Text="{Loc 'solar-control-window-panel-angular-velocity'}"/><Label Text=""/>
|
||||||
|
<LineEdit Name="PanelVelocity" HorizontalExpand="True"/><Label Text="{Loc 'solar-control-window-degrees-per-minute'}"/>
|
||||||
|
|
||||||
|
|
||||||
|
<Label Text="{Loc 'solar-control-window-press-enter-to-confirm'}"/><Label Text=""/>
|
||||||
|
</GridContainer>
|
||||||
|
<scc:SolarControlNotARadar Name="NotARadar"/>
|
||||||
|
</HBoxContainer>
|
||||||
|
</SS14Window>
|
||||||
|
|
||||||
@@ -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<SolarControlConsoleBoundInterfaceState>
|
||||||
|
{
|
||||||
|
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<IGameTiming>();
|
||||||
|
|
||||||
|
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<SolarControlWindow, SolarControlConsoleBoundInterfaceState>
|
||||||
|
{
|
||||||
|
public SolarControlConsoleBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Override this to handle messages from the UI before filtering them.
|
||||||
|
/// Calling base is necessary if you want this class to have any meaning.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
public virtual void ComputerLostPower()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.*
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<BaseComputerUserInterfaceComponent, ActivateInWorldEvent>(HandleActivate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleActivate(EntityUid uid, BaseComputerUserInterfaceComponent component, ActivateInWorldEvent args)
|
||||||
|
{
|
||||||
|
component.ActivateThunk(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using Content.Server.Power.Components;
|
|
||||||
using Content.Server.Solar.EntitySystems;
|
|
||||||
using Content.Server.UserInterface;
|
|
||||||
using Content.Shared.Interaction;
|
|
||||||
using Content.Shared.Solar;
|
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.Server.GameObjects;
|
||||||
using Robust.Shared.GameObjects;
|
using Robust.Shared.GameObjects;
|
||||||
using Robust.Shared.IoC;
|
using Robust.Shared.IoC;
|
||||||
@@ -12,26 +11,20 @@ using Robust.Shared.ViewVariables;
|
|||||||
namespace Content.Server.Solar.Components
|
namespace Content.Server.Solar.Components
|
||||||
{
|
{
|
||||||
[RegisterComponent]
|
[RegisterComponent]
|
||||||
[ComponentReference(typeof(IActivate))]
|
[ComponentReference(typeof(BaseComputerUserInterfaceComponent))]
|
||||||
public class SolarControlConsoleComponent : SharedSolarControlConsoleComponent, IActivate
|
public class SolarControlConsoleComponent : BaseComputerUserInterfaceComponent
|
||||||
{
|
{
|
||||||
|
public override string Name => "SolarControlConsole";
|
||||||
|
|
||||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||||
|
|
||||||
private PowerSolarSystem _powerSolarSystem = 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()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
if (UserInterface != null)
|
|
||||||
{
|
|
||||||
UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
Owner.EnsureComponent<PowerReceiverComponent>();
|
|
||||||
_powerSolarSystem = _entitySystemManager.GetEntitySystem<PowerSolarSystem>();
|
_powerSolarSystem = _entitySystemManager.GetEntitySystem<PowerSolarSystem>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +33,7 @@ namespace Content.Server.Solar.Components
|
|||||||
UserInterface?.SetState(new SolarControlConsoleBoundInterfaceState(_powerSolarSystem.TargetPanelRotation, _powerSolarSystem.TargetPanelVelocity, _powerSolarSystem.TotalPanelPower, _powerSolarSystem.TowardsSun));
|
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)
|
switch (obj.Message)
|
||||||
{
|
{
|
||||||
@@ -56,22 +49,5 @@ namespace Content.Server.Solar.Components
|
|||||||
break;
|
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ namespace Content.Server.Solar.EntitySystems
|
|||||||
{
|
{
|
||||||
// Determine if the solar panel is occluded, and zero out coverage if so.
|
// 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.
|
// 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<SharedBroadPhaseSystem>().IntersectRay(entity.Transform.MapID, ray, SunOcclusionCheckDistance, entity);
|
var rayCastResults = EntitySystem.Get<SharedBroadPhaseSystem>().IntersectRay(entity.Transform.MapID, ray, SunOcclusionCheckDistance, entity);
|
||||||
if (rayCastResults.Any())
|
if (rayCastResults.Any())
|
||||||
coverage = 0;
|
coverage = 0;
|
||||||
|
|||||||
@@ -6,12 +6,6 @@ using Robust.Shared.Serialization;
|
|||||||
|
|
||||||
namespace Content.Shared.Solar
|
namespace Content.Shared.Solar
|
||||||
{
|
{
|
||||||
public class SharedSolarControlConsoleComponent : Component
|
|
||||||
{
|
|
||||||
public override string Name => "SolarControlConsole";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[Serializable, NetSerializable]
|
[Serializable, NetSerializable]
|
||||||
public class SolarControlConsoleBoundInterfaceState : BoundUserInterfaceState
|
public class SolarControlConsoleBoundInterfaceState : BoundUserInterfaceState
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
10
Resources/Locale/en-US/ui/solar-control.ftl
Normal file
10
Resources/Locale/en-US/ui/solar-control.ftl
Normal file
@@ -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.
|
||||||
|
|
||||||
Reference in New Issue
Block a user