Surveillance cameras (#8246)

* cameras but i didn't feel like git stashing them

* Adds more functionality server-side for surveillance cameras

* rider moment

* Adds skeleton for SurveillanceCameraMonitorBoundUi on client

* whoops

* makes surveillance camera monitor UI more defined

* removes tree from SurveillanceCameraMonitorWindow

* surveillance camera active flag, other routing things

* actually sets how SurveillanceCameraMonitorSystem sends camera info to clients

* adds entity yaml, changes field

* adds the camera/monitor entities, makes the UI open

* SurveillanceCameraRouters (not implemented fully)

* subnets for cameras, server-side

* it works!

* fixes rotation in cameras

* whoops

restores surveillance cameras to ignored components
makes it so that switching cameras now lerps the other camera

* makes the UI work

* makes it so that cameras actually refresh now

* cleanup

* adds camera.rsi

* cleans up prototypes a bit

* adds camera subnet frequencies, cameras in subnets

* adds surveillance camera router subnets

* might fix testing errors

* adds the circuit board to the surveillance camera monitor

* fixes up the camera monitor (the detective will get his tv soon)

* adds heartbeat, ensures subnet data is passed into cameras to send

* fixes up a few things

* whoops

* changes to UI internals

* fixes subnet selection issue

* localized strings for UI

* changes 'test' id to 'camera' for cameras

* whoops

* missing s

* camera static!

* adds a delay to camera switching

* adjusts a few things in camera timing

* adds setup for cameras/routers, localization for frequency names

* adds setup ui for routers, makes subnet names in monitor window follow frequency name in prototype

* localization, some cleanup

* ui adjustments

* adds surveillance camera visuals

* fixes a bug when closing the UI for monitors

* adds disconnect message to UI

* adds construction graph to cameras

* adds the camera to the construction menu

* fixes network selection for setup, tweak to assembly

* adds surveillance camera router construction, fixes up surveillance camera wire cutting

* adds disconnect button to monitor UI

* switches around the status text

* tweaks monitor UI

* makes the address actually show

* might make tests pass

* UI adjustments, hard name limit

* ok, that didn't work

* adds wireless cameras

* makes the television work/look nicer

* adds tripod cameras in addition to mobile cameras

* makes wireless cameras constructable

* fixes up those prototypes

* reorganization in C#, small cleanup

* ensures that power changes deactivate some devices

* adds a component to the television, comments out a function

* actually, never mind, i forgot that wireless cameras existed/are creatable for a second

* tweaks to router construction, removes SubnetTest from prototypes

* removes it from frequencies too

* Apply suggestions from code review

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>

* type serializers into components

* setup window opens centered, enum is now byte

* replaces active monitor list with ActiveSurveillanceCameraMonitorComponent

* adds paused/deleted entity checks, changes how verbs are given

* removes EntitySystem.Get<T>() references

* fixes bug related to selecting network from setup, alphabet-orders network listing in setup

* rider moment

* adds minwidth to surveillance camera setup window

* addresses reviews

* should fix the issue with camera visuals not updating properly

* addresses some reviews

* addresses further review

* addresses reviews related to RSIs

* never needed a key there anyways

* changes a few things with routers to ensure that they're active

* whoops

* ensurecomp over addcomp

* whoops

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
This commit is contained in:
Flipp Syder
2022-05-31 01:44:57 -07:00
committed by GitHub
parent 69d8a2beee
commit d4697c000c
59 changed files with 2945 additions and 27 deletions

View File

@@ -0,0 +1,9 @@
namespace Content.Client.SurveillanceCamera;
[RegisterComponent]
public sealed class ActiveSurveillanceCameraMonitorVisualsComponent : Component
{
public float TimeLeft = 30f;
public Action? OnFinish;
}

View File

@@ -0,0 +1,49 @@
using Robust.Shared.Utility;
namespace Content.Client.SurveillanceCamera;
public sealed class SurveillanceCameraMonitorSystem : EntitySystem
{
private readonly RemQueue<EntityUid> _toRemove = new();
public override void Update(float frameTime)
{
foreach (var comp in EntityQuery<ActiveSurveillanceCameraMonitorVisualsComponent>())
{
if (Paused(comp.Owner))
{
continue;
}
comp.TimeLeft -= frameTime;
if (comp.TimeLeft <= 0 || Deleted(comp.Owner))
{
if (comp.OnFinish != null)
{
comp.OnFinish();
}
_toRemove.Add(comp.Owner);
}
}
foreach (var uid in _toRemove)
{
EntityManager.RemoveComponent<ActiveSurveillanceCameraMonitorVisualsComponent>(uid);
}
_toRemove.List?.Clear();
}
public void AddTimer(EntityUid uid, Action onFinish)
{
var comp = EnsureComp<ActiveSurveillanceCameraMonitorVisualsComponent>(uid);
comp.OnFinish = onFinish;
}
public void RemoveTimer(EntityUid uid)
{
EntityManager.RemoveComponent<ActiveSurveillanceCameraMonitorVisualsComponent>(uid);
}
}

View File

@@ -0,0 +1,12 @@
using Content.Shared.SurveillanceCamera;
namespace Content.Client.SurveillanceCamera;
// Dummy component so that targetted events work on client for
// appearance events.
[RegisterComponent]
public sealed class SurveillanceCameraVisualsComponent : Component
{
[DataField("sprites")]
public readonly Dictionary<SurveillanceCameraVisuals, string> CameraSprites = new();
}

View File

@@ -0,0 +1,29 @@
using Content.Shared.SurveillanceCamera;
using Robust.Client.GameObjects;
namespace Content.Client.SurveillanceCamera;
public sealed class SurveillanceCameraVisualsSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SurveillanceCameraVisualsComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnAppearanceChange(EntityUid uid, SurveillanceCameraVisualsComponent component,
ref AppearanceChangeEvent args)
{
if (!args.AppearanceData.TryGetValue(SurveillanceCameraVisualsKey.Key, out var data)
|| data is not SurveillanceCameraVisuals key
|| args.Sprite == null
|| !args.Sprite.LayerMapTryGet(SurveillanceCameraVisualsKey.Layer, out int layer)
|| !component.CameraSprites.TryGetValue(key, out var state))
{
return;
}
args.Sprite.LayerSetState(layer, state);
}
}

View File

@@ -0,0 +1,123 @@
using Content.Client.Eye;
using Content.Shared.SurveillanceCamera;
using Robust.Client.GameObjects;
namespace Content.Client.SurveillanceCamera.UI;
public sealed class SurveillanceCameraMonitorBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly EyeLerpingSystem _eyeLerpingSystem;
private readonly SurveillanceCameraMonitorSystem _surveillanceCameraMonitorSystem;
private SurveillanceCameraMonitorWindow? _window;
private EntityUid? _currentCamera;
public SurveillanceCameraMonitorBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
_eyeLerpingSystem = _entityManager.EntitySysManager.GetEntitySystem<EyeLerpingSystem>();
_surveillanceCameraMonitorSystem = _entityManager.EntitySysManager.GetEntitySystem<SurveillanceCameraMonitorSystem>();
}
protected override void Open()
{
base.Open();
_window = new SurveillanceCameraMonitorWindow();
if (State != null)
{
UpdateState(State);
}
_window.OpenCentered();
_window.CameraSelected += OnCameraSelected;
_window.SubnetOpened += OnSubnetRequest;
_window.CameraRefresh += OnCameraRefresh;
_window.SubnetRefresh += OnSubnetRefresh;
_window.OnClose += Close;
_window.CameraSwitchTimer += OnCameraSwitchTimer;
_window.CameraDisconnect += OnCameraDisconnect;
}
private void OnCameraSelected(string address)
{
SendMessage(new SurveillanceCameraMonitorSwitchMessage(address));
}
private void OnSubnetRequest(string subnet)
{
SendMessage(new SurveillanceCameraMonitorSubnetRequestMessage(subnet));
}
private void OnCameraSwitchTimer()
{
_surveillanceCameraMonitorSystem.AddTimer(Owner.Owner, _window!.OnSwitchTimerComplete);
}
private void OnCameraRefresh()
{
SendMessage(new SurveillanceCameraRefreshCamerasMessage());
}
private void OnSubnetRefresh()
{
SendMessage(new SurveillanceCameraRefreshSubnetsMessage());
}
private void OnCameraDisconnect()
{
SendMessage(new SurveillanceCameraDisconnectMessage());
}
protected override void UpdateState(BoundUserInterfaceState state)
{
if (_window == null || state is not SurveillanceCameraMonitorUiState cast)
{
return;
}
if (cast.ActiveCamera == null)
{
_window.UpdateState(null, cast.Subnets, cast.ActiveAddress, cast.ActiveSubnet, cast.Cameras);
if (_currentCamera != null)
{
_surveillanceCameraMonitorSystem.RemoveTimer(Owner.Owner);
_eyeLerpingSystem.RemoveEye(_currentCamera.Value);
_currentCamera = null;
}
}
else
{
if (_currentCamera == null)
{
_eyeLerpingSystem.AddEye(cast.ActiveCamera.Value);
_currentCamera = cast.ActiveCamera;
}
else if (_currentCamera != cast.ActiveCamera)
{
_eyeLerpingSystem.RemoveEye(_currentCamera.Value);
_eyeLerpingSystem.AddEye(cast.ActiveCamera.Value);
_currentCamera = cast.ActiveCamera;
}
if (_entityManager.TryGetComponent(cast.ActiveCamera, out EyeComponent eye))
{
_window.UpdateState(eye.Eye, cast.Subnets, cast.ActiveAddress, cast.ActiveSubnet, cast.Cameras);
}
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_window?.Dispose();
}
}
}

View File

@@ -0,0 +1,25 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:viewport="clr-namespace:Content.Client.Viewport"
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>
<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

@@ -0,0 +1,188 @@
using System.Linq;
using Content.Client.Resources;
using Content.Client.Viewport;
using Content.Shared.DeviceNetwork;
using Content.Shared.SurveillanceCamera;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.SurveillanceCamera.UI;
[GenerateTypedNameReferences]
public sealed partial class SurveillanceCameraMonitorWindow : DefaultWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
public event Action<string>? CameraSelected;
public event Action<string>? SubnetOpened;
public event Action? CameraRefresh;
public event Action? SubnetRefresh;
public event Action? CameraSwitchTimer;
public event Action? CameraDisconnect;
private string _currentAddress = string.Empty;
private readonly FixedEye _defaultEye = new();
private readonly Dictionary<string, int> _subnetMap = new();
private string? SelectedSubnet
{
get
{
if (SubnetSelector.ItemCount == 0
|| SubnetSelector.SelectedMetadata == null)
{
return null;
}
return (string) SubnetSelector.SelectedMetadata;
}
}
public SurveillanceCameraMonitorWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
// This could be done better. I don't want to deal with stylesheets at the moment.
var texture = _resourceCache.GetTexture("/Textures/Interface/Nano/square_black.png");
var shader = _prototypeManager.Index<ShaderPrototype>("CameraStatic").Instance().Duplicate();
CameraView.ViewportSize = new Vector2i(500, 500);
CameraView.Eye = _defaultEye; // sure
CameraViewBackground.Stretch = TextureRect.StretchMode.Scale;
CameraViewBackground.Texture = texture;
CameraViewBackground.ShaderOverride = shader;
SubnetList.OnItemSelected += OnSubnetListSelect;
SubnetSelector.OnItemSelected += args =>
{
// piss
SubnetOpened!((string) args.Button.GetItemMetadata(args.Id)!);
};
SubnetRefreshButton.OnPressed += _ => SubnetRefresh!();
CameraRefreshButton.OnPressed += _ => CameraRefresh!();
CameraDisconnectButton.OnPressed += _ => CameraDisconnect!();
}
// The UI class should get the eye from the entity, and then
// 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)
{
_currentAddress = activeAddress;
SetCameraView(eye);
if (subnets.Count == 0)
{
SubnetSelector.AddItem(Loc.GetString("surveillance-camera-monitor-ui-no-subnets"));
SubnetSelector.Disabled = true;
return;
}
if (SubnetSelector.Disabled && subnets.Count != 0)
{
SubnetSelector.Clear();
SubnetSelector.Disabled = false;
}
// That way, we have *a* subnet selected if this is ever opened.
if (string.IsNullOrEmpty(activeSubnet))
{
SubnetOpened!(subnets.First());
return;
}
// if the subnet count is unequal, that means
// we have to rebuild the subnet selector
if (SubnetSelector.ItemCount != subnets.Count)
{
SubnetSelector.Clear();
_subnetMap.Clear();
foreach (var subnet in subnets)
{
var id = AddSubnet(subnet);
_subnetMap.Add(subnet, id);
}
}
if (_subnetMap.TryGetValue(activeSubnet, out var subnetId))
{
SubnetSelector.Select(subnetId);
}
PopulateCameraList(cameras);
}
private void PopulateCameraList(Dictionary<string, string> cameras)
{
SubnetList.Clear();
foreach (var (address, name) in cameras)
{
AddCameraToList(name, address);
}
SubnetList.SortItemsByText();
}
private void SetCameraView(IEye? eye)
{
CameraView.Eye = eye ?? _defaultEye;
CameraView.Visible = eye != null;
CameraViewBackground.Visible = true;
CameraDisconnectButton.Disabled = eye == null;
if (eye != null)
{
CameraStatus.Text = Loc.GetString("surveillance-camera-monitor-ui-status",
("status", Loc.GetString("surveillance-camera-monitor-ui-status-connecting")),
("address", _currentAddress));
CameraSwitchTimer!();
}
else
{
CameraStatus.Text = Loc.GetString("surveillance-camera-monitor-ui-status-disconnected");
}
}
public void OnSwitchTimerComplete()
{
CameraViewBackground.Visible = false;
CameraStatus.Text = Loc.GetString("surveillance-camera-monitor-ui-status",
("status", Loc.GetString("surveillance-camera-monitor-ui-status-connected")),
("address", _currentAddress));
}
private int AddSubnet(string subnet)
{
var name = subnet;
if (_prototypeManager.TryIndex<DeviceFrequencyPrototype>(subnet, out var frequency))
{
name = Loc.GetString(frequency.Name ?? subnet);
}
SubnetSelector.AddItem(name);
SubnetSelector.SetItemMetadata(SubnetSelector.ItemCount - 1, subnet);
return SubnetSelector.ItemCount - 1;
}
private void AddCameraToList(string name, string address)
{
var item = SubnetList.AddItem($"{name}: {address}");
item.Metadata = address;
}
private void OnSubnetListSelect(ItemList.ItemListSelectedEventArgs args)
{
CameraSelected!((string) SubnetList[args.ItemIndex].Metadata!);
}
}

View File

@@ -0,0 +1,68 @@
using Content.Shared.SurveillanceCamera;
using Robust.Client.GameObjects;
namespace Content.Client.SurveillanceCamera.UI;
public sealed class SurveillanceCameraSetupBoundUi : BoundUserInterface
{
private SurveillanceCameraSetupWindow? _window;
private SurveillanceCameraSetupUiKey _type;
public SurveillanceCameraSetupBoundUi(ClientUserInterfaceComponent component, object uiKey) : base(component, uiKey)
{
if (uiKey is not SurveillanceCameraSetupUiKey key)
{
return;
}
_type = key;
}
protected override void Open()
{
_window = new();
if (_type == SurveillanceCameraSetupUiKey.Router)
{
_window.HideNameSelector();
}
_window.OpenCentered();
_window.OnNameConfirm += SendDeviceName;
_window.OnNetworkConfirm += SendSelectedNetwork;
}
private void SendSelectedNetwork(int idx)
{
SendMessage(new SurveillanceCameraSetupSetNetwork(idx));
}
private void SendDeviceName(string name)
{
SendMessage(new SurveillanceCameraSetupSetName(name));
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (_window == null || state is not SurveillanceCameraSetupBoundUiState cast)
{
return;
}
_window.UpdateState(cast.Name, cast.NameDisabled, cast.NetworkDisabled);
_window.LoadAvailableNetworks(cast.Network, cast.Networks);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
_window!.Dispose();
}
}
}

View File

@@ -0,0 +1,14 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'surveillance-camera-setup'}"
MinWidth="400">
<BoxContainer Orientation="Vertical">
<BoxContainer Name="NamingSection">
<LineEdit HorizontalExpand="True" Name="DeviceName" />
<Button Name="NameConfirm" Text="{Loc 'surveillance-camera-setup-ui-set'}"/>
</BoxContainer>
<BoxContainer Name="NetworkSection">
<OptionButton HorizontalExpand="True" Name="NetworkSelector" />
<Button Name="NetworkConfirm" Text="{Loc 'surveillance-camera-setup-ui-set'}" />
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -0,0 +1,78 @@
using Content.Shared.DeviceNetwork;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.SurveillanceCamera.UI;
[GenerateTypedNameReferences]
public sealed partial class SurveillanceCameraSetupWindow : DefaultWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public Action<string>? OnNameConfirm;
public Action<int>? OnNetworkConfirm;
public SurveillanceCameraSetupWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
NetworkConfirm.OnPressed += _ => OnNetworkConfirm!(NetworkSelector.SelectedId);
NameConfirm.OnPressed += _ => OnNameConfirm!(DeviceName.Text);
NetworkSelector.OnItemSelected += args => NetworkSelector.SelectId(args.Id);
}
public void HideNameSelector() => NamingSection.Visible = false;
public void UpdateState(string name, bool disableNaming, bool disableNetworkSelector)
{
DeviceName.Text = name;
DeviceName.Editable = !disableNaming;
NameConfirm.Disabled = disableNaming;
NetworkSelector.Disabled = disableNetworkSelector;
NetworkConfirm.Disabled = disableNetworkSelector;
}
// Pass in a list of frequency prototype IDs.
public void LoadAvailableNetworks(uint currentNetwork, List<string> networks)
{
NetworkSelector.Clear();
if (networks.Count == 0)
{
NetworkSection.Visible = false;
return;
}
var id = 0;
var idList = new List<(int id, string networkName)>();
foreach (var network in networks)
{
idList.Add((id, network));
id++;
}
idList.Sort((a, b) => a.networkName.CompareTo(b.networkName));
foreach (var (networkId, network) in idList)
{
if (!_prototypeManager.TryIndex(network, out DeviceFrequencyPrototype? frequency)
|| frequency.Name == null)
{
continue;
}
NetworkSelector.AddItem(Loc.GetString(frequency.Name), networkId);
if (frequency.Frequency == currentNetwork)
{
NetworkSelector.SelectId(networkId);
}
id++;
}
}
}

View File

@@ -24,7 +24,8 @@ namespace Content.Server.Entry
"ToggleableLightVisuals", "ToggleableLightVisuals",
"CableVisualizer", "CableVisualizer",
"PotencyVisuals", "PotencyVisuals",
"PaperVisuals" "PaperVisuals",
"SurveillanceCameraVisuals"
}; };
} }
} }

View File

@@ -0,0 +1,7 @@
namespace Content.Server.SurveillanceCamera;
// Dummy component for active surveillance monitors.
[RegisterComponent]
public sealed class ActiveSurveillanceCameraMonitorComponent : Component
{
}

View File

@@ -0,0 +1,46 @@
using Content.Shared.DeviceNetwork;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.SurveillanceCamera;
[RegisterComponent]
[Friend(typeof(SurveillanceCameraSystem))]
public sealed class SurveillanceCameraComponent : Component
{
// List of active viewers. This is for bookkeeping purposes,
// so that when a camera shuts down, any entity viewing it
// will immediately have their subscription revoked.
public HashSet<EntityUid> ActiveViewers { get; } = new();
// Monitors != Viewers, as viewers are entities that are tied
// to a player session that's viewing from this camera
//
// Monitors are grouped sets of viewers, and may be
// completely different monitor types (e.g., monitor console,
// AI, etc.)
public HashSet<EntityUid> ActiveMonitors { get; } = new();
// If this camera is active or not. Deactivating a camera
// will not allow it to obtain any new viewers.
[ViewVariables]
public bool Active { get; set; } = true;
// This one isn't easy to deal with. Will require a UI
// to change/set this so mapping these in isn't
// the most terrible thing possible.
[ViewVariables(VVAccess.ReadWrite)]
[DataField("id")]
public string CameraId { get; set; } = "camera";
[ViewVariables(VVAccess.ReadWrite)]
[DataField("nameSet")]
public bool NameSet { get; set; }
[ViewVariables(VVAccess.ReadWrite)]
[DataField("networkSet")]
public bool NetworkSet { get; set; }
// This has to be device network frequency prototypes.
[DataField("setupAvailableNetworks", customTypeSerializer:typeof(PrototypeIdListSerializer<DeviceFrequencyPrototype>))]
public List<string> AvailableNetworks { get; } = new();
}

View File

@@ -0,0 +1,35 @@
namespace Content.Server.SurveillanceCamera;
[RegisterComponent]
[Friend(typeof(SurveillanceCameraMonitorSystem))]
public sealed class SurveillanceCameraMonitorComponent : Component
{
// Currently active camera viewed by this monitor.
public EntityUid? ActiveCamera { get; set; }
public string ActiveCameraAddress { get; set; } = string.Empty;
// Last time this monitor was sent a heartbeat.
public float LastHeartbeat { get; set; }
// Last time this monitor sent a heartbeat.
public float LastHeartbeatSent { get; set; }
// Next camera this monitor is trying to connect to.
// If the monitor has connected to the camera, this
// should be set to null.
public string? NextCameraAddress { get; set; }
[ViewVariables]
// Set of viewers currently looking at this monitor.
public HashSet<EntityUid> Viewers { get; } = new();
// Current active subnet.
public string ActiveSubnet { get; set; } = default!;
// Known cameras in this subnet by address with name values.
// This is cleared when the subnet is changed.
public Dictionary<string, string> KnownCameras { get; } = new();
[ViewVariables]
// The subnets known by this camera monitor.
public Dictionary<string, string> KnownSubnets { get; } = new();
}

View File

@@ -0,0 +1,31 @@
using Content.Shared.DeviceNetwork;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.SurveillanceCamera;
[RegisterComponent]
public sealed class SurveillanceCameraRouterComponent : Component
{
[ViewVariables] public bool Active { get; set; }
// The name of the subnet connected to this router.
[DataField("subnetName")]
public string SubnetName { get; set; } = string.Empty;
[ViewVariables]
// The monitors to route to. This raises an issue related to
// camera monitors disappearing before sending a D/C packet,
// this could probably be refreshed every time a new monitor
// is added or removed from active routing.
public HashSet<string> MonitorRoutes { get; } = new();
[ViewVariables]
// The frequency that talks to this router's subnet.
public uint SubnetFrequency;
[DataField("subnetFrequency", customTypeSerializer:typeof(PrototypeIdSerializer<DeviceFrequencyPrototype>))]
public string? SubnetFrequencyId { get; set; }
[DataField("setupAvailableNetworks", customTypeSerializer:typeof(PrototypeIdListSerializer<DeviceFrequencyPrototype>))]
public List<string> AvailableNetworks { get; } = new();
}

View File

@@ -0,0 +1,497 @@
using System.Linq;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Power.Components;
using Content.Server.UserInterface;
using Content.Server.Wires;
using Content.Shared.Interaction;
using Content.Shared.SurveillanceCamera;
using Robust.Server.GameObjects;
using Robust.Server.Player;
namespace Content.Server.SurveillanceCamera;
public sealed class SurveillanceCameraMonitorSystem : EntitySystem
{
[Dependency] private readonly SurveillanceCameraSystem _surveillanceCameras = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
public override void Initialize()
{
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, SurveillanceCameraDeactivateEvent>(OnSurveillanceCameraDeactivate);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, BoundUIClosedEvent>(OnBoundUiClose);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, SurveillanceCameraMonitorSwitchMessage>(OnSwitchMessage);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, SurveillanceCameraMonitorSubnetRequestMessage>(OnSubnetRequest);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, AfterActivatableUIOpenEvent>(OnToggleInterface);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, SurveillanceCameraRefreshCamerasMessage>(OnRefreshCamerasMessage);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, SurveillanceCameraRefreshSubnetsMessage>(OnRefreshSubnetsMessage);
SubscribeLocalEvent<SurveillanceCameraMonitorComponent, SurveillanceCameraDisconnectMessage>(OnDisconnectMessage);
}
private const float _maxHeartbeatTime = 300f;
private const float _heartbeatDelay = 30f;
public override void Update(float frameTime)
{
foreach (var (_, monitor) in EntityQuery<ActiveSurveillanceCameraMonitorComponent, SurveillanceCameraMonitorComponent>())
{
if (Paused(monitor.Owner))
{
continue;
}
monitor.LastHeartbeatSent += frameTime;
SendHeartbeat(monitor.Owner, monitor);
monitor.LastHeartbeat += frameTime;
if (monitor.LastHeartbeat > _maxHeartbeatTime)
{
DisconnectCamera(monitor.Owner, true, monitor);
EntityManager.RemoveComponent<ActiveSurveillanceCameraMonitorComponent>(monitor.Owner);
}
}
}
/// ROUTING:
///
/// Monitor freq: General frequency for cameras, routers, and monitors to speak on.
///
/// Subnet freqs: Frequency for each specific subnet. Routers ping cameras here,
/// cameras ping back on monitor frequency. When a monitor
/// selects a subnet, it saves that subnet's frequency
/// so it can connect to the camera. All outbound cameras
/// always speak on the monitor frequency and will not
/// do broadcast pings - whatever talks to it, talks to it.
///
/// How a camera is discovered:
///
/// Subnet ping:
/// Surveillance camera monitor - [ monitor freq ] -> Router
/// Router -> camera discovery
/// Router - [ subnet freq ] -> Camera
/// Camera -> router ping
/// Camera - [ monitor freq ] -> Router
/// Router -> monitor data forward
/// Router - [ monitor freq ] -> Monitor
#region Event Handling
private void OnComponentStartup(EntityUid uid, SurveillanceCameraMonitorComponent component, ComponentStartup args)
{
RefreshSubnets(uid, component);
}
private void OnSubnetRequest(EntityUid uid, SurveillanceCameraMonitorComponent component,
SurveillanceCameraMonitorSubnetRequestMessage args)
{
if (args.Session.AttachedEntity != null)
{
SetActiveSubnet(uid, args.Subnet, component);
}
}
private void OnPacketReceived(EntityUid uid, SurveillanceCameraMonitorComponent component,
DeviceNetworkPacketEvent args)
{
if (string.IsNullOrEmpty(args.SenderAddress))
{
return;
}
if (args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command))
{
switch (command)
{
case SurveillanceCameraSystem.CameraConnectMessage:
if (component.NextCameraAddress == args.SenderAddress)
{
component.ActiveCameraAddress = args.SenderAddress;
TrySwitchCameraByUid(uid, args.Sender, component);
}
component.NextCameraAddress = null;
break;
case SurveillanceCameraSystem.CameraHeartbeatMessage:
if (args.SenderAddress == component.ActiveCameraAddress)
{
component.LastHeartbeat = 0;
}
break;
case SurveillanceCameraSystem.CameraDataMessage:
if (!args.Data.TryGetValue(SurveillanceCameraSystem.CameraNameData, out string? name)
|| !args.Data.TryGetValue(SurveillanceCameraSystem.CameraSubnetData, out string? subnetData)
|| !args.Data.TryGetValue(SurveillanceCameraSystem.CameraAddressData, out string? address))
{
return;
}
if (component.ActiveSubnet != subnetData)
{
DisconnectFromSubnet(uid, subnetData);
}
if (!component.KnownCameras.ContainsKey(address))
{
component.KnownCameras.Add(address, name);
}
UpdateUserInterface(uid, component);
break;
case SurveillanceCameraSystem.CameraSubnetData:
if (args.Data.TryGetValue(SurveillanceCameraSystem.CameraSubnetData, out string? subnet)
&& !component.KnownSubnets.ContainsKey(subnet))
{
component.KnownSubnets.Add(subnet, args.SenderAddress);
}
UpdateUserInterface(uid, component);
break;
}
}
}
private void OnDisconnectMessage(EntityUid uid, SurveillanceCameraMonitorComponent component,
SurveillanceCameraDisconnectMessage message)
{
DisconnectCamera(uid, true, component);
}
private void OnRefreshCamerasMessage(EntityUid uid, SurveillanceCameraMonitorComponent component,
SurveillanceCameraRefreshCamerasMessage message)
{
component.KnownCameras.Clear();
RequestActiveSubnetInfo(uid, component);
}
private void OnRefreshSubnetsMessage(EntityUid uid, SurveillanceCameraMonitorComponent component,
SurveillanceCameraRefreshSubnetsMessage message)
{
RefreshSubnets(uid, component);
}
private void OnSwitchMessage(EntityUid uid, SurveillanceCameraMonitorComponent component, SurveillanceCameraMonitorSwitchMessage message)
{
// there would be a null check here, but honestly
// whichever one is the "latest" switch message gets to
// do the switch
TrySwitchCameraByAddress(uid, message.Address, component);
}
private void OnPowerChanged(EntityUid uid, SurveillanceCameraMonitorComponent component, PowerChangedEvent args)
{
if (!args.Powered)
{
RemoveActiveCamera(uid, component);
component.NextCameraAddress = null;
component.ActiveSubnet = string.Empty;
}
}
private void OnToggleInterface(EntityUid uid, SurveillanceCameraMonitorComponent component,
AfterActivatableUIOpenEvent args)
{
AfterOpenUserInterface(uid, args.User, component);
}
// This is to ensure that there's no delay in ensuring that a camera is deactivated.
private void OnSurveillanceCameraDeactivate(EntityUid uid, SurveillanceCameraMonitorComponent monitor, SurveillanceCameraDeactivateEvent args)
{
DisconnectCamera(uid, false, monitor);
}
private void OnBoundUiClose(EntityUid uid, SurveillanceCameraMonitorComponent component, BoundUIClosedEvent args)
{
if (args.Session.AttachedEntity == null)
{
return;
}
RemoveViewer(uid, args.Session.AttachedEntity.Value, component);
}
#endregion
private void SendHeartbeat(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| monitor.LastHeartbeatSent < _heartbeatDelay
|| !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var subnetAddress))
{
return;
}
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraHeartbeatMessage },
{ SurveillanceCameraSystem.CameraAddressData, monitor.ActiveCameraAddress }
};
_deviceNetworkSystem.QueuePacket(uid, subnetAddress, payload);
}
private void DisconnectCamera(EntityUid uid, bool removeViewers, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
if (removeViewers)
{
RemoveActiveCamera(uid, monitor);
}
monitor.ActiveCamera = null;
monitor.ActiveCameraAddress = string.Empty;
EntityManager.RemoveComponent<ActiveSurveillanceCameraMonitorComponent>(uid);
UpdateUserInterface(uid, monitor);
}
private void RefreshSubnets(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
monitor.KnownSubnets.Clear();
PingCameraNetwork(uid, monitor);
}
private void PingCameraNetwork(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraPingMessage }
};
_deviceNetworkSystem.QueuePacket(uid, null, payload);
}
private void SetActiveSubnet(EntityUid uid, string subnet,
SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| !monitor.KnownSubnets.ContainsKey(subnet))
{
return;
}
DisconnectFromSubnet(uid, monitor.ActiveSubnet);
DisconnectCamera(uid, true, monitor);
monitor.ActiveSubnet = subnet;
monitor.KnownCameras.Clear();
UpdateUserInterface(uid, monitor);
ConnectToSubnet(uid, subnet);
}
private void RequestActiveSubnetInfo(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var address))
{
return;
}
var payload = new NetworkPayload()
{
{DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraPingSubnetMessage},
};
_deviceNetworkSystem.QueuePacket(uid, address, payload);
}
private void ConnectToSubnet(EntityUid uid, string subnet, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| !monitor.KnownSubnets.TryGetValue(subnet, out var address))
{
return;
}
var payload = new NetworkPayload()
{
{DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraSubnetConnectMessage},
};
_deviceNetworkSystem.QueuePacket(uid, address, payload);
RequestActiveSubnetInfo(uid);
}
private void DisconnectFromSubnet(EntityUid uid, string subnet, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| !monitor.KnownSubnets.TryGetValue(subnet, out var address))
{
return;
}
var payload = new NetworkPayload()
{
{DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraSubnetDisconnectMessage},
};
_deviceNetworkSystem.QueuePacket(uid, address, payload);
}
// Adds a viewer to the camera and the monitor.
private void AddViewer(EntityUid uid, EntityUid player, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
monitor.Viewers.Add(player);
if (monitor.ActiveCamera != null)
{
_surveillanceCameras.AddActiveViewer(monitor.ActiveCamera.Value, player, uid);
}
UpdateUserInterface(uid, monitor, player);
}
// Removes a viewer from the camera and the monitor.
private void RemoveViewer(EntityUid uid, EntityUid player, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
monitor.Viewers.Remove(player);
if (monitor.ActiveCamera != null)
{
EntityUid? monitorUid = monitor.Viewers.Count == 0 ? uid : null;
_surveillanceCameras.RemoveActiveViewer(monitor.ActiveCamera.Value, player, monitorUid);
}
}
// Sets the camera. If the camera is not null, this will return.
//
// The camera should always attempt to switch over, rather than
// directly setting it, so that the active viewer list and view
// subscriptions can be updated.
private void SetCamera(EntityUid uid, EntityUid camera, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| monitor.ActiveCamera != null)
{
return;
}
_surveillanceCameras.AddActiveViewers(camera, monitor.Viewers, uid);
monitor.ActiveCamera = camera;
AddComp<ActiveSurveillanceCameraMonitorComponent>(uid);
UpdateUserInterface(uid, monitor);
}
// Switches the camera's viewers over to this new given camera.
private void SwitchCamera(EntityUid uid, EntityUid camera, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| monitor.ActiveCamera == null)
{
return;
}
_surveillanceCameras.SwitchActiveViewers(monitor.ActiveCamera.Value, camera, monitor.Viewers, uid);
monitor.ActiveCamera = camera;
UpdateUserInterface(uid, monitor);
}
private void TrySwitchCameraByAddress(EntityUid uid, string address,
SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var subnetAddress))
{
return;
}
var payload = new NetworkPayload()
{
{DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraConnectMessage},
{SurveillanceCameraSystem.CameraAddressData, address}
};
monitor.NextCameraAddress = address;
_deviceNetworkSystem.QueuePacket(uid, subnetAddress, payload);
}
// Attempts to switch over the current viewed camera on this monitor
// to the new camera.
private void TrySwitchCameraByUid(EntityUid uid, EntityUid newCamera, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
if (monitor.ActiveCamera == null)
{
SetCamera(uid, newCamera, monitor);
}
else
{
SwitchCamera(uid, newCamera, monitor);
}
}
private void RemoveActiveCamera(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| monitor.ActiveCamera == null)
{
return;
}
_surveillanceCameras.RemoveActiveViewers(monitor.ActiveCamera.Value, monitor.Viewers, uid);
UpdateUserInterface(uid, monitor);
}
// This is public primarily because it might be useful to have the ability to
// have this component added to any entity, and have them open the BUI (somehow).
public void AfterOpenUserInterface(EntityUid uid, EntityUid player, SurveillanceCameraMonitorComponent? monitor = null, ActorComponent? actor = null)
{
if (!Resolve(uid, ref monitor)
|| !Resolve(player, ref actor))
{
return;
}
AddViewer(uid, player);
}
private void UpdateUserInterface(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null, EntityUid? player = null)
{
if (!Resolve(uid, ref monitor))
{
return;
}
IPlayerSession? session = null;
if (player != null
&& TryComp(player, out ActorComponent? actor))
{
session = actor.PlayerSession;
}
var state = new SurveillanceCameraMonitorUiState(monitor.ActiveCamera, monitor.KnownSubnets.Keys.ToHashSet(), monitor.ActiveCameraAddress, monitor.ActiveSubnet, monitor.KnownCameras);
_userInterface.TrySetUiState(uid, SurveillanceCameraMonitorUiKey.Key, state);
}
}

View File

@@ -0,0 +1,268 @@
using Content.Server.Administration.Managers;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Ghost.Components;
using Content.Server.Power.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.DeviceNetwork;
using Content.Shared.SurveillanceCamera;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Server.SurveillanceCamera;
public sealed class SurveillanceCameraRouterSystem : EntitySystem
{
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
public override void Initialize()
{
SubscribeLocalEvent<SurveillanceCameraRouterComponent, ComponentInit>(OnInitialize);
SubscribeLocalEvent<SurveillanceCameraRouterComponent, DeviceNetworkPacketEvent>(OnPacketReceive);
SubscribeLocalEvent<SurveillanceCameraRouterComponent, SurveillanceCameraSetupSetNetwork>(OnSetNetwork);
SubscribeLocalEvent<SurveillanceCameraRouterComponent, GetVerbsEvent<AlternativeVerb>>(AddVerbs);
SubscribeLocalEvent<SurveillanceCameraRouterComponent, PowerChangedEvent>(OnPowerChanged);
}
private void OnInitialize(EntityUid uid, SurveillanceCameraRouterComponent router, ComponentInit args)
{
if (router.SubnetFrequencyId == null ||
!_prototypeManager.TryIndex(router.SubnetFrequencyId, out DeviceFrequencyPrototype? subnetFrequency))
{
return;
}
router.SubnetFrequency = subnetFrequency.Frequency;
router.Active = true;
}
private void OnPacketReceive(EntityUid uid, SurveillanceCameraRouterComponent router, DeviceNetworkPacketEvent args)
{
if (!router.Active
|| string.IsNullOrEmpty(args.SenderAddress)
|| !args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command))
{
return;
}
switch (command)
{
case SurveillanceCameraSystem.CameraConnectMessage:
if (!args.Data.TryGetValue(SurveillanceCameraSystem.CameraAddressData, out string? address))
{
return;
}
ConnectCamera(uid, args.SenderAddress, address, router);
break;
case SurveillanceCameraSystem.CameraHeartbeatMessage:
if (!args.Data.TryGetValue(SurveillanceCameraSystem.CameraAddressData, out string? camera))
{
return;
}
SendHeartbeat(uid, args.SenderAddress, camera, router);
break;
case SurveillanceCameraSystem.CameraSubnetConnectMessage:
AddMonitorToRoute(uid, args.SenderAddress, router);
PingSubnet(uid, router);
break;
case SurveillanceCameraSystem.CameraSubnetDisconnectMessage:
RemoveMonitorFromRoute(uid, args.SenderAddress, router);
break;
case SurveillanceCameraSystem.CameraPingSubnetMessage:
PingSubnet(uid, router);
break;
case SurveillanceCameraSystem.CameraPingMessage:
SubnetPingResponse(uid, args.SenderAddress, router);
break;
case SurveillanceCameraSystem.CameraDataMessage:
SendCameraInfo(uid, args.Data, router);
break;
}
}
private void OnPowerChanged(EntityUid uid, SurveillanceCameraRouterComponent component, PowerChangedEvent args)
{
component.MonitorRoutes.Clear();
component.Active = args.Powered;
}
private void AddVerbs(EntityUid uid, SurveillanceCameraRouterComponent component, GetVerbsEvent<AlternativeVerb> verbs)
{
if (!_actionBlocker.CanInteract(verbs.User, uid))
{
return;
}
if (component.SubnetFrequencyId != null)
{
return;
}
AlternativeVerb verb = new();
verb.Text = Loc.GetString("surveillance-camera-setup");
verb.Act = () => OpenSetupInterface(uid, verbs.User, component);
verbs.Verbs.Add(verb);
}
private void OnSetNetwork(EntityUid uid, SurveillanceCameraRouterComponent component,
SurveillanceCameraSetupSetNetwork args)
{
if (args.UiKey is not SurveillanceCameraSetupUiKey key
|| key != SurveillanceCameraSetupUiKey.Router)
{
return;
}
if (args.Network < 0 || args.Network >= component.AvailableNetworks.Count)
{
return;
}
if (!_prototypeManager.TryIndex<DeviceFrequencyPrototype>(component.AvailableNetworks[args.Network],
out var frequency))
{
return;
}
component.SubnetFrequencyId = component.AvailableNetworks[args.Network];
component.SubnetFrequency = frequency.Frequency;
component.Active = true;
UpdateSetupInterface(uid, component);
}
private void OpenSetupInterface(EntityUid uid, EntityUid player, SurveillanceCameraRouterComponent? camera = null, ActorComponent? actor = null)
{
if (!Resolve(uid, ref camera)
|| !Resolve(player, ref actor))
{
return;
}
_userInterface.GetUiOrNull(uid, SurveillanceCameraSetupUiKey.Router)!.Open(actor.PlayerSession);
UpdateSetupInterface(uid, camera);
}
private void UpdateSetupInterface(EntityUid uid, SurveillanceCameraRouterComponent? router = null, DeviceNetworkComponent? deviceNet = null)
{
if (!Resolve(uid, ref router, ref deviceNet))
{
return;
}
if (router.AvailableNetworks.Count == 0 || router.SubnetFrequencyId != null)
{
_userInterface.TryCloseAll(uid, SurveillanceCameraSetupUiKey.Router);
return;
}
var state = new SurveillanceCameraSetupBoundUiState(router.SubnetName, deviceNet.ReceiveFrequency ?? 0,
router.AvailableNetworks, true, router.SubnetFrequencyId != null);
_userInterface.TrySetUiState(uid, SurveillanceCameraSetupUiKey.Router, state);
}
private void SendHeartbeat(EntityUid uid, string origin, string destination,
SurveillanceCameraRouterComponent? router = null)
{
if (!Resolve(uid, ref router))
{
return;
}
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraHeartbeatMessage },
{ SurveillanceCameraSystem.CameraAddressData, origin }
};
_deviceNetworkSystem.QueuePacket(uid, destination, payload, router.SubnetFrequency);
}
private void SubnetPingResponse(EntityUid uid, string origin, SurveillanceCameraRouterComponent? router = null)
{
if (!Resolve(uid, ref router) || router.SubnetFrequencyId == null)
{
return;
}
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraSubnetData },
{ SurveillanceCameraSystem.CameraSubnetData, router.SubnetFrequencyId }
};
_deviceNetworkSystem.QueuePacket(uid, origin, payload);
}
private void ConnectCamera(EntityUid uid, string origin, string address, SurveillanceCameraRouterComponent? router = null)
{
if (!Resolve(uid, ref router))
{
return;
}
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraConnectMessage },
{ SurveillanceCameraSystem.CameraAddressData, origin }
};
_deviceNetworkSystem.QueuePacket(uid, address, payload, router.SubnetFrequency);
}
// Adds a monitor to the set of routes.
private void AddMonitorToRoute(EntityUid uid, string address, SurveillanceCameraRouterComponent? router = null)
{
if (!Resolve(uid, ref router))
{
return;
}
router.MonitorRoutes.Add(address);
}
private void RemoveMonitorFromRoute(EntityUid uid, string address, SurveillanceCameraRouterComponent? router = null)
{
if (!Resolve(uid, ref router))
{
return;
}
router.MonitorRoutes.Remove(address);
}
// Pings a subnet to get all camera information.
private void PingSubnet(EntityUid uid, SurveillanceCameraRouterComponent? router = null)
{
if (!Resolve(uid, ref router))
{
return;
}
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, SurveillanceCameraSystem.CameraPingMessage },
{ SurveillanceCameraSystem.CameraSubnetData, router.SubnetName }
};
_deviceNetworkSystem.QueuePacket(uid, null, payload, router.SubnetFrequency);
}
// Sends camera information to all monitors currently interested.
private void SendCameraInfo(EntityUid uid, NetworkPayload payload, SurveillanceCameraRouterComponent? router = null)
{
if (!Resolve(uid, ref router))
{
return;
}
foreach (var address in router.MonitorRoutes)
{
_deviceNetworkSystem.QueuePacket(uid, address, payload);
}
}
}

View File

@@ -0,0 +1,417 @@
using Content.Server.Administration.Managers;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Ghost.Components;
using Content.Server.Power.Components;
using Content.Shared.ActionBlocker;
using Content.Shared.DeviceNetwork;
using Content.Shared.SurveillanceCamera;
using Content.Shared.Verbs;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Server.SurveillanceCamera;
public sealed class SurveillanceCameraSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly ViewSubscriberSystem _viewSubscriberSystem = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
// Pings a surveillance camera subnet. All cameras will always respond
// with a data message if they are on the same subnet.
public const string CameraPingSubnetMessage = "surveillance_camera_ping_subnet";
// Pings a surveillance camera. Useful to ensure that the camera is still on
// before connecting fully.
public const string CameraPingMessage = "surveillance_camera_ping";
// Camera heartbeat. Monitors ping this to ensure that a camera is still able to
// be contacted. If this doesn't get sent after some time, the monitor will
// automatically disconnect.
public const string CameraHeartbeatMessage = "surveillance_camera_heartbeat";
// Surveillance camera data. This generally should contain nothing
// except for the subnet that this camera is on -
// this is because of the fact that the PacketEvent already
// contains the sender UID, and that this will always be targeted
// towards the sender that pinged the camera.
public const string CameraDataMessage = "surveillance_camera_data";
public const string CameraConnectMessage = "surveillance_camera_connect";
public const string CameraSubnetConnectMessage = "surveillance_camera_subnet_connect";
public const string CameraSubnetDisconnectMessage = "surveillance_camera_subnet_disconnect";
public const string CameraAddressData = "surveillance_camera_data_origin";
public const string CameraNameData = "surveillance_camera_data_name";
public const string CameraSubnetData = "surveillance_camera_data_subnet";
public const int CameraNameLimit = 32;
public override void Initialize()
{
SubscribeLocalEvent<SurveillanceCameraComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<SurveillanceCameraComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<SurveillanceCameraComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
SubscribeLocalEvent<SurveillanceCameraComponent, SurveillanceCameraSetupSetName>(OnSetName);
SubscribeLocalEvent<SurveillanceCameraComponent, SurveillanceCameraSetupSetNetwork>(OnSetNetwork);
SubscribeLocalEvent<SurveillanceCameraComponent, GetVerbsEvent<AlternativeVerb>>(AddVerbs);
}
private void OnPacketReceived(EntityUid uid, SurveillanceCameraComponent component, DeviceNetworkPacketEvent args)
{
if (!component.Active)
{
return;
}
if (!TryComp(uid, out DeviceNetworkComponent? deviceNet))
{
return;
}
if (args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command))
{
var payload = new NetworkPayload()
{
{ DeviceNetworkConstants.Command, string.Empty },
{ CameraAddressData, deviceNet.Address },
{ CameraNameData, component.CameraId },
{ CameraSubnetData, string.Empty }
};
var dest = string.Empty;
switch (command)
{
case CameraConnectMessage:
if (!args.Data.TryGetValue(CameraAddressData, out dest)
|| string.IsNullOrEmpty(args.Address))
{
return;
}
payload[DeviceNetworkConstants.Command] = CameraConnectMessage;
break;
case CameraHeartbeatMessage:
if (!args.Data.TryGetValue(CameraAddressData, out dest)
|| string.IsNullOrEmpty(args.Address))
{
return;
}
payload[DeviceNetworkConstants.Command] = CameraHeartbeatMessage;
break;
case CameraPingMessage:
if (!args.Data.TryGetValue(CameraSubnetData, out string? subnet))
{
return;
}
dest = args.SenderAddress;
payload[CameraSubnetData] = subnet;
payload[DeviceNetworkConstants.Command] = CameraDataMessage;
break;
}
_deviceNetworkSystem.QueuePacket(
uid,
dest,
payload);
}
}
private void AddVerbs(EntityUid uid, SurveillanceCameraComponent component, GetVerbsEvent<AlternativeVerb> verbs)
{
if (!_actionBlocker.CanInteract(verbs.User, uid))
{
return;
}
if (component.NameSet && component.NetworkSet)
{
return;
}
AlternativeVerb verb = new();
verb.Text = Loc.GetString("surveillance-camera-setup");
verb.Act = () => OpenSetupInterface(uid, verbs.User, component);
verbs.Verbs.Add(verb);
}
private void OnPowerChanged(EntityUid camera, SurveillanceCameraComponent component, PowerChangedEvent args)
{
SetActive(camera, args.Powered, component);
}
private void OnShutdown(EntityUid camera, SurveillanceCameraComponent component, ComponentShutdown args)
{
Deactivate(camera, component);
}
private void OnSetName(EntityUid uid, SurveillanceCameraComponent component, SurveillanceCameraSetupSetName args)
{
if (args.UiKey is not SurveillanceCameraSetupUiKey key
|| key != SurveillanceCameraSetupUiKey.Camera
|| string.IsNullOrEmpty(args.Name)
|| args.Name.Length > CameraNameLimit)
{
return;
}
component.CameraId = args.Name;
component.NameSet = true;
UpdateSetupInterface(uid, component);
}
private void OnSetNetwork(EntityUid uid, SurveillanceCameraComponent component,
SurveillanceCameraSetupSetNetwork args)
{
if (args.UiKey is not SurveillanceCameraSetupUiKey key
|| key != SurveillanceCameraSetupUiKey.Camera)
{
return;
}
if (args.Network < 0 || args.Network >= component.AvailableNetworks.Count)
{
return;
}
if (!_prototypeManager.TryIndex<DeviceFrequencyPrototype>(component.AvailableNetworks[args.Network],
out var frequency))
{
return;
}
_deviceNetworkSystem.SetReceiveFrequency(uid, frequency.Frequency);
component.NetworkSet = true;
UpdateSetupInterface(uid, component);
}
private void OpenSetupInterface(EntityUid uid, EntityUid player, SurveillanceCameraComponent? camera = null, ActorComponent? actor = null)
{
if (!Resolve(uid, ref camera)
|| !Resolve(player, ref actor))
{
return;
}
_userInterface.GetUiOrNull(uid, SurveillanceCameraSetupUiKey.Camera)!.Open(actor.PlayerSession);
UpdateSetupInterface(uid, camera);
}
private void UpdateSetupInterface(EntityUid uid, SurveillanceCameraComponent? camera = null, DeviceNetworkComponent? deviceNet = null)
{
if (!Resolve(uid, ref camera, ref deviceNet))
{
return;
}
if (camera.NameSet && camera.NetworkSet)
{
_userInterface.TryCloseAll(uid, SurveillanceCameraSetupUiKey.Camera);
return;
}
if (camera.AvailableNetworks.Count == 0)
{
if (deviceNet.ReceiveFrequencyId != null)
{
camera.AvailableNetworks.Add(deviceNet.ReceiveFrequencyId);
}
else if (!camera.NetworkSet)
{
_userInterface.TryCloseAll(uid, SurveillanceCameraSetupUiKey.Camera);
return;
}
}
var state = new SurveillanceCameraSetupBoundUiState(camera.CameraId, deviceNet.ReceiveFrequency ?? 0,
camera.AvailableNetworks, camera.NameSet, camera.NetworkSet);
_userInterface.TrySetUiState(uid, SurveillanceCameraSetupUiKey.Camera, state);
}
// If the camera deactivates for any reason, it must have all viewers removed,
// and the relevant event broadcast to all systems.
private void Deactivate(EntityUid camera, SurveillanceCameraComponent? component = null)
{
if (!Resolve(camera, ref component))
{
return;
}
var ev = new SurveillanceCameraDeactivateEvent(camera);
RemoveActiveViewers(camera, new(component.ActiveViewers), null, component);
component.Active = false;
// Send a targetted event to all monitors.
foreach (var monitor in component.ActiveMonitors)
{
RaiseLocalEvent(monitor, ev);
}
component.ActiveMonitors.Clear();
// Send a local event that's broadcasted everywhere afterwards.
RaiseLocalEvent(ev);
UpdateVisuals(camera, component);
}
public void SetActive(EntityUid camera, bool setting, SurveillanceCameraComponent? component = null)
{
if (!Resolve(camera, ref component))
{
return;
}
if (setting)
{
component.Active = setting;
}
else
{
Deactivate(camera, component);
}
UpdateVisuals(camera, component);
}
public void AddActiveViewer(EntityUid camera, EntityUid player, EntityUid? monitor = null, SurveillanceCameraComponent? component = null, ActorComponent? actor = null)
{
if (!Resolve(camera, ref component)
|| !component.Active
|| !Resolve(player, ref actor))
{
return;
}
_viewSubscriberSystem.AddViewSubscriber(camera, actor.PlayerSession);
component.ActiveViewers.Add(player);
if (monitor != null)
{
component.ActiveMonitors.Add(monitor.Value);
}
UpdateVisuals(camera, component);
}
public void AddActiveViewers(EntityUid camera, HashSet<EntityUid> players, EntityUid? monitor = null, SurveillanceCameraComponent? component = null)
{
if (!Resolve(camera, ref component) || !component.Active)
{
return;
}
foreach (var player in players)
{
AddActiveViewer(camera, player, monitor, component);
}
}
// Switch the set of active viewers from one camera to another.
public void SwitchActiveViewers(EntityUid oldCamera, EntityUid newCamera, HashSet<EntityUid> players, EntityUid? monitor = null, SurveillanceCameraComponent? oldCameraComponent = null, SurveillanceCameraComponent? newCameraComponent = null)
{
if (!Resolve(oldCamera, ref oldCameraComponent)
|| !Resolve(newCamera, ref newCameraComponent)
|| !oldCameraComponent.Active
|| !newCameraComponent.Active)
{
return;
}
if (monitor != null)
{
oldCameraComponent.ActiveMonitors.Remove(monitor.Value);
newCameraComponent.ActiveMonitors.Add(monitor.Value);
}
foreach (var player in players)
{
RemoveActiveViewer(oldCamera, player, null, oldCameraComponent);
AddActiveViewer(newCamera, player, null, newCameraComponent);
}
}
public void RemoveActiveViewer(EntityUid camera, EntityUid player, EntityUid? monitor = null, SurveillanceCameraComponent? component = null, ActorComponent? actor = null)
{
if (!Resolve(camera, ref component)
|| !Resolve(player, ref actor))
{
return;
}
_viewSubscriberSystem.RemoveViewSubscriber(camera, actor.PlayerSession);
component.ActiveViewers.Remove(player);
if (monitor != null)
{
component.ActiveMonitors.Remove(monitor.Value);
}
UpdateVisuals(camera, component);
}
public void RemoveActiveViewers(EntityUid camera, HashSet<EntityUid> players, EntityUid? monitor = null, SurveillanceCameraComponent? component = null)
{
if (!Resolve(camera, ref component))
{
return;
}
foreach (var player in players)
{
RemoveActiveViewer(camera, player, monitor, component);
}
}
private void UpdateVisuals(EntityUid camera, SurveillanceCameraComponent? component = null, AppearanceComponent? appearance = null)
{
// Don't log missing, because otherwise tests fail.
if (!Resolve(camera, ref component, ref appearance, false))
{
return;
}
var key = SurveillanceCameraVisuals.Disabled;
if (component.Active)
{
key = SurveillanceCameraVisuals.Active;
}
if (component.ActiveViewers.Count > 0 || component.ActiveMonitors.Count > 0)
{
key = SurveillanceCameraVisuals.InUse;
}
appearance.SetData(SurveillanceCameraVisualsKey.Key, key);
}
}
public sealed class OnSurveillanceCameraViewerAddEvent : EntityEventArgs
{
}
public sealed class OnSurveillanceCameraViewerRemoveEvent : EntityEventArgs
{
}
// What happens when a camera deactivates.
public sealed class SurveillanceCameraDeactivateEvent : EntityEventArgs
{
public EntityUid Camera { get; }
public SurveillanceCameraDeactivateEvent(EntityUid camera)
{
Camera = camera;
}
}

View File

@@ -0,0 +1,128 @@
using Robust.Shared.Serialization;
namespace Content.Shared.SurveillanceCamera;
// Camera monitor state. If the camera is null, there should be a blank
// space where the camera is.
[Serializable, NetSerializable]
public sealed class SurveillanceCameraMonitorUiState : BoundUserInterfaceState
{
// The active camera on the monitor. If this is null, the part of the UI
// that contains the monitor should clear.
public EntityUid? ActiveCamera { get; }
// Currently available subnets. Does not send the entirety of the possible
// cameras to view because that could be really, really large
public HashSet<string> Subnets { get; }
public string ActiveAddress;
// Currently active subnet.
public string ActiveSubnet { get; }
// Known cameras, by address and name.
public Dictionary<string, string> Cameras { get; }
public SurveillanceCameraMonitorUiState(EntityUid? activeCamera, HashSet<string> subnets, string activeAddress, string activeSubnet, Dictionary<string, string> cameras)
{
ActiveCamera = activeCamera;
Subnets = subnets;
ActiveAddress = activeAddress;
ActiveSubnet = activeSubnet;
Cameras = cameras;
}
}
[Serializable, NetSerializable]
public sealed class SurveillanceCameraMonitorSwitchMessage : BoundUserInterfaceMessage
{
public string Address { get; }
public SurveillanceCameraMonitorSwitchMessage(string address)
{
Address = address;
}
}
[Serializable, NetSerializable]
public sealed class SurveillanceCameraMonitorSubnetRequestMessage : BoundUserInterfaceMessage
{
public string Subnet { get; }
public SurveillanceCameraMonitorSubnetRequestMessage(string subnet)
{
Subnet = subnet;
}
}
// Sent when the user requests that the cameras on the current subnet be refreshed.
[Serializable, NetSerializable]
public sealed class SurveillanceCameraRefreshCamerasMessage : BoundUserInterfaceMessage
{}
// Sent when the user requests that the subnets known by the monitor be refreshed.
[Serializable, NetSerializable]
public sealed class SurveillanceCameraRefreshSubnetsMessage : BoundUserInterfaceMessage
{}
// Sent when the user wants to disconnect the monitor from the camera.
[Serializable, NetSerializable]
public sealed class SurveillanceCameraDisconnectMessage : BoundUserInterfaceMessage
{}
[Serializable, NetSerializable]
public enum SurveillanceCameraMonitorUiKey : byte
{
Key
}
// SETUP
[Serializable, NetSerializable]
public sealed class SurveillanceCameraSetupBoundUiState : BoundUserInterfaceState
{
public string Name { get; }
public uint Network { get; }
public List<string> Networks { get; }
public bool NameDisabled { get; }
public bool NetworkDisabled { get; }
public SurveillanceCameraSetupBoundUiState(string name, uint network, List<string> networks, bool nameDisabled, bool networkDisabled)
{
Name = name;
Network = network;
Networks = networks;
NameDisabled = nameDisabled;
NetworkDisabled = networkDisabled;
}
}
[Serializable, NetSerializable]
public sealed class SurveillanceCameraSetupSetName : BoundUserInterfaceMessage
{
public string Name { get; }
public SurveillanceCameraSetupSetName(string name)
{
Name = name;
}
}
[Serializable, NetSerializable]
public sealed class SurveillanceCameraSetupSetNetwork : BoundUserInterfaceMessage
{
public int Network { get; }
public SurveillanceCameraSetupSetNetwork(int network)
{
Network = network;
}
}
[Serializable, NetSerializable]
public enum SurveillanceCameraSetupUiKey : byte
{
Camera,
Router
}

View File

@@ -0,0 +1,22 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.SurveillanceCamera;
[Serializable, NetSerializable]
public enum SurveillanceCameraVisualsKey : byte
{
Key,
Layer
}
[Serializable, NetSerializable]
public enum SurveillanceCameraVisuals : byte
{
Active,
InUse,
Disabled,
// Reserved for future use
Xray,
Emp
}

View File

@@ -3,6 +3,18 @@ device-frequency-prototype-name-atmos = Atmospheric Devices
device-frequency-prototype-name-suit-sensors = Suit Sensors device-frequency-prototype-name-suit-sensors = Suit Sensors
device-frequency-prototype-name-lights = Smart Lights device-frequency-prototype-name-lights = Smart Lights
## camera frequencies
device-frequency-prototype-name-surveillance-camera-test = Subnet Test
device-frequency-prototype-name-surveillance-camera-engineering = Engineering Cameras
device-frequency-prototype-name-surveillance-camera-security = Security Cameras
device-frequency-prototype-name-surveillance-camera-science = Science Cameras
device-frequency-prototype-name-surveillance-camera-supply = Supply Cameras
device-frequency-prototype-name-surveillance-camera-command = Command Cameras
device-frequency-prototype-name-surveillance-camera-service = Service Cameras
device-frequency-prototype-name-surveillance-camera-medical = Medical Cameras
device-frequency-prototype-name-surveillance-camera-general = General Cameras
device-frequency-prototype-name-surveillance-camera-entertainment = Entertainment Cameras
# prefixes for randomly generated device addresses # prefixes for randomly generated device addresses
device-address-prefix-vent = Vnt- device-address-prefix-vent = Vnt-
device-address-prefix-scrubber = Scr- device-address-prefix-scrubber = Scr-

View File

@@ -0,0 +1,12 @@
surveillance-camera-monitor-ui-window = Camera monitor
surveillance-camera-monitor-ui-refresh-cameras = Refresh cameras
surveillance-camera-monitor-ui-refresh-subnets = Refresh subnets
surveillance-camera-monitor-ui-disconnect = Disconnect
surveillance-camera-monitor-ui-status = {$status} {$address}
surveillance-camera-monitor-ui-status-connecting = Connecting:
surveillance-camera-monitor-ui-status-connected = Connected:
surveillance-camera-monitor-ui-status-disconnected = Disconnected
surveillance-camera-monitor-ui-no-subnets = No Subnets
surveillance-camera-setup = Setup
surveillance-camera-setup-ui-set = Set

View File

@@ -277,6 +277,10 @@
- OreProcessorMachineCircuitboard - OreProcessorMachineCircuitboard
- CircuitImprinterMachineCircuitboard - CircuitImprinterMachineCircuitboard
- UniformPrinterMachineCircuitboard - UniformPrinterMachineCircuitboard
- SurveillanceCameraRouterCircuitboard
- SurveillanceCameraWirelessRouterCircuitboard
- SurveillanceWirelessCameraMovableCircuitboard
- SurveillanceWirelessCameraAnchoredCircuitboard
- AirAlarmElectronics - AirAlarmElectronics
- FireAlarmElectronics - FireAlarmElectronics

View File

@@ -1,3 +1,53 @@
- type: deviceFrequency
id: SurveillanceCamera
name: device-frequency-prototype-name-surveillance-camera
frequency: 1926 # i only have so many of these, y'know?
- type: deviceFrequency
id: SurveillanceCameraEngineering
name: device-frequency-prototype-name-surveillance-camera-engineering
frequency: 1931
- type: deviceFrequency
id: SurveillanceCameraSecurity
name: device-frequency-prototype-name-surveillance-camera-security
frequency: 1932
- type: deviceFrequency
id: SurveillanceCameraScience
name: device-frequency-prototype-name-surveillance-camera-science
frequency: 1933
- type: deviceFrequency
id: SurveillanceCameraSupply
name: device-frequency-prototype-name-surveillance-camera-supply
frequency: 1934
- type: deviceFrequency
id: SurveillanceCameraCommand
name: device-frequency-prototype-name-surveillance-camera-command
frequency: 1935
- type: deviceFrequency
id: SurveillanceCameraService
name: device-frequency-prototype-name-surveillance-camera-service
frequency: 1936
- type: deviceFrequency
id: SurveillanceCameraMedical
name: device-frequency-prototype-name-surveillance-camera-medical
frequency: 1937
- type: deviceFrequency
id: SurveillanceCameraGeneral
name: device-frequency-prototype-name-surveillance-camera-general
frequency: 1938
- type: deviceFrequency
id: SurveillanceCameraEntertainment
name: device-frequency-prototype-name-surveillance-camera-entertainment
frequency: 1939
- type: deviceFrequency - type: deviceFrequency
id: AtmosMonitor id: AtmosMonitor
name: device-frequency-prototype-name-atmos name: device-frequency-prototype-name-atmos

View File

@@ -396,3 +396,56 @@
materialRequirements: materialRequirements:
Glass: 2 Glass: 2
Cable: 2 Cable: 2
- type: entity
id: SurveillanceCameraRouterCircuitboard
parent: BaseMachineCircuitboard
name: Surveillance camera router board
description: A machine printed circuit board for a surveillance camera router
components:
- type: MachineBoard
prototype: SurveillanceCameraRouterConstructed
requirements:
Capacitor: 4
- type: entity
id: SurveillanceCameraWirelessRouterCircuitboard
parent: BaseMachineCircuitboard
name: Surveillance camera wireless router board
description: A machine printed circuit board for a surveillance camera wireless router
components:
- type: MachineBoard
prototype: SurveillanceCameraWirelessRouterConstructed
requirements:
Laser: 1
Capacitor: 4
- type: entity
id: SurveillanceWirelessCameraMovableCircuitboard
parent: BaseMachineCircuitboard
name: movable wireless camera board
description: A machine printed circuit board for a movable wireless camera
components:
- type: MachineBoard
prototype: SurveillanceWirelessCameraMovableConstructed
requirements:
Laser: 1
Capacitor: 4
materialRequirements:
Glass: 2
Cable: 2
- type: entity
id: SurveillanceWirelessCameraAnchoredCircuitboard
parent: BaseMachineCircuitboard
name: wireless camera board
description: A machine printed circuit board for a wireless camera
components:
- type: MachineBoard
prototype: SurveillanceWirelessCameraAnchoredConstructed
requirements:
Laser: 1
Capacitor: 4
materialRequirements:
Glass: 2
Cable: 2

View File

@@ -66,6 +66,24 @@
- type: ComputerBoard - type: ComputerBoard
prototype: ComputerSupplyRequest prototype: ComputerSupplyRequest
- type: entity
parent: BaseComputerCircuitboard
id: SurveillanceCameraMonitorCircuitboard
name: surveillance camera monitor board
description: A computer printed circuit board for a surveillance camera monitor
components:
- type: ComputerBoard
prototype: ComputerSurveillanceCameraMonitor
- type: entity
parent: BaseComputerCircuitboard
id: SurveillanceWirelessCameraMonitorCircuitboard
name: surveillance wireless camera monitor board
description: A computer printed circuit board for a surveillance wireless camera monitor
components:
- type: ComputerBoard
prototype: ComputerSurveillanceWirelessCameraMonitor
- type: entity - type: entity
parent: BaseComputerCircuitboard parent: BaseComputerCircuitboard
id: PowerMonitoringComputerCircuitboard id: PowerMonitoringComputerCircuitboard

View File

@@ -399,30 +399,99 @@
color: "#b89f25" color: "#b89f25"
- type: entity - type: entity
parent: BaseStructure parent: ComputerBase
id: ComputerSurveillanceCameraMonitor
name: camera monitor
description: A surveillance camera monitor. You're watching them. Maybe.
components:
- type: Appearance
visuals:
- type: ComputerVisualizer
key: tech_key
screen: cameras
- type: Computer
board: SurveillanceCameraMonitorCircuitboard
- type: DeviceNetwork
deviceNetId: Wired
receiveFrequencyId: SurveillanceCamera
transmitFrequencyId: SurveillanceCamera
- type: WiredNetworkConnection
- type: SurveillanceCameraMonitor
- type: ActivatableUI
key: enum.SurveillanceCameraMonitorUiKey.Key
- type: ActivatableUIRequiresPower
- type: Transform
anchored: true
- type: UserInterface
interfaces:
- key: enum.SurveillanceCameraMonitorUiKey.Key
type: SurveillanceCameraMonitorBoundUserInterface
- type: entity
parent: ComputerBase
id: ComputerSurveillanceWirelessCameraMonitor
name: wireless camera monitor
description: A wireless surveillance camera monitor. You're watching them. Maybe.
components:
- type: Appearance
visuals:
- type: ComputerVisualizer
key: tech_key
screen: cameras
- type: Computer
board: SurveillanceWirelessCameraMonitorCircuitboard
- type: DeviceNetwork
deviceNetId: Wireless
receiveFrequencyId: SurveillanceCamera
transmitFrequencyId: SurveillanceCamera
- type: WirelessNetworkConnection
range: 200
- type: SurveillanceCameraMonitor
- type: ActivatableUI
key: enum.SurveillanceCameraMonitorUiKey.Key
- type: ActivatableUIRequiresPower
- type: Transform
anchored: true
- type: UserInterface
interfaces:
- key: enum.SurveillanceCameraMonitorUiKey.Key
type: SurveillanceCameraMonitorBoundUserInterface
- type: entity
parent: ComputerSurveillanceWirelessCameraMonitor
id: ComputerTelevision id: ComputerTelevision
name: wooden television name: wooden television
description: It's an old television displaying the station's cameras, if they worked. description: Finally, some decent reception around here...
components: components:
- type: InteractionOutline - type: Appearance
- type: Sprite visuals:
sprite: Structures/Machines/computers.rsi - type: ComputerVisualizer
layers: body: television
- state: television bodyBroken: television_broken
- state: detective_television screen: detective_television
shader: unshaded - type: Sprite
visuals: sprite: Structures/Machines/computers.rsi
- type: PointLight layers:
radius: 1.5 - map: [ "enum.ComputerVisualizer+Layers.KeyboardOn" ]
energy: 1.6 visible: false
color: "#b89f25" - map: [ "enum.ComputerVisualizer+Layers.Keyboard" ]
- type: Fixtures visible: false
fixtures: - map: [ "enum.ComputerVisualizer+Layers.Body"]
- shape: state: television
!type:PhysShapeAabb - map: [ "enum.ComputerVisualizer+Layers.Screen" ]
bounds: "-0.25,-0.25,0.25,0.25" state: detective_television
mass: 50 shader: unshaded
mask: - type: PointLight
- TabletopMachineMask radius: 1.5
layer: energy: 1.6
- TabletopMachineLayer color: "#b89f25"
- type: Fixtures
fixtures:
- shape:
!type:PhysShapeAabb
bounds: "-0.25,-0.25,0.25,0.25"
mass: 50
mask:
- TabletopMachineMask
layer:
- TabletopMachineLayer

View File

@@ -250,6 +250,7 @@
- DiagnoserMachineCircuitboard - DiagnoserMachineCircuitboard
- ChemMasterMachineCircuitboard - ChemMasterMachineCircuitboard
- ChemDispenserMachineCircuitboard - ChemDispenserMachineCircuitboard
- SurveillanceCameraRouterCircuitboard
- HydroponicsTrayMachineCircuitboard - HydroponicsTrayMachineCircuitboard
- SolarControlComputerCircuitboard - SolarControlComputerCircuitboard
- AutolatheMachineCircuitboard - AutolatheMachineCircuitboard

View File

@@ -0,0 +1,148 @@
- type: entity
abstract: true
parent: BaseMachinePowered
id: SurveillanceCameraRouterBase
name: camera router
description: A surveillance camera router. It routes. Perhaps.
components:
- type: DeviceNetwork
deviceNetId: Wired
receiveFrequencyId: SurveillanceCamera
transmitFrequencyId: SurveillanceCamera
- type: WiredNetworkConnection
- type: UserInterface
interfaces:
- key: enum.SurveillanceCameraSetupUiKey.Router
type: SurveillanceCameraSetupBoundUi
- type: Construction
graph: Machine
node: machine
- type: Machine
board: SurveillanceCameraRouterCircuitboard
- type: Sprite
sprite: Structures/Machines/server.rsi
layers:
- state: server
- type: entity
parent: SurveillanceCameraRouterBase
id: SurveillanceCameraRouterConstructed
suffix: Constructed
components:
- type: SurveillanceCameraRouter
setupAvailableNetworks:
- SurveillanceCameraEngineering
- SurveillanceCameraSecurity
- SurveillanceCameraService
- SurveillanceCameraSupply
- SurveillanceCameraScience
- SurveillanceCameraGeneral
- SurveillanceCameraMedical
- SurveillanceCameraCommand
- type: entity
parent: SurveillanceCameraRouterBase
id: SurveillanceCameraRouterEngineering
suffix: Engineering
components:
- type: SurveillanceCameraRouter
subnetFrequency: SurveillanceCameraEngineering
- type: entity
parent: SurveillanceCameraRouterBase
id: SurveillanceCameraRouterSecurity
suffix: Security
components:
- type: SurveillanceCameraRouter
subnetFrequency: SurveillanceCameraSecurity
- type: entity
parent: SurveillanceCameraRouterBase
id: SurveillanceCameraRouterScience
suffix: Science
components:
- type: SurveillanceCameraRouter
subnetFrequency: SurveillanceCameraScience
- type: entity
parent: SurveillanceCameraRouterBase
id: SurveillanceCameraRouterSupply
suffix: Supply
components:
- type: SurveillanceCameraRouter
subnetFrequency: SurveillanceCameraSupply
- type: entity
parent: SurveillanceCameraRouterBase
id: SurveillanceCameraRouterCommand
suffix: Command
components:
- type: SurveillanceCameraRouter
subnetFrequency: SurveillanceCameraCommand
- type: entity
parent: SurveillanceCameraRouterBase
id: SurveillanceCameraRouterService
suffix: Service
components:
- type: SurveillanceCameraRouter
subnetFrequency: SurveillanceCameraService
- type: entity
parent: SurveillanceCameraRouterBase
id: SurveillanceCameraRouterMedical
suffix: Medical
components:
- type: SurveillanceCameraRouter
subnetFrequency: SurveillanceCameraMedical
- type: entity
parent: SurveillanceCameraRouterBase
id: SurveillanceCameraRouterGeneral
suffix: General
components:
- type: SurveillanceCameraRouter
subnetFrequency: SurveillanceCameraGeneral
- type: entity
parent: BaseMachinePowered
id: SurveillanceCameraWirelessRouterBase
name: wireless camera router
description: A wireless surveillance camera router. It routes. Perhaps.
components:
- type: DeviceNetwork
deviceNetId: Wireless
receiveFrequencyId: SurveillanceCamera
transmitFrequencyId: SurveillanceCamera
- type: WirelessNetworkConnection
range: 200
- type: UserInterface
interfaces:
- key: enum.SurveillanceCameraSetupUiKey.Router
type: SurveillanceCameraSetupBoundUi
- type: Construction
graph: Machine
node: machine
- type: Machine
board: SurveillanceCameraWirelessRouterCircuitboard
- type: Sprite
sprite: Structures/Machines/server.rsi
layers:
- state: server
- type: entity
parent: SurveillanceCameraWirelessRouterBase
id: SurveillanceCameraWirelessRouterConstructed
suffix: Constructed
components:
- type: SurveillanceCameraRouter
setupAvailableNetworks:
- SurveillanceCameraEntertainment
- type: entity
parent: SurveillanceCameraWirelessRouterBase
id: SurveillanceCameraWirelessRouterEntertainment
suffix: Entertainment
components:
- type: SurveillanceCameraRouter
subnetFrequency: SurveillanceCameraEntertainment

View File

@@ -0,0 +1,117 @@
- type: entity
abstract: true
parent: BaseStructureDynamic
id: SurveillanceWirelessCameraBase
name: wireless camera
description: A camera. It's watching you. Kinda.
components:
- type: Construction
graph: Machine
node: machine
- type: InteractionOutline
- type: Eye
- type: WirelessNetworkConnection
range: 200
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
- type: Rotatable
rotateWhileAnchored: true
- type: Fixtures
fixtures:
- shape:
!type:PhysShapeCircle
radius: 0.45
mass: 50
mask:
- MachineMask
- type: UserInterface
interfaces:
- key: enum.SurveillanceCameraSetupUiKey.Camera
type: SurveillanceCameraSetupBoundUi
placement:
mode: SnapgridCenter
- type: entity
abstract: true
id: SurveillanceWirelessCameraAnchoredBase
parent: SurveillanceWirelessCameraBase
suffix: Anchored
components:
- type: Machine
board: SurveillanceWirelessCameraAnchoredCircuitboard
- type: Anchorable
- type: Transform
anchored: true
- type: Sprite
noRot: true
sprite: Structures/monitors.rsi
layers:
- map: [ "enum.SurveillanceCameraVisualsKey.Key" ]
state: television
- type: entity
abstract: true
id: SurveillanceWirelessCameraMovableBase
parent: SurveillanceWirelessCameraBase
suffix: Movable
components:
- type: Machine
board: SurveillanceWirelessCameraMovableCircuitboard
- type: Transform
- type: Sprite
noRot: true
sprite: Structures/monitors.rsi
layers:
- map: [ "enum.SurveillanceCameraVisualsKey.Key" ]
state: mobilevision
- type: entity
parent: SurveillanceWirelessCameraAnchoredBase
suffix: Constructed, Anchored
id: SurveillanceWirelessCameraAnchoredConstructed
components:
- type: DeviceNetwork
deviceNetId: Wireless
receiveFrequencyId: SurveillanceCamera
transmitFrequencyId: SurveillanceCamera
- type: SurveillanceCamera
setupAvailableNetworks:
- SurveillanceCameraEntertainment
- type: entity
parent: SurveillanceWirelessCameraMovableBase
suffix: Constructed, Movable
id: SurveillanceWirelessCameraMovableConstructed
components:
- type: DeviceNetwork
deviceNetId: Wireless
receiveFrequencyId: SurveillanceCameraEntertainment
transmitFrequencyId: SurveillanceCamera
- type: SurveillanceCamera
setupAvailableNetworks:
- SurveillanceCameraEntertainment
- type: entity
parent: SurveillanceWirelessCameraAnchoredBase
suffix: Entertainment, Anchored
id: SurveillanceWirelessCameraAnchoredEntertainment
components:
- type: DeviceNetwork
deviceNetId: Wireless
receiveFrequencyId: SurveillanceCameraEntertainment
transmitFrequencyId: SurveillanceCamera
- type: SurveillanceCamera
networkSet: true
- type: entity
parent: SurveillanceWirelessCameraMovableBase
suffix: Entertainment, Movable
id: SurveillanceWirelessCameraMovableEntertainment
components:
- type: DeviceNetwork
deviceNetId: Wireless
receiveFrequencyId: SurveillanceCameraEntertainment
transmitFrequencyId: SurveillanceCamera
- type: SurveillanceCamera
networkSet: true

View File

@@ -0,0 +1,212 @@
- type: entity
abstract: true
id: SurveillanceCameraBase
name: camera
description: A surveillance camera. It's watching you. Kinda.
components:
- type: Clickable
- type: InteractionOutline
- type: Construction
graph: SurveillanceCamera
node: camera
- type: Electrified
enabled: false
usesApcPower: true
- type: WallMount
- type: ApcPowerReceiver
- type: ExtensionCableReceiver
- type: Eye
- type: WiredNetworkConnection
- type: Transform
anchored: true
- type: Wires
alwaysRandomize: true
LayoutId: SurveillanceCamera
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
- type: Sprite
drawdepth: WallMountedItems
sprite: Structures/Wallmounts/camera.rsi
layers:
- map: [ "enum.SurveillanceCameraVisualsKey.Layer" ]
state: camera
- type: Appearance
- type: SurveillanceCameraVisuals
sprites:
Active: camera
Disabled: camera_off
InUse: camera_in_use
- type: UserInterface
interfaces:
- key: enum.SurveillanceCameraSetupUiKey.Camera
type: SurveillanceCameraSetupBoundUi
- key: enum.WiresUiKey.Key
type: WiresBoundUserInterface
placement:
mode: SnapgridCenter
snap:
- Wallmount
- type: entity
parent: SurveillanceCameraBase
id: SurveillanceCameraConstructed
name: camera
suffix: Constructed
components:
- type: DeviceNetwork
deviceNetId: Wired
transmitFrequencyId: SurveillanceCamera
- type: SurveillanceCamera
setupAvailableNetworks:
- SurveillanceCameraEngineering
- SurveillanceCameraSecurity
- SurveillanceCameraService
- SurveillanceCameraSupply
- SurveillanceCameraScience
- SurveillanceCameraGeneral
- SurveillanceCameraMedical
- SurveillanceCameraCommand
- type: entity
parent: SurveillanceCameraBase
id: SurveillanceCameraEngineering
name: camera
suffix: Engineering
components:
- type: DeviceNetwork
deviceNetId: Wired
receiveFrequencyId: SurveillanceCameraEngineering
transmitFrequencyId: SurveillanceCamera
- type: SurveillanceCamera
networkSet: true
- type: entity
parent: SurveillanceCameraBase
id: SurveillanceCameraSecurity
name: camera
suffix: Security
components:
- type: DeviceNetwork
deviceNetId: Wired
receiveFrequencyId: SurveillanceCameraSecurity
transmitFrequencyId: SurveillanceCamera
- type: SurveillanceCamera
networkSet: true
- type: entity
parent: SurveillanceCameraBase
id: SurveillanceCameraScience
name: camera
suffix: Science
components:
- type: DeviceNetwork
deviceNetId: Wired
receiveFrequencyId: SurveillanceCameraScience
transmitFrequencyId: SurveillanceCamera
- type: SurveillanceCamera
networkSet: true
- type: entity
parent: SurveillanceCameraBase
id: SurveillanceCameraSupply
name: camera
suffix: Supply
components:
- type: DeviceNetwork
deviceNetId: Wired
receiveFrequencyId: SurveillanceCameraSupply
transmitFrequencyId: SurveillanceCamera
- type: SurveillanceCamera
networkSet: true
- type: entity
parent: SurveillanceCameraBase
id: SurveillanceCameraScience
name: camera
suffix: Science
components:
- type: DeviceNetwork
deviceNetId: Wired
receiveFrequencyId: SurveillanceCameraScience
transmitFrequencyId: SurveillanceCamera
- type: SurveillanceCamera
networkSet: true
- type: entity
parent: SurveillanceCameraBase
id: SurveillanceCameraCommand
name: camera
suffix: Command
components:
- type: DeviceNetwork
deviceNetId: Wired
receiveFrequencyId: SurveillanceCameraCommand
transmitFrequencyId: SurveillanceCamera
- type: SurveillanceCamera
networkSet: true
- type: entity
parent: SurveillanceCameraBase
id: SurveillanceCameraService
name: camera
suffix: Service
components:
- type: DeviceNetwork
deviceNetId: Wired
receiveFrequencyId: SurveillanceCameraService
transmitFrequencyId: SurveillanceCamera
- type: SurveillanceCamera
networkSet: true
- type: entity
parent: SurveillanceCameraBase
id: SurveillanceCameraMedical
name: camera
suffix: Medical
components:
- type: DeviceNetwork
deviceNetId: Wired
receiveFrequencyId: SurveillanceCameraMedical
transmitFrequencyId: SurveillanceCamera
- type: SurveillanceCamera
networkSet: true
- type: entity
parent: SurveillanceCameraBase
id: SurveillanceCameraGeneral
name: camera
suffix: General
components:
- type: DeviceNetwork
deviceNetId: Wired
receiveFrequencyId: SurveillanceCameraGeneral
transmitFrequencyId: SurveillanceCamera
- type: SurveillanceCamera
networkSet: true
- type: entity
id: SurveillanceCameraAssembly
name: camera
description: A surveillance camera. Doesn't seem to be watching anybody any time soon. Probably.
components:
- type: Clickable
- type: InteractionOutline
- type: Construction
graph: SurveillanceCamera
node: assembly
- type: WallMount
- type: Transform
anchored: true
- type: Damageable
damageContainer: Inorganic
damageModifierSet: Metallic
- type: Sprite
drawdepth: WallMountedItems
sprite: Structures/Wallmounts/camera.rsi
layers:
- state: camera_off
placement:
mode: SnapgridCenter
snap:
- Wallmount

View File

@@ -0,0 +1,57 @@
- type: constructionGraph
id: SurveillanceCamera
start: start
graph:
- node: start
edges:
- to: assembly
steps:
- material: Steel
amount: 2
doAfter: 2.0
- node: assembly
entity: SurveillanceCameraAssembly
edges:
- to: wired
steps:
- material: Cable
amount: 1
doAfter: 1
- to: start
completed:
- !type:SpawnPrototype
prototype: SheetSteel1
amount: 2
- !type:DeleteEntity {}
steps:
- tool: Welding
doAfter: 2
- node: wired
entity: SurveillanceCameraAssembly
edges:
- to: camera
steps:
- tool: Screwing
doAfter: 2
# once a camera is constructed, it will
# instead just require wire cutting + welding
# to immediately deconstruct
- node: camera
entity: SurveillanceCameraConstructed
edges:
- to: start
conditions:
- !type:AllWiresCut {}
- !type:WirePanel {}
completed:
- !type:SpawnPrototype
prototype: SheetSteel1
amount: 2
- !type:DeleteEntity {}
steps:
- tool: Welding
doAfter: 2

View File

@@ -1,3 +1,17 @@
- type: construction
name: camera
id: camera
graph: SurveillanceCamera
startNode: start
targetNode: camera
category: Utilities
description: "Surveillance camera. It's watching. Soon."
icon:
sprite: Structures/Wallmounts/camera.rsi
state: camera
objectType: Structure
placementMode: SnapgridCenter
# POWER # POWER
- type: construction - type: construction
name: APC name: APC
@@ -457,4 +471,4 @@
icon: icon:
sprite: Structures/Piping/Atmospherics/gasmixer.rsi sprite: Structures/Piping/Atmospherics/gasmixer.rsi
state: gasMixer state: gasMixer

View File

@@ -294,3 +294,39 @@
materials: materials:
Steel: 100 Steel: 100
Glass: 900 Glass: 900
- type: latheRecipe
id: SurveillanceCameraRouterCircuitboard
icon: Objects/Misc/module.rsi/id_mod.png
result: SurveillanceCameraRouterCircuitboard
completetime: 4
materials:
Steel: 100
Glass: 900
- type: latheRecipe
id: SurveillanceCameraWirelessRouterCircuitboard
icon: Objects/Misc/module.rsi/id_mod.png
result: SurveillanceCameraWirelessRouterCircuitboard
completetime: 4
materials:
Steel: 100
Glass: 900
- type: latheRecipe
id: SurveillanceWirelessCameraAnchoredCircuitboard
icon: Objects/Misc/module.rsi/id_mod.png
result: SurveillanceWirelessCameraAnchoredCircuitboard
completetime: 4
materials:
Steel: 100
Glass: 900
- type: latheRecipe
id: SurveillanceWirelessCameraMovableCircuitboard
icon: Objects/Misc/module.rsi/id_mod.png
result: SurveillanceWirelessCameraMovableCircuitboard
completetime: 4
materials:
Steel: 100
Glass: 900

View File

@@ -31,6 +31,11 @@
positionInput: 0,0 positionInput: 0,0
life: 0 life: 0
- type: shader
id: CameraStatic
kind: source
path: "/Textures/Shaders/camera_static.swsl"
- type: shader - type: shader
id: Texture id: Texture
kind: source kind: source

View File

@@ -66,3 +66,9 @@
- !type:ArcadeOverflowWireAction - !type:ArcadeOverflowWireAction
- !type:ArcadePlayerInvincibleWireAction - !type:ArcadePlayerInvincibleWireAction
- !type:ArcadeEnemyInvincibleWireAction - !type:ArcadeEnemyInvincibleWireAction
- type: wireLayout
id: SurveillanceCamera
dummyWires: 4
wires:
- !type:PowerWireAction

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@@ -0,0 +1,40 @@
// Credited to PelicanPolice on Shadertoy
// Free for any purpose, commercial or otherwise.
// But do post here so I can see where it got used!
// If using on shadertoy, do link to this project on yours :)
highp float noise(highp vec2 pos, highp float evolve) {
// Loop the evolution (over a very long period of time).
highp float e = fract((evolve*0.01));
// Coordinates
highp float cx = pos.x*e;
highp float cy = sin(pos.y*e * 376.0); // is this close enough to a 60hz interference? :smol:
highp float v2_2 = cx*2.4/cy*23.0+pow(abs(cy/22.4),3.3);
highp float v2_1 = fract(fract(v2_2));
highp float v2 = fract(2.0/v2_1);
highp float v3 = fract(cx*evolve/pow(abs(cy),0.050));
// Generate a "random" black or white value
return fract(0.1*v2*v3);
}
void fragment() {
highp vec2 coords = FRAGCOORD.xy;
// Increase this number to test performance
int intensity = 1;
highp vec3 colour;
for (int i = 0; i < intensity; i++)
{
// Generate a black to white pixel
colour = vec3(noise(coords,TIME));
}
// Output to screen
COLOR = vec4(colour,1.0);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 894 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

View File

@@ -0,0 +1 @@
{"version": 1, "size": {"x": 32, "y": 32}, "license": "CC-BY-SA-3.0", "copyright": "Taken from /tg/station at https://github.com/tgstation/tgstation/commit/6bfe3b2e4fcbcdac9159dc4f0327a82ddf05ba7bi", "states": [{"name": "camera", "directions": 8, "delays": [[0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3]]}, {"name": "camera_assembly", "directions": 8, "delays": [[1.0], [1.0], [1.0], [1.0], [1.0], [1.0], [1.0], [1.0]]}, {"name": "camera_emp", "directions": 8, "delays": [[0.2, 0.2, 0.1, 0.1], [0.2, 0.2, 0.1, 0.1], [0.2, 0.2, 0.1, 0.1], [0.2, 0.2, 0.1, 0.1], [0.2, 0.2, 0.1, 0.1], [0.2, 0.2, 0.1, 0.1], [0.2, 0.2, 0.1, 0.1], [0.2, 0.2, 0.1, 0.1]]}, {"name": "camera_in_use", "directions": 8, "delays": [[0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3]]}, {"name": "camera_off", "directions": 8, "delays": [[1.0], [1.0], [1.0], [1.0], [1.0], [1.0], [1.0], [1.0]]}, {"name": "cameracase", "directions": 1, "delays": [[1.0]]}, {"name": "xraycamera", "directions": 8, "delays": [[0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3]]}, {"name": "xraycamera_assembly", "directions": 8, "delays": [[1.0], [1.0], [1.0], [1.0], [1.0], [1.0], [1.0], [1.0]]}, {"name": "xraycamera_emp", "directions": 8, "delays": [[0.2, 0.2, 0.1, 0.1], [0.2, 0.2, 0.1, 0.1], [0.2, 0.2, 0.1, 0.1], [0.2, 0.2, 0.1, 0.1], [0.2, 0.2, 0.1, 0.1], [0.2, 0.2, 0.1, 0.1], [0.2, 0.2, 0.1, 0.1], [0.2, 0.2, 0.1, 0.1]]}, {"name": "xraycamera_in_use", "directions": 8, "delays": [[0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3], [0.3, 0.3, 0.3, 0.3, 0.3, 0.3, 0.3]]}, {"name": "xraycamera_off", "directions": 8, "delays": [[1.0], [1.0], [1.0], [1.0], [1.0], [1.0], [1.0], [1.0]]}]}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1023 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,16 @@
{
"version": 1,
"size": {"x": 32, "y": 32},
"license": "From Goonstation at https://github.com/goonstation/goonstation/commit/ed3b3c552e384bb3eb7056468a125f6a8c661261",
"copyright": "CC-BY-NC-SA-3.0",
"states": [
{"name": "mobilevision", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]},
{"name": "party", "directions": 1, "delays": [[1.0]]},
{"name": "rad0", "directions": 1, "delays": [[1.0]]},
{"name": "rad1", "directions": 1, "delays": [[0.1, 0.1]]},
{"name": "shipalert0", "directions": 1, "delays": [[1.0]]},
{"name": "shipalert1", "directions": 1, "delays": [[1.0]]},
{"name": "shipalert2", "directions": 1, "delays": [[1.0]]},
{"name": "television", "directions": 4, "delays": [[1.0], [1.0], [1.0], [1.0]]}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB