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:
20kdc
2021-06-16 15:49:02 +01:00
committed by GitHub
parent 69969bbdc6
commit a2f5c953dc
11 changed files with 466 additions and 268 deletions

View File

@@ -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) {}
}
}

View File

@@ -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>

View File

@@ -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) {}
}
}

View File

@@ -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);
}
}
}
}