Camera map (#39684)

* Camera map

* I hope this helps

* Review 1

* Review 2

* Review 3

* Review 4

* Review 5

* Colorblind mode support

* Review 6

* Change design

* Map wire

* Logic fix

* Fix a terrible mistake

* Fix

* Fix 2

* Small rename

* More fix

* Better removal

* And another fix

* Will it work?

* It is literally pointless

* some small things
This commit is contained in:
B_Kirill
2026-01-16 07:21:55 +10:00
committed by GitHub
parent 8fb3e138a9
commit b14964398b
12 changed files with 519 additions and 37 deletions

View File

@@ -34,11 +34,17 @@ public sealed class SurveillanceCameraMonitorBoundUserInterface : BoundUserInter
_window.SubnetRefresh += OnSubnetRefresh;
_window.CameraSwitchTimer += OnCameraSwitchTimer;
_window.CameraDisconnect += OnCameraDisconnect;
var xform = EntMan.GetComponent<TransformComponent>(Owner);
var gridUid = xform.GridUid ?? xform.MapUid;
if (gridUid is not null)
_window?.SetMap(gridUid.Value);
}
private void OnCameraSelected(string address)
private void OnCameraSelected(string address, string? subnet)
{
SendMessage(new SurveillanceCameraMonitorSwitchMessage(address));
SendMessage(new SurveillanceCameraMonitorSwitchMessage(address, subnet));
}
private void OnSubnetRequest(string subnet)

View File

@@ -1,25 +1,71 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:viewport="clr-namespace:Content.Client.Viewport"
xmlns:local="clr-namespace:Content.Client.SurveillanceCamera.UI"
Title="{Loc 'surveillance-camera-monitor-ui-window'}">
<BoxContainer Orientation="Horizontal">
<BoxContainer Orientation="Vertical" MinWidth="350" VerticalExpand="True">
<!-- lazy -->
<OptionButton Name="SubnetSelector" />
<Button Name="SubnetRefreshButton" Text="{Loc 'surveillance-camera-monitor-ui-refresh-subnets'}" />
<ScrollContainer VerticalExpand="True">
<ItemList Name="SubnetList" />
</ScrollContainer>
<Button Name="CameraRefreshButton" Text="{Loc 'surveillance-camera-monitor-ui-refresh-cameras'}" />
<Button Name="CameraDisconnectButton" Text="{Loc 'surveillance-camera-monitor-ui-disconnect'}" />
<Label Name="CameraStatus" />
<BoxContainer>
<!-- Panel with tabs -->
<BoxContainer Orientation="Vertical" MinWidth="350">
<TabContainer Name="ViewModeTabs" VerticalExpand="True">
<!-- Camera list tab -->
<BoxContainer Name="{Loc 'surveillance-camera-monitor-ui-tab-list'}" Orientation="Vertical">
<OptionButton Name="SubnetSelector"/>
<Button Name="SubnetRefreshButton" Text="{Loc 'surveillance-camera-monitor-ui-refresh-subnets'}"/>
<ScrollContainer VerticalExpand="True">
<ItemList Name="SubnetList"/>
</ScrollContainer>
<Button Name="CameraRefreshButton" Text="{Loc 'surveillance-camera-monitor-ui-refresh-cameras'}"/>
</BoxContainer>
<!-- Map view tab -->
<BoxContainer Name="{Loc 'surveillance-camera-monitor-ui-tab-map'}" Orientation="Vertical" VerticalExpand="True">
<local:SurveillanceCameraNavMapControl Name="CameraMap"
VerticalExpand="True"
HorizontalExpand="True"
MinSize="350 350"/>
<!-- Map legend -->
<BoxContainer Name="LegendContainer" Margin="0 10">
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_triangle.png"
Modulate="#FF00FF"
SetSize="20 20"
Margin="10 0 5 0"/>
<Label Text="{Loc 'surveillance-camera-monitor-ui-legend-active'}"/>
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_triangle.png"
SetSize="20 20"
Modulate="#fbff19ff"
Margin="10 0 5 0"/>
<Label Text="{Loc 'surveillance-camera-monitor-ui-legend-selected'}"/>
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_circle.png"
SetSize="20 20"
Modulate="#a09f9fff"
Margin="10 0 5 0"/>
<Label Text="{Loc 'surveillance-camera-monitor-ui-legend-inactive'}"/>
<TextureRect Stretch="KeepAspectCentered"
TexturePath="/Textures/Interface/NavMap/beveled_square.png"
SetSize="20 20"
Modulate="#fa1f1fff"
Margin="10 0 5 0"/>
<Label Text="{Loc 'surveillance-camera-monitor-ui-legend-invalid'}"/>
</BoxContainer>
<Button Name="SubnetRefreshButtonMap" Text="{Loc 'surveillance-camera-monitor-ui-refresh-subnets'}"/>
</BoxContainer>
</TabContainer>
<Button Name="CameraDisconnectButton" Text="{Loc 'surveillance-camera-monitor-ui-disconnect'}"/>
</BoxContainer>
<!-- Right panel with camera view -->
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<Label Name="CameraStatus"/>
<Control VerticalExpand="True" Margin="5" Name="CameraViewBox">
<viewport:ScalingViewport Name="CameraView" MinSize="500 500" MouseFilter="Ignore"/>
<TextureRect MinSize="500 500" Name="CameraViewBackground" />
</Control>
</BoxContainer>
<Control VerticalExpand="True" HorizontalExpand="True" Margin="5 5 5 5" Name="CameraViewBox">
<viewport:ScalingViewport Name="CameraView"
VerticalExpand="True"
HorizontalExpand="True"
MinSize="500 500"
MouseFilter="Ignore" />
<TextureRect VerticalExpand="True" HorizontalExpand="True" MinSize="500 500" Name="CameraViewBackground" />
</Control>
</BoxContainer>
</DefaultWindow>

View File

@@ -3,6 +3,7 @@ using Content.Client.Resources;
using Content.Client.Viewport;
using Content.Shared.DeviceNetwork;
using Content.Shared.SurveillanceCamera;
using Content.Shared.SurveillanceCamera.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
@@ -21,8 +22,15 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
/// <summary>
/// Triggered when a camera is selected.
/// First parameter contains the camera's address.
/// Second optional parameter contains a subnet - if possible, the monitor will switch to this subnet.
/// </summary>
public event Action<string, string?>? CameraSelected;
public event Action<string>? CameraSelected;
public event Action<string>? SubnetOpened;
public event Action? CameraRefresh;
public event Action? SubnetRefresh;
@@ -33,6 +41,7 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
private bool _isSwitching;
private readonly FixedEye _defaultEye = new();
private readonly Dictionary<string, int> _subnetMap = new();
private EntityUid? _mapUid;
private string? SelectedSubnet
{
@@ -68,11 +77,15 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
SubnetSelector.OnItemSelected += args =>
{
// piss
SubnetOpened!((string) args.Button.GetItemMetadata(args.Id)!);
SubnetOpened?.Invoke((string) args.Button.GetItemMetadata(args.Id)!);
};
SubnetRefreshButton.OnPressed += _ => SubnetRefresh!();
CameraRefreshButton.OnPressed += _ => CameraRefresh!();
CameraDisconnectButton.OnPressed += _ => CameraDisconnect!();
SubnetRefreshButton.OnPressed += _ => SubnetRefresh?.Invoke();
SubnetRefreshButtonMap.OnPressed += _ => SubnetRefresh?.Invoke();
CameraRefreshButton.OnPressed += _ => CameraRefresh?.Invoke();
CameraDisconnectButton.OnPressed += _ => CameraDisconnect?.Invoke();
CameraMap.EnableCameraSelection = true;
CameraMap.CameraSelected += OnCameraMapSelected;
}
@@ -80,6 +93,9 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
// pass it here so that the UI can change its view.
public void UpdateState(IEye? eye, HashSet<string> subnets, string activeAddress, string activeSubnet, Dictionary<string, string> cameras)
{
CameraMap.SetActiveCameraAddress(activeAddress);
CameraMap.SetAvailableSubnets(subnets);
_currentAddress = activeAddress;
SetCameraView(eye);
@@ -189,6 +205,25 @@ public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
private void OnSubnetListSelect(ItemList.ItemListSelectedEventArgs args)
{
CameraSelected!((string) SubnetList[args.ItemIndex].Metadata!);
CameraSelected!((string) SubnetList[args.ItemIndex].Metadata!, null);
}
public void SetMap(EntityUid mapUid)
{
CameraMap.MapUid = _mapUid = mapUid;
}
private void OnCameraMapSelected(NetEntity netEntity)
{
if (_mapUid is null || !_entityManager.TryGetComponent<SurveillanceCameraMapComponent>(_mapUid.Value, out var mapComp))
return;
if (!mapComp.Cameras.TryGetValue(netEntity, out var marker) || !marker.Active)
return;
if (!string.IsNullOrEmpty(marker.Address))
CameraSelected?.Invoke(marker.Address, marker.Subnet);
else
_entityManager.RaisePredictiveEvent(new RequestCameraMarkerUpdateMessage(netEntity));
}
}

View File

@@ -0,0 +1,130 @@
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Map;
using Content.Client.Pinpointer.UI;
using Content.Client.Resources;
using Content.Shared.SurveillanceCamera.Components;
namespace Content.Client.SurveillanceCamera.UI;
public sealed class SurveillanceCameraNavMapControl : NavMapControl
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
private static readonly Color CameraActiveColor = Color.FromHex("#FF00FF");
private static readonly Color CameraInactiveColor = Color.FromHex("#a09f9fff");
private static readonly Color CameraSelectedColor = Color.FromHex("#fbff19ff");
private static readonly Color CameraInvalidColor = Color.FromHex("#fa1f1fff");
private readonly Texture _activeTexture;
private readonly Texture _inactiveTexture;
private readonly Texture _selectedTexture;
private readonly Texture _invalidTexture;
private string _activeCameraAddress = string.Empty;
private HashSet<string> _availableSubnets = new();
private (Dictionary<NetEntity, CameraMarker> Cameras, string ActiveAddress, HashSet<string> AvailableSubnets) _lastState;
public bool EnableCameraSelection { get; set; }
public event Action<NetEntity>? CameraSelected;
public SurveillanceCameraNavMapControl()
{
IoCManager.InjectDependencies(this);
_activeTexture = _resourceCache.GetTexture("/Textures/Interface/NavMap/beveled_triangle.png");
_selectedTexture = _activeTexture;
_inactiveTexture = _resourceCache.GetTexture("/Textures/Interface/NavMap/beveled_circle.png");
_invalidTexture = _resourceCache.GetTexture("/Textures/Interface/NavMap/beveled_square.png");
TrackedEntitySelectedAction += entity =>
{
if (entity.HasValue)
CameraSelected?.Invoke(entity.Value);
};
}
public void SetActiveCameraAddress(string address)
{
if (_activeCameraAddress == address)
return;
_activeCameraAddress = address;
ForceNavMapUpdate();
}
public void SetAvailableSubnets(HashSet<string> subnets)
{
if (_availableSubnets.SetEquals(subnets))
return;
_availableSubnets = subnets;
ForceNavMapUpdate();
}
protected override void UpdateNavMap()
{
base.UpdateNavMap();
if (MapUid is null || !_entityManager.TryGetComponent<SurveillanceCameraMapComponent>(MapUid, out var mapComp))
return;
var currentState = (mapComp.Cameras, _activeCameraAddress, _availableSubnets);
if (_lastState.Equals(currentState))
return;
_lastState = currentState;
UpdateCameraMarkers(mapComp);
}
private void UpdateCameraMarkers(SurveillanceCameraMapComponent mapComp)
{
TrackedEntities.Clear();
if (MapUid is null)
return;
foreach (var (netEntity, marker) in mapComp.Cameras)
{
if (!marker.Visible || !_availableSubnets.Contains(marker.Subnet))
continue;
var coords = new EntityCoordinates(MapUid.Value, marker.Position);
Texture texture;
Color color;
if (string.IsNullOrEmpty(marker.Address))
{
color = CameraInvalidColor;
texture = _invalidTexture;
}
else if (marker.Address == _activeCameraAddress)
{
color = CameraSelectedColor;
texture = _selectedTexture;
}
else if (marker.Active)
{
color = CameraActiveColor;
texture = _activeTexture;
}
else
{
color = CameraInactiveColor;
texture = _inactiveTexture;
}
TrackedEntities[netEntity] = new NavMapBlip(
coords,
texture,
color,
false,
EnableCameraSelection
);
}
}
}