Station maps (#13027)

This commit is contained in:
metalgearsloth
2023-04-13 16:21:24 +10:00
committed by GitHub
parent fc94d5245e
commit be4e69b0c0
45 changed files with 1210 additions and 153 deletions

View File

@@ -7,15 +7,25 @@ namespace Content.Client.Medical.CrewMonitoring
{ {
public sealed class CrewMonitoringBoundUserInterface : BoundUserInterface public sealed class CrewMonitoringBoundUserInterface : BoundUserInterface
{ {
private readonly IEntityManager _entManager;
private CrewMonitoringWindow? _menu; private CrewMonitoringWindow? _menu;
public CrewMonitoringBoundUserInterface([NotNull] ClientUserInterfaceComponent owner, [NotNull] Enum uiKey) : base(owner, uiKey) public CrewMonitoringBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{ {
_entManager = IoCManager.Resolve<IEntityManager>();
} }
protected override void Open() protected override void Open()
{ {
_menu = new CrewMonitoringWindow(); EntityUid? gridUid = null;
if (_entManager.TryGetComponent<TransformComponent>(Owner.Owner, out var xform))
{
gridUid = xform.GridUid;
}
_menu = new CrewMonitoringWindow(gridUid);
_menu.OpenCentered(); _menu.OpenCentered();
_menu.OnClose += Close; _menu.OnClose += Close;
} }
@@ -27,7 +37,15 @@ namespace Content.Client.Medical.CrewMonitoring
switch (state) switch (state)
{ {
case CrewMonitoringState st: case CrewMonitoringState st:
_menu?.ShowSensors(st.Sensors, st.WorldPosition, st.Snap, st.Precision); _entManager.TryGetComponent<TransformComponent>(Owner.Owner, out var xform);
Vector2 localPosition = Vector2.Zero;
if (_entManager.TryGetComponent<TransformComponent>(xform?.GridUid, out var gridXform))
{
localPosition = gridXform.InvWorldMatrix.Transform(xform.WorldPosition);
}
_menu?.ShowSensors(st.Sensors, localPosition, st.Snap, st.Precision);
break; break;
} }
} }

View File

@@ -1,16 +1,33 @@
<controls:FancyWindow xmlns="https://spacestation14.io" <controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.Pinpointer.UI"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'crew-monitoring-user-interface-title'}" Title="{Loc 'crew-monitoring-user-interface-title'}"
SetSize="775 400"> SetSize="1130 700"
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" Margin="5"> MinSize="1130 700">
<GridContainer Name="SensorsTable" HorizontalExpand="True" VerticalExpand="True" HSeparationOverride="5" VSeparationOverride="20" Columns="4"> <BoxContainer Orientation="Horizontal">
<!-- Category Headers --> <ScrollContainer HorizontalExpand="True"
<Label Text="{Loc 'crew-monitoring-user-interface-name'}" StyleClasses="LabelHeading"/> VerticalExpand="True"
<Label Text="{Loc 'crew-monitoring-user-interface-job'}" StyleClasses="LabelHeading"/> Margin="0, 0, 8, 0">
<Label Text="{Loc 'crew-monitoring-user-interface-status'}" StyleClasses="LabelHeading"/> <GridContainer Name="SensorsTable"
<Label Text="{Loc 'crew-monitoring-user-interface-location'}" StyleClasses="LabelHeading"/> HorizontalExpand="True"
VerticalExpand="True"
HSeparationOverride="5"
VSeparationOverride="20"
Columns="4">
<!-- Table header -->
<Label Text="{Loc 'crew-monitoring-user-interface-name'}"
StyleClasses="LabelHeading"/>
<Label Text="{Loc 'crew-monitoring-user-interface-job'}"
StyleClasses="LabelHeading"/>
<Label Text="{Loc 'crew-monitoring-user-interface-status'}"
StyleClasses="LabelHeading"/>
<Label Text="{Loc 'crew-monitoring-user-interface-location'}"
StyleClasses="LabelHeading"/>
<!-- Additional table rows are filled by code --> <!-- Table rows are filled by code -->
</GridContainer> </GridContainer>
</ScrollContainer> </ScrollContainer>
<ui:NavMapControl Name="NavMap"
Margin="5 5"/>
</BoxContainer>
</controls:FancyWindow> </controls:FancyWindow>

View File

@@ -1,11 +1,11 @@
using System.Linq; using System.Linq;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Controls;
using Content.Shared.Medical.SuitSensor; using Content.Shared.Medical.SuitSensor;
using Robust.Client.AutoGenerated; using Robust.Client.AutoGenerated;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML; using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -18,19 +18,32 @@ namespace Content.Client.Medical.CrewMonitoring
{ {
private List<Control> _rowsContent = new(); private List<Control> _rowsContent = new();
private List<(DirectionIcon Icon, Vector2 Position)> _directionIcons = new(); private List<(DirectionIcon Icon, Vector2 Position)> _directionIcons = new();
private readonly IEntityManager _entManager;
private readonly IEyeManager _eye; private readonly IEyeManager _eye;
private readonly IEntityManager _entityManager; private EntityUid? _stationUid;
public static int IconSize = 16; // XAML has a `VSeparationOverride` of 20 for each row. public static int IconSize = 16; // XAML has a `VSeparationOverride` of 20 for each row.
public CrewMonitoringWindow() public CrewMonitoringWindow(EntityUid? mapUid)
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
_eye = IoCManager.Resolve<IEyeManager>(); _eye = IoCManager.Resolve<IEyeManager>();
_entityManager = IoCManager.Resolve<IEntityManager>(); _entManager = IoCManager.Resolve<IEntityManager>();
_stationUid = mapUid;
if (_entManager.TryGetComponent<TransformComponent>(mapUid, out var xform))
{
NavMap.MapUid = xform.GridUid;
}
else
{
NavMap.Visible = false;
SetSize = new Vector2(775, 400);
MinSize = SetSize;
}
} }
public void ShowSensors(List<SuitSensorStatus> stSensors, Vector2 worldPosition, bool snap, float precision) public void ShowSensors(List<SuitSensorStatus> stSensors, Vector2 localPosition, bool snap, float precision)
{ {
ClearAllSensors(); ClearAllSensors();
@@ -43,13 +56,21 @@ namespace Content.Client.Medical.CrewMonitoring
{ {
// add users name // add users name
// format: UserName // format: UserName
var nameLabel = new Label() var nameLabel = new PanelContainer()
{
PanelOverride = new StyleBoxFlat()
{
BackgroundColor = StyleNano.ButtonColorDisabled,
},
Children =
{
new Label()
{ {
Text = sensor.Name, Text = sensor.Name,
HorizontalExpand = true Margin = new Thickness(5f, 5f),
}
}
}; };
SensorsTable.AddChild(nameLabel);
_rowsContent.Add(nameLabel);
// add users job // add users job
// format: JobName // format: JobName
@@ -58,6 +79,9 @@ namespace Content.Client.Medical.CrewMonitoring
Text = sensor.Job, Text = sensor.Job,
HorizontalExpand = true HorizontalExpand = true
}; };
SensorsTable.AddChild(nameLabel);
_rowsContent.Add(nameLabel);
SensorsTable.AddChild(jobLabel); SensorsTable.AddChild(jobLabel);
_rowsContent.Add(jobLabel); _rowsContent.Add(jobLabel);
@@ -79,9 +103,36 @@ namespace Content.Client.Medical.CrewMonitoring
// add users positions // add users positions
// format: (x, y) // format: (x, y)
var box = GetPositionBox(sensor.Coordinates, worldPosition, snap, precision); var box = GetPositionBox(sensor.Coordinates, localPosition, snap, precision);
SensorsTable.AddChild(box); SensorsTable.AddChild(box);
_rowsContent.Add(box); _rowsContent.Add(box);
if (sensor.Coordinates != null && NavMap.Visible)
{
NavMap.TrackedCoordinates.Add(sensor.Coordinates.Value, (true, Color.FromHex("#B02E26")));
nameLabel.MouseFilter = MouseFilterMode.Stop;
// Hide all others upon mouseover.
nameLabel.OnMouseEntered += args =>
{
foreach (var (coord, value) in NavMap.TrackedCoordinates)
{
if (coord == sensor.Coordinates)
continue;
NavMap.TrackedCoordinates[coord] = (false, value.Color);
}
};
nameLabel.OnMouseExited += args =>
{
foreach (var (coord, value) in NavMap.TrackedCoordinates)
{
NavMap.TrackedCoordinates[coord] = (true, value.Color);
}
};
}
} }
} }
@@ -89,7 +140,7 @@ namespace Content.Client.Medical.CrewMonitoring
{ {
var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal }; var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal };
if (coordinates == null) if (coordinates == null || !_entManager.TryGetComponent<TransformComponent>(_stationUid, out var xform))
{ {
var dirIcon = new DirectionIcon() var dirIcon = new DirectionIcon()
{ {
@@ -101,17 +152,18 @@ namespace Content.Client.Medical.CrewMonitoring
} }
else else
{ {
// todo: add locations names (kitchen, bridge, etc) var position = coordinates.Value.ToMapPos(_entManager);
var pos = (Vector2i) coordinates.Value.Position; var local = xform.InvWorldMatrix.Transform(position);
var mapCoords = coordinates.Value.ToMap(_entityManager).Position;
var displayPos = local.Floored();
var dirIcon = new DirectionIcon(snap, precision) var dirIcon = new DirectionIcon(snap, precision)
{ {
SetSize = (IconSize, IconSize), SetSize = (IconSize, IconSize),
Margin = new(0, 0, 4, 0) Margin = new(0, 0, 4, 0)
}; };
box.AddChild(dirIcon); box.AddChild(dirIcon);
box.AddChild(new Label() { Text = pos.ToString() }); box.AddChild(new Label() { Text = displayPos.ToString() });
_directionIcons.Add((dirIcon, mapCoords - sensorPosition)); _directionIcons.Add((dirIcon, local - sensorPosition));
} }
return box; return box;
@@ -134,7 +186,9 @@ namespace Content.Client.Medical.CrewMonitoring
{ {
SensorsTable.RemoveChild(child); SensorsTable.RemoveChild(child);
} }
_rowsContent.Clear(); _rowsContent.Clear();
NavMap.TrackedCoordinates.Clear();
} }
} }
} }

View File

@@ -0,0 +1,96 @@
using Content.Shared.Pinpointer;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
namespace Content.Client.Pinpointer;
public sealed class NavMapSystem : SharedNavMapSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NavMapComponent, ComponentHandleState>(OnHandleState);
}
private void OnHandleState(EntityUid uid, NavMapComponent component, ref ComponentHandleState args)
{
if (args.Current is not NavMapComponentState state)
return;
component.Chunks.Clear();
foreach (var (origin, data) in state.TileData)
{
component.Chunks.Add(origin, new NavMapChunk(origin)
{
TileData = data,
});
}
}
}
public sealed class NavMapOverlay : Overlay
{
private readonly IEntityManager _entManager;
private readonly IMapManager _mapManager;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public NavMapOverlay(IEntityManager entManager, IMapManager mapManager)
{
_entManager = entManager;
_mapManager = mapManager;
}
protected override void Draw(in OverlayDrawArgs args)
{
var query = _entManager.GetEntityQuery<NavMapComponent>();
var xformQuery = _entManager.GetEntityQuery<TransformComponent>();
var scale = Matrix3.CreateScale(new Vector2(1f, 1f));
foreach (var grid in _mapManager.FindGridsIntersecting(args.MapId, args.WorldBounds))
{
if (!query.TryGetComponent(grid.Owner, out var navMap) || !xformQuery.TryGetComponent(grid.Owner, out var xform))
continue;
// TODO: Faster helper method
var (_, _, matrix, invMatrix) = xform.GetWorldPositionRotationMatrixWithInv();
var localAABB = invMatrix.TransformBox(args.WorldBounds);
Matrix3.Multiply(in scale, in matrix, out var matty);
args.WorldHandle.SetTransform(matty);
for (var x = Math.Floor(localAABB.Left); x <= Math.Ceiling(localAABB.Right); x += SharedNavMapSystem.ChunkSize * grid.TileSize)
{
for (var y = Math.Floor(localAABB.Bottom); y <= Math.Ceiling(localAABB.Top); y += SharedNavMapSystem.ChunkSize * grid.TileSize)
{
var floored = new Vector2i((int) x, (int) y);
var chunkOrigin = SharedMapSystem.GetChunkIndices(floored, SharedNavMapSystem.ChunkSize);
if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk))
continue;
// TODO: Okay maybe I should just use ushorts lmao...
for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
{
var value = (int) Math.Pow(2, i);
var mask = chunk.TileData & value;
if (mask == 0x0)
continue;
var tile = chunk.Origin * SharedNavMapSystem.ChunkSize + SharedNavMapSystem.GetTile(mask);
args.WorldHandle.DrawRect(new Box2(tile * grid.TileSize, (tile + 1) * grid.TileSize), Color.Aqua, false);
}
}
}
}
args.WorldHandle.SetTransform(Matrix3.Identity);
}
}

View File

@@ -5,7 +5,7 @@ using Robust.Shared.GameStates;
namespace Content.Client.Pinpointer namespace Content.Client.Pinpointer
{ {
public sealed class ClientPinpointerSystem : SharedPinpointerSystem public sealed class PinpointerSystem : SharedPinpointerSystem
{ {
[Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!;

View File

@@ -0,0 +1,333 @@
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Pinpointer;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Vector2 = Robust.Shared.Maths.Vector2;
namespace Content.Client.Pinpointer.UI;
/// <summary>
/// Displays the nav map data of the specified grid.
/// </summary>
public sealed class NavMapControl : MapGridControl
{
[Dependency] private readonly IEntityManager _entManager = default!;
public EntityUid? MapUid;
public Dictionary<EntityCoordinates, (bool Visible, Color Color)> TrackedCoordinates = new();
private Vector2 _offset;
private bool _draggin;
private bool _recentering = false;
private float _recenterMinimum = 0.05f;
// TODO: https://github.com/space-wizards/RobustToolbox/issues/3818
private readonly Label _zoom = new()
{
VerticalAlignment = VAlignment.Top,
Margin = new Thickness(8f, 8f),
};
private readonly Button _recenter = new()
{
Text = "Recentre",
VerticalAlignment = VAlignment.Top,
HorizontalAlignment = HAlignment.Right,
Margin = new Thickness(8f, 4f),
Disabled = true,
};
public NavMapControl() : base(8f, 128f, 48f)
{
IoCManager.InjectDependencies(this);
RectClipContent = true;
HorizontalExpand = true;
VerticalExpand = true;
var topPanel = new PanelContainer()
{
PanelOverride = new StyleBoxFlat()
{
BackgroundColor = StyleNano.ButtonColorContext.WithAlpha(1f),
BorderColor = StyleNano.PanelDark
},
VerticalExpand = false,
Children =
{
_zoom,
_recenter,
}
};
var topContainer = new BoxContainer()
{
Orientation = BoxContainer.LayoutOrientation.Vertical,
Children =
{
topPanel,
new Control()
{
Name = "DrawingControl",
VerticalExpand = true,
Margin = new Thickness(5f, 5f)
}
}
};
AddChild(topContainer);
topPanel.Measure(Vector2.Infinity);
_recenter.OnPressed += args =>
{
_recentering = true;
};
}
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
{
base.KeyBindDown(args);
if (args.Function == EngineKeyFunctions.Use)
{
_draggin = true;
}
}
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{
base.KeyBindUp(args);
if (args.Function == EngineKeyFunctions.Use)
{
_draggin = false;
}
}
protected override void MouseMove(GUIMouseMoveEventArgs args)
{
base.MouseMove(args);
if (!_draggin)
return;
_recentering = false;
_offset -= new Vector2(args.Relative.X, -args.Relative.Y) / MidPoint * WorldRange;
if (_offset != Vector2.Zero)
{
_recenter.Disabled = false;
}
else
{
_recenter.Disabled = true;
}
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (_recentering)
{
var frameTime = Timing.FrameTime;
var diff = _offset * (float) frameTime.TotalSeconds;
if (_offset.LengthSquared < _recenterMinimum)
{
_offset = Vector2.Zero;
_recentering = false;
_recenter.Disabled = true;
}
else
{
_offset -= diff * 5f;
}
}
_zoom.Text = $"Zoom: {(WorldRange / WorldMaxRange * 100f):0.00}%";
if (!_entManager.TryGetComponent<NavMapComponent>(MapUid, out var navMap) ||
!_entManager.TryGetComponent<TransformComponent>(MapUid, out var xform) ||
!_entManager.TryGetComponent<MapGridComponent>(MapUid, out var grid))
{
return;
}
var offset = _offset;
var tileColor = new Color(30, 67, 30);
var lineColor = new Color(102, 217, 102);
if (_entManager.TryGetComponent<PhysicsComponent>(MapUid, out var physics))
{
offset += physics.LocalCenter;
}
// Draw tiles
if (_entManager.TryGetComponent<FixturesComponent>(MapUid, out var manager))
{
Span<Vector2> verts = new Vector2[8];
foreach (var fixture in manager.Fixtures.Values)
{
if (fixture.Shape is not PolygonShape poly)
continue;
for (var i = 0; i < poly.VertexCount; i++)
{
var vert = poly.Vertices[i] - offset;
verts[i] = Scale(new Vector2(vert.X, -vert.Y));
}
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleFan, verts[..poly.VertexCount], tileColor);
}
}
// Draw the wall data
var area = new Box2(-WorldRange, -WorldRange, WorldRange + 1f, WorldRange + 1f).Translated(offset);
var tileSize = new Vector2(grid.TileSize, -grid.TileSize);
for (var x = Math.Floor(area.Left); x <= Math.Ceiling(area.Right); x += SharedNavMapSystem.ChunkSize * grid.TileSize)
{
for (var y = Math.Floor(area.Bottom); y <= Math.Ceiling(area.Top); y += SharedNavMapSystem.ChunkSize * grid.TileSize)
{
var floored = new Vector2i((int) x, (int) y);
var chunkOrigin = SharedMapSystem.GetChunkIndices(floored, SharedNavMapSystem.ChunkSize);
if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk))
continue;
// TODO: Okay maybe I should just use ushorts lmao...
for (var i = 0; i < SharedNavMapSystem.ChunkSize * SharedNavMapSystem.ChunkSize; i++)
{
var value = (int) Math.Pow(2, i);
var mask = chunk.TileData & value;
if (mask == 0x0)
continue;
// Alright now we'll work out our edges
var relativeTile = SharedNavMapSystem.GetTile(mask);
var tile = (chunk.Origin * SharedNavMapSystem.ChunkSize + relativeTile) * grid.TileSize - offset;
var position = new Vector2(tile.X, -tile.Y);
NavMapChunk? neighborChunk;
bool neighbor;
// North edge
if (relativeTile.Y == SharedNavMapSystem.ChunkSize - 1)
{
neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(0, 1), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, 0))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, 1));
neighbor = (chunk.TileData & flag) != 0x0;
}
if (!neighbor)
{
handle.DrawLine(Scale(position + new Vector2(0f, -grid.TileSize)), Scale(position + tileSize), lineColor);
}
// East edge
if (relativeTile.X == SharedNavMapSystem.ChunkSize - 1)
{
neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(1, 0), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(0, relativeTile.Y))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(1, 0));
neighbor = (chunk.TileData & flag) != 0x0;
}
if (!neighbor)
{
handle.DrawLine(Scale(position + tileSize), Scale(position + new Vector2(grid.TileSize, 0f)), lineColor);
}
// South edge
if (relativeTile.Y == 0)
{
neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(0, -1), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(relativeTile.X, SharedNavMapSystem.ChunkSize - 1))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(0, -1));
neighbor = (chunk.TileData & flag) != 0x0;
}
if (!neighbor)
{
handle.DrawLine(Scale(position + new Vector2(grid.TileSize, 0f)), Scale(position), lineColor);
}
// West edge
if (relativeTile.X == 0)
{
neighbor = navMap.Chunks.TryGetValue(chunkOrigin + new Vector2i(-1, 0), out neighborChunk) &&
(neighborChunk.TileData &
SharedNavMapSystem.GetFlag(new Vector2i(SharedNavMapSystem.ChunkSize - 1, relativeTile.Y))) != 0x0;
}
else
{
var flag = SharedNavMapSystem.GetFlag(relativeTile + new Vector2i(-1, 0));
neighbor = (chunk.TileData & flag) != 0x0;
}
if (!neighbor)
{
handle.DrawLine(Scale(position), Scale(position + new Vector2(0f, -grid.TileSize)), lineColor);
}
// Draw a diagonal line for interiors.
handle.DrawLine(Scale(position + new Vector2(0f, -grid.TileSize)), Scale(position + new Vector2(grid.TileSize, 0f)), lineColor);
}
}
}
var curTime = Timing.RealTime;
var blinkFrequency = 1f / 1f;
var lit = curTime.TotalSeconds % blinkFrequency > blinkFrequency / 2f;
foreach (var (coord, value) in TrackedCoordinates)
{
if (lit && value.Visible)
{
var mapPos = coord.ToMap(_entManager);
if (mapPos.MapId != MapId.Nullspace)
{
var position = xform.InvWorldMatrix.Transform(mapPos.Position) - offset;
position = Scale(new Vector2(position.X, -position.Y));
handle.DrawCircle(position, MinimapScale / 2f, value.Color);
}
}
}
}
private Vector2 Scale(Vector2 position)
{
return position * MinimapScale + MidPoint;
}
}

View File

@@ -0,0 +1,35 @@
using Robust.Client.GameObjects;
using Robust.Client.Player;
namespace Content.Client.Pinpointer.UI;
public sealed class StationMapBoundUserInterface : BoundUserInterface
{
private StationMapWindow? _window;
public StationMapBoundUserInterface(ClientUserInterfaceComponent owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window?.Close();
EntityUid? gridUid = null;
if (IoCManager.Resolve<IEntityManager>().TryGetComponent<TransformComponent>(Owner.Owner, out var xform))
{
gridUid = xform.GridUid;
}
_window = new StationMapWindow(gridUid, Owner.Owner);
_window.OpenCentered();
_window.OnClose += Close;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_window?.Dispose();
}
}

View File

@@ -0,0 +1,7 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:ui="clr-namespace:Content.Client.Pinpointer.UI"
Title="{Loc 'station-map-window-title'}"
Resizable="False">
<ui:NavMapControl Name="NavMapScreen"/>
</controls:FancyWindow>

View File

@@ -0,0 +1,24 @@
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map;
namespace Content.Client.Pinpointer.UI;
[GenerateTypedNameReferences]
public sealed partial class StationMapWindow : FancyWindow
{
public StationMapWindow(EntityUid? mapUid, EntityUid? trackedEntity)
{
RobustXamlLoader.Load(this);
NavMapScreen.MapUid = mapUid;
if (trackedEntity != null)
NavMapScreen.TrackedCoordinates.Add(new EntityCoordinates(trackedEntity.Value, Vector2.Zero), (true, Color.Red));
if (IoCManager.Resolve<IEntityManager>().TryGetComponent<MetaDataComponent>(mapUid, out var metadata))
{
Title = metadata.EntityName;
}
}
}

View File

@@ -1,3 +1,4 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Shuttles.BUIStates; using Content.Shared.Shuttles.BUIStates;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
@@ -20,11 +21,9 @@ public class DockingControl : Control
private float _rangeSquared = 0f; private float _rangeSquared = 0f;
private const float GridLinesDistance = 32f; private const float GridLinesDistance = 32f;
private int MinimapRadius => (int) Math.Min(Size.X, Size.Y) / 2; private int MidPoint => SizeFull / 2;
private int SizeFull => (int) (MapGridControl.UIDisplayRadius * 2 * UIScale);
private Vector2 MidPoint => (Size / 2) * UIScale; private int ScaledMinimapRadius => (int) (MapGridControl.UIDisplayRadius * UIScale);
private int SizeFull => (int) (MinimapRadius * 2 * UIScale);
private int ScaledMinimapRadius => (int) (MinimapRadius * UIScale);
private float MinimapScale => _range != 0 ? ScaledMinimapRadius / _range : 0f; private float MinimapScale => _range != 0 ? ScaledMinimapRadius / _range : 0f;
public EntityUid? ViewedDock; public EntityUid? ViewedDock;
@@ -52,8 +51,8 @@ public class DockingControl : Control
var fakeAA = new Color(0.08f, 0.08f, 0.08f); var fakeAA = new Color(0.08f, 0.08f, 0.08f);
handle.DrawCircle((MidPoint.X, MidPoint.Y), ScaledMinimapRadius + 1, fakeAA); handle.DrawCircle((MidPoint, MidPoint), ScaledMinimapRadius + 1, fakeAA);
handle.DrawCircle((MidPoint.X, MidPoint.Y), ScaledMinimapRadius, Color.Black); handle.DrawCircle((MidPoint, MidPoint), ScaledMinimapRadius, Color.Black);
var gridLines = new Color(0.08f, 0.08f, 0.08f); var gridLines = new Color(0.08f, 0.08f, 0.08f);
var gridLinesRadial = 8; var gridLinesRadial = 8;
@@ -61,14 +60,14 @@ public class DockingControl : Control
for (var i = 1; i < gridLinesEquatorial + 1; i++) for (var i = 1; i < gridLinesEquatorial + 1; i++)
{ {
handle.DrawCircle((MidPoint.X, MidPoint.Y), GridLinesDistance * MinimapScale * i, gridLines, false); handle.DrawCircle((MidPoint, MidPoint), GridLinesDistance * MinimapScale * i, gridLines, false);
} }
for (var i = 0; i < gridLinesRadial; i++) for (var i = 0; i < gridLinesRadial; i++)
{ {
Angle angle = (Math.PI / gridLinesRadial) * i; Angle angle = (Math.PI / gridLinesRadial) * i;
var aExtent = angle.ToVec() * ScaledMinimapRadius; var aExtent = angle.ToVec() * ScaledMinimapRadius;
handle.DrawLine((MidPoint.X, MidPoint.Y) - aExtent, (MidPoint.X, MidPoint.Y) + aExtent, gridLines); handle.DrawLine((MidPoint, MidPoint) - aExtent, (MidPoint, MidPoint) + aExtent, gridLines);
} }
if (Coordinates == null || if (Coordinates == null ||

View File

@@ -1,3 +1,4 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Shuttles.BUIStates; using Content.Shared.Shuttles.BUIStates;
using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Components;
using JetBrains.Annotations; using JetBrains.Annotations;
@@ -13,7 +14,6 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Physics; using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Components;
using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Client.Shuttles.UI; namespace Content.Client.Shuttles.UI;
@@ -21,13 +21,11 @@ namespace Content.Client.Shuttles.UI;
/// <summary> /// <summary>
/// Displays nearby grids inside of a control. /// Displays nearby grids inside of a control.
/// </summary> /// </summary>
public sealed class RadarControl : Control public sealed class RadarControl : MapGridControl
{ {
[Dependency] private readonly IEntityManager _entManager = default!; [Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IMapManager _mapManager = default!;
private const float ScrollSensitivity = 8f;
private const float GridLinesDistance = 32f; private const float GridLinesDistance = 32f;
/// <summary> /// <summary>
@@ -37,26 +35,6 @@ public sealed class RadarControl : Control
private Angle? _rotation; private Angle? _rotation;
private float _radarMinRange = SharedRadarConsoleSystem.DefaultMinRange;
private float _radarMaxRange = SharedRadarConsoleSystem.DefaultMaxRange;
public float RadarRange { get; private set; } = SharedRadarConsoleSystem.DefaultMinRange;
/// <summary>
/// We'll lerp between the radarrange and actual range
/// </summary>
private float _actualRadarRange = SharedRadarConsoleSystem.DefaultMinRange;
/// <summary>
/// Controls the maximum distance that IFF labels will display.
/// </summary>
public float MaxRadarRange { get; private set; } = 256f * 10f;
private int MinimapRadius => (int) Math.Min(Size.X, Size.Y) / 2;
private Vector2 MidPoint => (Size / 2) * UIScale;
private int SizeFull => (int) (MinimapRadius * 2 * UIScale);
private int ScaledMinimapRadius => (int) (MinimapRadius * UIScale);
private float MinimapScale => RadarRange != 0 ? ScaledMinimapRadius / RadarRange : 0f;
/// <summary> /// <summary>
/// Shows a label on each radar object. /// Shows a label on each radar object.
/// </summary> /// </summary>
@@ -79,11 +57,9 @@ public sealed class RadarControl : Control
/// </summary> /// </summary>
public Action<EntityCoordinates>? OnRadarClick; public Action<EntityCoordinates>? OnRadarClick;
public RadarControl() public RadarControl() : base(64f, 256f, 256f)
{ {
IoCManager.InjectDependencies(this);
MinSize = (SizeFull, SizeFull);
RectClipContent = true;
} }
public void SetMatrix(EntityCoordinates? coordinates, Angle? angle) public void SetMatrix(EntityCoordinates? coordinates, Angle? angle)
@@ -92,25 +68,6 @@ public sealed class RadarControl : Control
_rotation = angle; _rotation = angle;
} }
public void UpdateState(RadarConsoleBoundInterfaceState ls)
{
_radarMaxRange = ls.MaxRange;
if (_radarMaxRange < _radarMinRange)
_radarMinRange = _radarMaxRange;
_actualRadarRange = Math.Clamp(_actualRadarRange, _radarMinRange, _radarMaxRange);
_docks.Clear();
foreach (var state in ls.Docks)
{
var coordinates = state.Coordinates;
var grid = _docks.GetOrNew(coordinates.EntityId);
grid.Add(state);
}
}
protected override void KeyBindUp(GUIBoundKeyEventArgs args) protected override void KeyBindUp(GUIBoundKeyEventArgs args)
{ {
base.KeyBindUp(args); base.KeyBindUp(args);
@@ -148,32 +105,38 @@ public sealed class RadarControl : Control
return coords; return coords;
} }
protected override void MouseWheel(GUIMouseWheelEventArgs args) public void UpdateState(RadarConsoleBoundInterfaceState ls)
{ {
base.MouseWheel(args); WorldMaxRange = ls.MaxRange;
AddRadarRange(-args.Delta.Y * 1f / ScrollSensitivity * RadarRange);
if (WorldMaxRange < WorldRange)
{
ActualRadarRange = WorldMaxRange;
} }
public void AddRadarRange(float value) if (WorldMaxRange < WorldMinRange)
WorldMinRange = WorldMaxRange;
ActualRadarRange = Math.Clamp(ActualRadarRange, WorldMinRange, WorldMaxRange);
_docks.Clear();
foreach (var state in ls.Docks)
{ {
_actualRadarRange = Math.Clamp(_actualRadarRange + value, _radarMinRange, _radarMaxRange); var coordinates = state.Coordinates;
var grid = _docks.GetOrNew(coordinates.EntityId);
grid.Add(state);
}
} }
protected override void Draw(DrawingHandleScreen handle) protected override void Draw(DrawingHandleScreen handle)
{ {
if (!_actualRadarRange.Equals(RadarRange)) base.Draw(handle);
{
var diff = _actualRadarRange - RadarRange;
var lerpRate = 10f;
RadarRange += (float) Math.Clamp(diff, -lerpRate * MathF.Abs(diff) * _timing.FrameTime.TotalSeconds, lerpRate * MathF.Abs(diff) * _timing.FrameTime.TotalSeconds);
OnRadarRangeChanged?.Invoke(RadarRange);
}
var fakeAA = new Color(0.08f, 0.08f, 0.08f); var fakeAA = new Color(0.08f, 0.08f, 0.08f);
handle.DrawCircle((MidPoint.X, MidPoint.Y), ScaledMinimapRadius + 1, fakeAA); handle.DrawCircle((MidPoint, MidPoint), ScaledMinimapRadius + 1, fakeAA);
handle.DrawCircle((MidPoint.X, MidPoint.Y), ScaledMinimapRadius, Color.Black); handle.DrawCircle((MidPoint, MidPoint), ScaledMinimapRadius, Color.Black);
// No data // No data
if (_coordinates == null || _rotation == null) if (_coordinates == null || _rotation == null)
@@ -184,18 +147,18 @@ public sealed class RadarControl : Control
var gridLines = new Color(0.08f, 0.08f, 0.08f); var gridLines = new Color(0.08f, 0.08f, 0.08f);
var gridLinesRadial = 8; var gridLinesRadial = 8;
var gridLinesEquatorial = (int) Math.Floor(RadarRange / GridLinesDistance); var gridLinesEquatorial = (int) Math.Floor(WorldRange / GridLinesDistance);
for (var i = 1; i < gridLinesEquatorial + 1; i++) for (var i = 1; i < gridLinesEquatorial + 1; i++)
{ {
handle.DrawCircle((MidPoint.X, MidPoint.Y), GridLinesDistance * MinimapScale * i, gridLines, false); handle.DrawCircle((MidPoint, MidPoint), GridLinesDistance * MinimapScale * i, gridLines, false);
} }
for (var i = 0; i < gridLinesRadial; i++) for (var i = 0; i < gridLinesRadial; i++)
{ {
Angle angle = (Math.PI / gridLinesRadial) * i; Angle angle = (Math.PI / gridLinesRadial) * i;
var aExtent = angle.ToVec() * ScaledMinimapRadius; var aExtent = angle.ToVec() * ScaledMinimapRadius;
handle.DrawLine((MidPoint.X, MidPoint.Y) - aExtent, (MidPoint.X, MidPoint.Y) + aExtent, gridLines); handle.DrawLine((MidPoint, MidPoint) - aExtent, (MidPoint, MidPoint) + aExtent, gridLines);
} }
var metaQuery = _entManager.GetEntityQuery<MetaDataComponent>(); var metaQuery = _entManager.GetEntityQuery<MetaDataComponent>();
@@ -368,7 +331,7 @@ public sealed class RadarControl : Control
var position = state.Coordinates.Position; var position = state.Coordinates.Position;
var uiPosition = matrix.Transform(position); var uiPosition = matrix.Transform(position);
if (uiPosition.Length > RadarRange - DockScale) continue; if (uiPosition.Length > WorldRange - DockScale) continue;
var color = HighlightedDock == ent ? state.HighlightedColor : state.Color; var color = HighlightedDock == ent ? state.HighlightedColor : state.Color;
@@ -446,7 +409,7 @@ public sealed class RadarControl : Control
var adjustedStart = matrix.Transform(start); var adjustedStart = matrix.Transform(start);
var adjustedEnd = matrix.Transform(end); var adjustedEnd = matrix.Transform(end);
if (adjustedStart.Length > RadarRange || adjustedEnd.Length > RadarRange) if (adjustedStart.Length > ActualRadarRange || adjustedEnd.Length > ActualRadarRange)
continue; continue;
start = ScalePosition(new Vector2(adjustedStart.X, -adjustedStart.Y)); start = ScalePosition(new Vector2(adjustedStart.X, -adjustedStart.Y));

View File

@@ -52,8 +52,8 @@ public sealed partial class ShuttleConsoleWindow : FancyWindow,
_entManager = IoCManager.Resolve<IEntityManager>(); _entManager = IoCManager.Resolve<IEntityManager>();
_timing = IoCManager.Resolve<IGameTiming>(); _timing = IoCManager.Resolve<IGameTiming>();
OnRadarRangeChange(RadarScreen.RadarRange); WorldRangeChange(RadarScreen.WorldRange);
RadarScreen.OnRadarRangeChanged += OnRadarRangeChange; RadarScreen.WorldRangeChanged += WorldRangeChange;
IFFToggle.OnToggled += OnIFFTogglePressed; IFFToggle.OnToggled += OnIFFTogglePressed;
IFFToggle.Pressed = RadarScreen.ShowIFF; IFFToggle.Pressed = RadarScreen.ShowIFF;
@@ -64,7 +64,7 @@ public sealed partial class ShuttleConsoleWindow : FancyWindow,
UndockButton.OnPressed += OnUndockPressed; UndockButton.OnPressed += OnUndockPressed;
} }
private void OnRadarRangeChange(float value) private void WorldRangeChange(float value)
{ {
RadarRange.Text = $"{value:0}"; RadarRange.Text = $"{value:0}";
} }

View File

@@ -81,6 +81,8 @@ namespace Content.Client.Stylesheets
public const string StyleClassPopupMessageLarge = "PopupMessageLarge"; public const string StyleClassPopupMessageLarge = "PopupMessageLarge";
public const string StyleClassPopupMessageLargeCaution = "PopupMessageLargeCaution"; public const string StyleClassPopupMessageLargeCaution = "PopupMessageLargeCaution";
public static readonly Color PanelDark = Color.FromHex("#1E1E22");
public static readonly Color NanoGold = Color.FromHex("#A88B5E"); public static readonly Color NanoGold = Color.FromHex("#A88B5E");
public static readonly Color GoodGreenFore = Color.FromHex("#31843E"); public static readonly Color GoodGreenFore = Color.FromHex("#31843E");
public static readonly Color ConcerningOrangeFore = Color.FromHex("#A5762F"); public static readonly Color ConcerningOrangeFore = Color.FromHex("#A5762F");
@@ -453,7 +455,7 @@ namespace Content.Client.Stylesheets
var sliderBackBox = new StyleBoxTexture var sliderBackBox = new StyleBoxTexture
{ {
Texture = sliderFillTex, Texture = sliderFillTex,
Modulate = Color.FromHex("#1E1E22") Modulate = PanelDark,
}; };
var sliderForeBox = new StyleBoxTexture var sliderForeBox = new StyleBoxTexture

View File

@@ -0,0 +1,79 @@
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Shared.Timing;
namespace Content.Client.UserInterface.Controls;
/// <summary>
/// Handles generic grid-drawing data, with zoom and dragging.
/// </summary>
public abstract class MapGridControl : Control
{
[Dependency] protected readonly IGameTiming Timing = default!;
protected const float ScrollSensitivity = 8f;
/// <summary>
/// UI pixel radius.
/// </summary>
public const int UIDisplayRadius = 320;
protected const int MinimapMargin = 4;
protected float WorldMinRange;
protected float WorldMaxRange;
public float WorldRange;
/// <summary>
/// We'll lerp between the radarrange and actual range
/// </summary>
protected float ActualRadarRange;
/// <summary>
/// Controls the maximum distance that will display.
/// </summary>
public float MaxRadarRange { get; private set; } = 256f * 10f;
protected int MidPoint => SizeFull / 2;
protected int SizeFull => (int) ((UIDisplayRadius + MinimapMargin) * 2 * UIScale);
protected int ScaledMinimapRadius => (int) (UIDisplayRadius * UIScale);
protected float MinimapScale => WorldRange != 0 ? ScaledMinimapRadius / WorldRange : 0f;
public event Action<float>? WorldRangeChanged;
public MapGridControl(float minRange, float maxRange, float range)
{
IoCManager.InjectDependencies(this);
SetSize = (SizeFull, SizeFull);
RectClipContent = true;
MouseFilter = MouseFilterMode.Stop;
ActualRadarRange = WorldRange;
WorldMinRange = minRange;
WorldMaxRange = maxRange;
WorldRange = range;
ActualRadarRange = range;
}
protected override void MouseWheel(GUIMouseWheelEventArgs args)
{
base.MouseWheel(args);
AddRadarRange(-args.Delta.Y * 1f / ScrollSensitivity * ActualRadarRange);
}
public void AddRadarRange(float value)
{
ActualRadarRange = Math.Clamp(ActualRadarRange + value, WorldMinRange, WorldMaxRange);
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
if (!ActualRadarRange.Equals(WorldRange))
{
var diff = ActualRadarRange - WorldRange;
const float lerpRate = 10f;
WorldRange += (float) Math.Clamp(diff, -lerpRate * MathF.Abs(diff) * Timing.FrameTime.TotalSeconds, lerpRate * MathF.Abs(diff) * Timing.FrameTime.TotalSeconds);
WorldRangeChanged?.Invoke(WorldRange);
}
}
}

View File

@@ -60,9 +60,8 @@ namespace Content.Server.Medical.CrewMonitoring
return; return;
// update all sensors info // update all sensors info
var xform = Transform(uid);
var allSensors = component.ConnectedSensors.Values.ToList(); var allSensors = component.ConnectedSensors.Values.ToList();
var uiState = new CrewMonitoringState(allSensors, xform.WorldPosition, component.Snap, component.Precision); var uiState = new CrewMonitoringState(allSensors, component.Snap, component.Precision);
ui.SetState(uiState); ui.SetState(uiState);
} }
} }

View File

@@ -1,4 +1,5 @@
using Content.Shared.Medical.SuitSensor; using Content.Shared.Medical.SuitSensor;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Medical.SuitSensors namespace Content.Server.Medical.SuitSensors
{ {
@@ -53,9 +54,10 @@ namespace Content.Server.Medical.SuitSensors
public EntityUid? User = null; public EntityUid? User = null;
/// <summary> /// <summary>
/// Last time when sensor updated owners status /// Next time when sensor updated owners status
/// </summary> /// </summary>
public TimeSpan LastUpdate = TimeSpan.Zero; [DataField("nextUpdate", customTypeSerializer:typeof(TimeOffsetSerializer))]
public TimeSpan NextUpdate = TimeSpan.Zero;
/// <summary> /// <summary>
/// The station this suit sensor belongs to. If it's null the suit didn't spawn on a station and the sensor doesn't work. /// The station this suit sensor belongs to. If it's null the suit didn't spawn on a station and the sensor doesn't work.

View File

@@ -21,23 +21,21 @@ namespace Content.Server.Medical.SuitSensors
{ {
public sealed class SuitSensorSystem : EntitySystem public sealed class SuitSensorSystem : EntitySystem
{ {
[Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly CrewMonitoringServerSystem _monitoringServerSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly IdCardSystem _idCardSystem = default!; [Dependency] private readonly IdCardSystem _idCardSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly CrewMonitoringServerSystem _monitoringServerSystem = default!;
[Dependency] private readonly StationSystem _stationSystem = default!; [Dependency] private readonly StationSystem _stationSystem = default!;
[Dependency] private readonly SharedTransformSystem _xform = default!;
private const float UpdateRate = 1f;
private float _updateDif;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
SubscribeLocalEvent<SuitSensorComponent, MapInitEvent>(OnMapInit); SubscribeLocalEvent<SuitSensorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SuitSensorComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<SuitSensorComponent, GotEquippedEvent>(OnEquipped); SubscribeLocalEvent<SuitSensorComponent, GotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<SuitSensorComponent, GotUnequippedEvent>(OnUnequipped); SubscribeLocalEvent<SuitSensorComponent, GotUnequippedEvent>(OnUnequipped);
SubscribeLocalEvent<SuitSensorComponent, ExaminedEvent>(OnExamine); SubscribeLocalEvent<SuitSensorComponent, ExaminedEvent>(OnExamine);
@@ -46,30 +44,29 @@ namespace Content.Server.Medical.SuitSensors
SubscribeLocalEvent<SuitSensorComponent, EntGotRemovedFromContainerMessage>(OnRemove); SubscribeLocalEvent<SuitSensorComponent, EntGotRemovedFromContainerMessage>(OnRemove);
} }
private void OnUnpaused(EntityUid uid, SuitSensorComponent component, ref EntityUnpausedEvent args)
{
component.NextUpdate += args.PausedTime;
}
public override void Update(float frameTime) public override void Update(float frameTime)
{ {
base.Update(frameTime); base.Update(frameTime);
// check update rate
_updateDif += frameTime;
if (_updateDif < UpdateRate)
return;
_updateDif -= UpdateRate;
var curTime = _gameTiming.CurTime; var curTime = _gameTiming.CurTime;
var sensors = EntityManager.EntityQuery<SuitSensorComponent, DeviceNetworkComponent>(); var sensors = EntityManager.EntityQueryEnumerator<SuitSensorComponent, DeviceNetworkComponent>();
foreach (var (sensor, device) in sensors)
while (sensors.MoveNext(out var sensor, out var device))
{ {
if (!device.TransmitFrequency.HasValue || !sensor.StationId.HasValue) if (device.TransmitFrequency is null || !sensor.StationId.HasValue)
continue; continue;
// check if sensor is ready to update // check if sensor is ready to update
if (curTime - sensor.LastUpdate < sensor.UpdateRate) if (curTime < sensor.NextUpdate)
continue; continue;
// Add a random offset to the next update time that isn't longer than the sensors update rate // TODO: This would cause imprecision at different tick rates.
sensor.LastUpdate = curTime.Add(TimeSpan.FromSeconds(_random.Next(0, sensor.UpdateRate.Seconds))); sensor.NextUpdate = curTime + sensor.UpdateRate;
// get sensor status // get sensor status
var status = GetSensorState(sensor.Owner, sensor); var status = GetSensorState(sensor.Owner, sensor);
@@ -278,9 +275,6 @@ namespace Content.Server.Medical.SuitSensors
totalDamage = damageable.TotalDamage.Int(); totalDamage = damageable.TotalDamage.Int();
// finally, form suit sensor status // finally, form suit sensor status
var xForm = Transform(sensor.User.Value);
var xFormQuery = GetEntityQuery<TransformComponent>();
var coords = _xform.GetMoverCoordinates(xForm, xFormQuery);
var status = new SuitSensorStatus(userName, userJob); var status = new SuitSensorStatus(userName, userJob);
switch (sensor.Mode) switch (sensor.Mode)
{ {
@@ -294,7 +288,26 @@ namespace Content.Server.Medical.SuitSensors
case SuitSensorMode.SensorCords: case SuitSensorMode.SensorCords:
status.IsAlive = isAlive; status.IsAlive = isAlive;
status.TotalDamage = totalDamage; status.TotalDamage = totalDamage;
status.Coordinates = coords; EntityCoordinates coordinates;
var xformQuery = GetEntityQuery<TransformComponent>();
if (transform.GridUid != null)
{
coordinates = new EntityCoordinates(transform.GridUid.Value,
_transform.GetInvWorldMatrix(xformQuery.GetComponent(transform.GridUid.Value), xformQuery)
.Transform(_transform.GetWorldPosition(transform, xformQuery)));
}
else if (transform.MapUid != null)
{
coordinates = new EntityCoordinates(transform.MapUid.Value,
_transform.GetWorldPosition(transform, xformQuery));
}
else
{
coordinates = EntityCoordinates.Invalid;
}
status.Coordinates = coordinates;
break; break;
} }
@@ -317,7 +330,7 @@ namespace Content.Server.Medical.SuitSensors
if (status.TotalDamage != null) if (status.TotalDamage != null)
payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE, status.TotalDamage); payload.Add(SuitSensorConstants.NET_TOTAL_DAMAGE, status.TotalDamage);
if (status.Coordinates != null) if (status.Coordinates != null)
payload.Add(SuitSensorConstants.NET_CORDINATES, status.Coordinates); payload.Add(SuitSensorConstants.NET_COORDINATES, status.Coordinates);
return payload; return payload;
@@ -341,7 +354,7 @@ namespace Content.Server.Medical.SuitSensors
// try get total damage and cords (optionals) // try get total damage and cords (optionals)
payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE, out int? totalDamage); payload.TryGetValue(SuitSensorConstants.NET_TOTAL_DAMAGE, out int? totalDamage);
payload.TryGetValue(SuitSensorConstants.NET_CORDINATES, out EntityCoordinates? cords); payload.TryGetValue(SuitSensorConstants.NET_COORDINATES, out EntityCoordinates? cords);
var status = new SuitSensorStatus(name, job) var status = new SuitSensorStatus(name, job)
{ {

View File

@@ -0,0 +1,158 @@
using Content.Shared.Pinpointer;
using Content.Shared.Tag;
using Robust.Shared.GameStates;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
namespace Content.Server.Pinpointer;
/// <summary>
/// Handles data to be used for in-grid map displays.
/// </summary>
public sealed class NavMapSystem : SharedNavMapSystem
{
[Dependency] private readonly TagSystem _tags = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AnchorStateChangedEvent>(OnAnchorChange);
SubscribeLocalEvent<ReAnchorEvent>(OnReAnchor);
SubscribeLocalEvent<NavMapComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<NavMapComponent, GridSplitEvent>(OnNavMapSplit);
}
private void OnNavMapSplit(EntityUid uid, NavMapComponent component, ref GridSplitEvent args)
{
var physicsQuery = GetEntityQuery<PhysicsComponent>();
var tagQuery = GetEntityQuery<TagComponent>();
var gridQuery = GetEntityQuery<MapGridComponent>();
foreach (var grid in args.NewGrids)
{
var newComp = EnsureComp<NavMapComponent>(grid);
RefreshGrid(newComp, gridQuery.GetComponent(grid), physicsQuery, tagQuery);
}
RefreshGrid(component, gridQuery.GetComponent(uid), physicsQuery, tagQuery);
}
private void RefreshGrid(NavMapComponent component, MapGridComponent grid, EntityQuery<PhysicsComponent> physicsQuery, EntityQuery<TagComponent> tagQuery)
{
component.Chunks.Clear();
var tiles = grid.GetAllTilesEnumerator();
while (tiles.MoveNext(out var tile))
{
var chunkOrigin = SharedMapSystem.GetChunkIndices(tile.Value.GridIndices, ChunkSize);
if (!component.Chunks.TryGetValue(chunkOrigin, out var chunk))
{
chunk = new NavMapChunk(chunkOrigin);
}
RefreshTile(grid, component, chunk, tile.Value.GridIndices, physicsQuery, tagQuery);
}
}
private void OnGetState(EntityUid uid, NavMapComponent component, ref ComponentGetState args)
{
var data = new Dictionary<Vector2i, int>(component.Chunks.Count);
foreach (var (index, chunk) in component.Chunks)
{
data.Add(index, chunk.TileData);
}
// TODO: Diffs
args.State = new NavMapComponentState()
{
TileData = data,
};
}
private void OnReAnchor(ref ReAnchorEvent ev)
{
if (TryComp<MapGridComponent>(ev.OldGrid, out var oldGrid) &&
TryComp<NavMapComponent>(ev.OldGrid, out var navMap))
{
var chunkOrigin = SharedMapSystem.GetChunkIndices(ev.TilePos, ChunkSize);
if (navMap.Chunks.TryGetValue(chunkOrigin, out var chunk))
{
var physicsQuery = GetEntityQuery<PhysicsComponent>();
var tagQuery = GetEntityQuery<TagComponent>();
RefreshTile(oldGrid, navMap, chunk, ev.TilePos, physicsQuery, tagQuery);
}
}
HandleAnchor(ev.Xform);
}
private void OnAnchorChange(ref AnchorStateChangedEvent ev)
{
HandleAnchor(ev.Transform);
}
private void HandleAnchor(TransformComponent xform)
{
if (!TryComp<NavMapComponent>(xform.GridUid, out var navMap) ||
!TryComp<MapGridComponent>(xform.GridUid, out var grid))
return;
var tile = grid.LocalToTile(xform.Coordinates);
var chunkOrigin = SharedMapSystem.GetChunkIndices(tile, ChunkSize);
var physicsQuery = GetEntityQuery<PhysicsComponent>();
var tagQuery = GetEntityQuery<TagComponent>();
if (!navMap.Chunks.TryGetValue(chunkOrigin, out var chunk))
{
chunk = new NavMapChunk(chunkOrigin);
navMap.Chunks[chunkOrigin] = chunk;
}
RefreshTile(grid, navMap, chunk, tile, physicsQuery, tagQuery);
}
private void RefreshTile(MapGridComponent grid, NavMapComponent component, NavMapChunk chunk, Vector2i tile,
EntityQuery<PhysicsComponent> physicsQuery,
EntityQuery<TagComponent> tagQuery)
{
var relative = SharedMapSystem.GetChunkRelative(tile, ChunkSize);
var existing = chunk.TileData;
var flag = GetFlag(relative);
chunk.TileData &= ~flag;
var enumerator = grid.GetAnchoredEntitiesEnumerator(tile);
// TODO: Use something to get convex poly.
while (enumerator.MoveNext(out var ent))
{
if (!physicsQuery.TryGetComponent(ent, out var body) ||
!body.CanCollide ||
!body.Hard ||
body.BodyType != BodyType.Static ||
(!_tags.HasTag(ent.Value, "Wall", tagQuery) &&
!_tags.HasTag(ent.Value, "Window", tagQuery)))
{
continue;
}
chunk.TileData |= flag;
break;
}
if (chunk.TileData == 0)
{
component.Chunks.Remove(chunk.Origin);
}
if (existing == chunk.TileData)
return;
Dirty(component);
}
}

View File

@@ -6,7 +6,7 @@ using Content.Server.Shuttles.Events;
namespace Content.Server.Pinpointer namespace Content.Server.Pinpointer
{ {
public sealed class ServerPinpointerSystem : SharedPinpointerSystem public sealed class PinpointerSystem : SharedPinpointerSystem
{ {
[Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly SharedTransformSystem _transform = default!;

View File

@@ -0,0 +1,17 @@
namespace Content.Server.Pinpointer;
[RegisterComponent]
public sealed class StationMapComponent : Component
{
}
/// <summary>
/// Added to an entity using station map so when its parent changes we reset it.
/// </summary>
[RegisterComponent]
public sealed class StationMapUserComponent : Component
{
[DataField("mapUid")]
public EntityUid Map;
}

View File

@@ -0,0 +1,43 @@
using Content.Shared.Interaction.Events;
using Content.Shared.Pinpointer;
using Robust.Server.GameObjects;
namespace Content.Server.Pinpointer;
public sealed class StationMapSystem : EntitySystem
{
[Dependency] private readonly UserInterfaceSystem _ui = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StationMapUserComponent, EntParentChangedMessage>(OnUserParentChanged);
SubscribeLocalEvent<StationMapComponent, BoundUIOpenedEvent>(OnStationMapOpened);
SubscribeLocalEvent<StationMapComponent, BoundUIClosedEvent>(OnStationMapClosed);
}
private void OnStationMapClosed(EntityUid uid, StationMapComponent component, BoundUIClosedEvent args)
{
if (!Equals(args.UiKey, StationMapUiKey.Key) || args.Session.AttachedEntity == null)
return;
RemCompDeferred<StationMapUserComponent>(args.Session.AttachedEntity.Value);
}
private void OnUserParentChanged(EntityUid uid, StationMapUserComponent component, ref EntParentChangedMessage args)
{
if (TryComp<ActorComponent>(uid, out var actor))
{
_ui.TryClose(component.Map, StationMapUiKey.Key, actor.PlayerSession);
}
}
private void OnStationMapOpened(EntityUid uid, StationMapComponent component, BoundUIOpenedEvent args)
{
if (args.Session.AttachedEntity == null)
return;
var comp = EnsureComp<StationMapUserComponent>(args.Session.AttachedEntity.Value);
comp.Map = uid;
}
}

View File

@@ -13,14 +13,12 @@ namespace Content.Shared.Medical.CrewMonitoring
public sealed class CrewMonitoringState : BoundUserInterfaceState public sealed class CrewMonitoringState : BoundUserInterfaceState
{ {
public List<SuitSensorStatus> Sensors; public List<SuitSensorStatus> Sensors;
public readonly Vector2 WorldPosition;
public readonly bool Snap; public readonly bool Snap;
public readonly float Precision; public readonly float Precision;
public CrewMonitoringState(List<SuitSensorStatus> sensors, Vector2 worldPosition, bool snap, float precision) public CrewMonitoringState(List<SuitSensorStatus> sensors, bool snap, float precision)
{ {
Sensors = sensors; Sensors = sensors;
WorldPosition = worldPosition;
Snap = snap; Snap = snap;
Precision = precision; Precision = precision;
} }

View File

@@ -50,7 +50,7 @@ namespace Content.Shared.Medical.SuitSensor
public const string NET_JOB = "job"; public const string NET_JOB = "job";
public const string NET_IS_ALIVE = "alive"; public const string NET_IS_ALIVE = "alive";
public const string NET_TOTAL_DAMAGE = "vitals"; public const string NET_TOTAL_DAMAGE = "vitals";
public const string NET_CORDINATES = "cords"; public const string NET_COORDINATES = "coords";
///Used by the CrewMonitoringServerSystem to send the status of all connected suit sensors to each crew monitor ///Used by the CrewMonitoringServerSystem to send the status of all connected suit sensors to each crew monitor
public const string NET_STATUS_COLLECTION = "suit-status-collection"; public const string NET_STATUS_COLLECTION = "suit-status-collection";

View File

@@ -0,0 +1,29 @@
using Robust.Shared.GameStates;
using Robust.Shared.Timing;
namespace Content.Shared.Pinpointer;
/// <summary>
/// Used to store grid poly data to be used for UIs.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed class NavMapComponent : Component
{
[ViewVariables]
public readonly Dictionary<Vector2i, NavMapChunk> Chunks = new();
}
public sealed class NavMapChunk
{
public readonly Vector2i Origin;
/// <summary>
/// Bitmask for tiles, 1 for occupied and 0 for empty.
/// </summary>
public int TileData;
public NavMapChunk(Vector2i origin)
{
Origin = origin;
}
}

View File

@@ -0,0 +1,45 @@
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Pinpointer;
public abstract class SharedNavMapSystem : EntitySystem
{
public const byte ChunkSize = 4;
/// <summary>
/// Converts the chunk's tile into a bitflag for the slot.
/// </summary>
public static int GetFlag(Vector2i relativeTile)
{
return 1 << (relativeTile.X * ChunkSize + relativeTile.Y);
}
/// <summary>
/// Converts the chunk's tile into a bitflag for the slot.
/// </summary>
public static Vector2i GetTile(int flag)
{
var value = Math.Log2(flag);
var x = (int) value / ChunkSize;
var y = (int) value % ChunkSize;
var result = new Vector2i(x, y);
DebugTools.Assert(GetFlag(result) == flag);
return new Vector2i(x, y);
}
[Serializable, NetSerializable]
protected sealed class NavMapComponentState : ComponentState
{
public Dictionary<Vector2i, int> TileData = new();
}
[Serializable, NetSerializable]
protected sealed class NavMapDiffComponentState : ComponentState
{
public Dictionary<Vector2i, int> TileData = new();
public List<Vector2i> RemovedChunks = new();
}
}

View File

@@ -0,0 +1,9 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Pinpointer;
[Serializable, NetSerializable]
public enum StationMapUiKey : byte
{
Key,
}

View File

@@ -3,7 +3,7 @@ using Robust.Shared.Serialization;
namespace Content.Shared.Power namespace Content.Shared.Power
{ {
[Serializable, NetSerializable] [Serializable, NetSerializable]
public enum PowerDeviceVisuals public enum PowerDeviceVisuals : byte
{ {
VisualState, VisualState,
Powered Powered

View File

@@ -0,0 +1 @@
station-map-window-title = Station map

View File

@@ -0,0 +1,5 @@
- type: entity
parent: BaseElectronics
id: StationMapCircuitboard
name: station map circuit board
description: A printed circuit board for a station map.

View File

@@ -0,0 +1,33 @@
- type: entity
id: StationMap
name: station map
description: Displays a readout of the current station.
parent: BaseItem
suffix: Handheld
components:
- type: StationMap
- type: Sprite
netsync: false
sprite: Objects/Devices/tablets.rsi
layers:
- state: tablet
- state: generic
shader: unshaded
- type: ActivatableUI
inHandsOnly: true
singleUser: true
key: enum.StationMapUiKey.Key
- type: Damageable
damageContainer: Inorganic
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 100
behaviors:
- !type:DoActsBehavior
acts: [ "Destruction" ]
- type: UserInterface
interfaces:
- key: enum.StationMapUiKey.Key
type: StationMapBoundUserInterface

View File

@@ -0,0 +1,76 @@
- type: entity
id: WallStationMapBroken
name: station map
description: A virtual map of the surrounding station.
suffix: Wall broken
placement:
mode: SnapgridCenter
components:
- type: InteractionOutline
- type: Clickable
- type: Sprite
netsync: false
sprite: Structures/Machines/station_map.rsi
drawdepth: WallMountedItems
layers:
- state: station_map_broken
- type: Damageable
damageContainer: Inorganic
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 100
behaviors:
- !type:PlaySoundBehavior
sound:
collection: GlassBreak
- !type:DoActsBehavior
acts: [ "Destruction" ]
- type: entity
id: WallStationMap
name: station map
parent: WallStationMapBroken
suffix: Wall
placement:
mode: SnapgridCenter
components:
- type: StationMap
- type: Transform
anchored: true
- type: Sprite
layers:
- state: station_map0
- state: unshaded
map: [ "enum.PowerDeviceVisualLayers.Powered" ]
shader: unshaded
- type: ApcPowerReceiver
powerLoad: 200
priority: Low
- type: WallMount
arc: 360
- type: ExtensionCableReceiver
- type: ActivatableUIRequiresPower
- type: ActivatableUI
key: enum.StationMapUiKey.Key
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 100
behaviors:
- !type:PlaySoundBehavior
sound:
collection: GlassBreak
- !type:SpawnEntitiesBehavior
spawn:
WallStationMapBroken:
min: 1
max: 1
- !type:DoActsBehavior
acts: [ "Destruction" ]
- type: UserInterface
interfaces:
- key: enum.StationMapUiKey.Key
type: StationMapBoundUserInterface

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

View File

@@ -0,0 +1 @@
{"version":1,"license":"CC-BY-SA-3.0","copyright":"https://github.com/Baystation12/Baystation12/tree/17e84546562b97d775cb26790a08e6d87e7f8077","size":{"x":32,"y":32},"states":[{"name":"tablet"},{"name":"tabletsol"},{"name":"generic","delays":[[0.3,0.3]]}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

View File

@@ -0,0 +1 @@
{"version":1,"license":"CC-BY-SA-3.0","copyright":"https://github.com/vgstation-coders/vgstation13/tree/c5d2df28ee81ca4c7d57f4a2a3dd548a7995c084","size":{"x":32,"y":32},"states":[{"name":"station_map_frame0"},{"name":"station_map_frame1"},{"name":"station_map_frame2"},{"name":"station_map_frame3"},{"name":"unshaded"},{"name":"station_map_broken"},{"name":"station_map0"},{"name":"station_map-panel"}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 513 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB