diff --git a/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml
new file mode 100644
index 0000000000..84d581487d
--- /dev/null
+++ b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs
new file mode 100644
index 0000000000..da68653ce5
--- /dev/null
+++ b/Content.Client/Access/UI/GroupedAccessLevelChecklist.xaml.cs
@@ -0,0 +1,449 @@
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Access;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using System.Linq;
+using System.Numerics;
+
+namespace Content.Client.Access.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class GroupedAccessLevelChecklist : BoxContainer
+{
+ [Dependency] private readonly IPrototypeManager _protoManager = default!;
+
+ private bool _isMonotone;
+ private string? _labelStyleClass;
+
+ // Access data
+ private HashSet> _accessGroups = new();
+ private HashSet> _accessLevels = new();
+ private HashSet> _activeAccessLevels = new();
+
+ // Button groups
+ private readonly ButtonGroup _accessGroupsButtons = new();
+
+ // Temp values
+ private int _accessGroupTabIndex = 0;
+ private bool _canInteract = false;
+ private List _accessLevelsForTab = new();
+ private readonly List _accessLevelEntries = new();
+ private readonly Dictionary> _groupedAccessLevels = new();
+
+ // Events
+ public event Action>, bool>? OnAccessLevelsChangedEvent;
+
+ ///
+ /// Creates a UI control for changing access levels.
+ /// Access levels are organized under a list of tabs by their associated access group.
+ ///
+ public GroupedAccessLevelChecklist()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ }
+
+ private void ArrangeAccessControls()
+ {
+ // Create a list of known access groups with which to populate the UI
+ _groupedAccessLevels.Clear();
+
+ foreach (var accessGroup in _accessGroups)
+ {
+ if (!_protoManager.TryIndex(accessGroup, out var accessGroupProto))
+ continue;
+
+ _groupedAccessLevels.Add(accessGroupProto, new());
+ }
+
+ // Ensure that the 'general' access group is added to handle
+ // misc. access levels that aren't associated with any group
+ if (_protoManager.TryIndex("General", out var generalAccessProto))
+ _groupedAccessLevels.TryAdd(generalAccessProto, new());
+
+ // Assign known access levels with their associated groups
+ foreach (var accessLevel in _accessLevels)
+ {
+ if (!_protoManager.TryIndex(accessLevel, out var accessLevelProto))
+ continue;
+
+ var assigned = false;
+
+ foreach (var (accessGroup, accessLevels) in _groupedAccessLevels)
+ {
+ if (!accessGroup.Tags.Contains(accessLevelProto.ID))
+ continue;
+
+ assigned = true;
+ _groupedAccessLevels[accessGroup].Add(accessLevelProto);
+ }
+
+ if (!assigned && generalAccessProto != null)
+ _groupedAccessLevels[generalAccessProto].Add(accessLevelProto);
+ }
+
+ // Remove access groups that have no assigned access levels
+ foreach (var (group, accessLevels) in _groupedAccessLevels)
+ {
+ if (accessLevels.Count == 0)
+ _groupedAccessLevels.Remove(group);
+ }
+ }
+
+ private bool TryRebuildAccessGroupControls()
+ {
+ AccessGroupList.DisposeAllChildren();
+ AccessLevelChecklist.DisposeAllChildren();
+
+ // No access level prototypes were assigned to any of the access level groups.
+ // Either the turret controller has no assigned access levels or their names were invalid.
+ if (_groupedAccessLevels.Count == 0)
+ return false;
+
+ // Reorder the access groups alphabetically
+ var orderedAccessGroups = _groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList();
+
+ // Add group access buttons to the UI
+ foreach (var accessGroup in orderedAccessGroups)
+ {
+ var accessGroupButton = CreateAccessGroupButton();
+
+ // Button styling
+ if (_groupedAccessLevels.Count > 1)
+ {
+ if (AccessGroupList.ChildCount == 0)
+ accessGroupButton.AddStyleClass(StyleBase.ButtonOpenLeft);
+ else if (_groupedAccessLevels.Count > 1 && AccessGroupList.ChildCount == (_groupedAccessLevels.Count - 1))
+ accessGroupButton.AddStyleClass(StyleBase.ButtonOpenRight);
+ else
+ accessGroupButton.AddStyleClass(StyleBase.ButtonOpenBoth);
+ }
+
+ accessGroupButton.Pressed = _accessGroupTabIndex == orderedAccessGroups.IndexOf(accessGroup);
+
+ // Label text and styling
+ if (_labelStyleClass != null)
+ accessGroupButton.Label.SetOnlyStyleClass(_labelStyleClass);
+
+ var accessLevelPrototypes = _groupedAccessLevels[accessGroup];
+ var prefix = accessLevelPrototypes.All(x => _activeAccessLevels.Contains(x))
+ ? "»"
+ : accessLevelPrototypes.Any(x => _activeAccessLevels.Contains(x))
+ ? "›"
+ : " ";
+
+ var text = Loc.GetString(
+ "turret-controls-window-access-group-label",
+ ("prefix", prefix),
+ ("label", accessGroup.GetAccessGroupName())
+ );
+
+ accessGroupButton.Text = text;
+
+ // Button events
+ accessGroupButton.OnPressed += _ => OnAccessGroupChanged(accessGroupButton.GetPositionInParent());
+
+ AccessGroupList.AddChild(accessGroupButton);
+ }
+
+ // Adjust the current tab index so it remains in range
+ if (_accessGroupTabIndex >= _groupedAccessLevels.Count)
+ _accessGroupTabIndex = _groupedAccessLevels.Count - 1;
+
+ return true;
+ }
+
+ ///
+ /// Rebuilds the checkbox list for the access level controls.
+ ///
+ public void RebuildAccessLevelsControls()
+ {
+ AccessLevelChecklist.DisposeAllChildren();
+ _accessLevelEntries.Clear();
+
+ // No access level prototypes were assigned to any of the access level groups
+ // Either turret controller has no assigned access levels, or their names were invalid
+ if (_groupedAccessLevels.Count == 0)
+ return;
+
+ // Reorder the access groups alphabetically
+ var orderedAccessGroups = _groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList();
+
+ // Get the access levels associated with the current tab
+ var selectedAccessGroupTabProto = orderedAccessGroups[_accessGroupTabIndex];
+ _accessLevelsForTab = _groupedAccessLevels[selectedAccessGroupTabProto];
+ _accessLevelsForTab = _accessLevelsForTab.OrderBy(x => x.GetAccessLevelName()).ToList();
+
+ // Add an 'all' checkbox as the first child of the list if it has more than one access level
+ // Toggling this checkbox on will mark all other boxes below it on/off
+ var allCheckBox = CreateAccessLevelCheckbox();
+ allCheckBox.Text = Loc.GetString("turret-controls-window-all-checkbox");
+
+ if (_labelStyleClass != null)
+ allCheckBox.Label.SetOnlyStyleClass(_labelStyleClass);
+
+ // Add the 'all' checkbox events
+ allCheckBox.OnPressed += args =>
+ {
+ SetCheckBoxPressedState(_accessLevelEntries, allCheckBox.Pressed);
+
+ var accessLevels = new HashSet>();
+
+ foreach (var accessLevel in _accessLevelsForTab)
+ {
+ accessLevels.Add(accessLevel);
+ }
+
+ OnAccessLevelsChangedEvent?.Invoke(accessLevels, allCheckBox.Pressed);
+ };
+
+ AccessLevelChecklist.AddChild(allCheckBox);
+
+ // Hide the 'all' checkbox if the tab has only one access level
+ var allCheckBoxVisible = _accessLevelsForTab.Count > 1;
+
+ allCheckBox.Visible = allCheckBoxVisible;
+ allCheckBox.Disabled = !_canInteract;
+
+ // Add any remaining missing access level buttons to the UI
+ foreach (var accessLevel in _accessLevelsForTab)
+ {
+ // Create the entry
+ var accessLevelEntry = new AccessLevelEntry(_isMonotone);
+
+ accessLevelEntry.AccessLevel = accessLevel;
+ accessLevelEntry.CheckBox.Text = accessLevel.GetAccessLevelName();
+ accessLevelEntry.CheckBox.Pressed = _activeAccessLevels.Contains(accessLevel);
+ accessLevelEntry.CheckBox.Disabled = !_canInteract;
+
+ if (_labelStyleClass != null)
+ accessLevelEntry.CheckBox.Label.SetOnlyStyleClass(_labelStyleClass);
+
+ // Set the checkbox linkage lines
+ var isEndOfList = _accessLevelsForTab.IndexOf(accessLevel) == (_accessLevelsForTab.Count - 1);
+
+ var lines = new List<(Vector2, Vector2)>
+ {
+ (new Vector2(0.5f, 0f), new Vector2(0.5f, isEndOfList ? 0.5f : 1f)),
+ (new Vector2(0.5f, 0.5f), new Vector2(1f, 0.5f)),
+ };
+
+ accessLevelEntry.UpdateCheckBoxLink(lines);
+ accessLevelEntry.CheckBoxLink.Visible = allCheckBoxVisible;
+ accessLevelEntry.CheckBoxLink.Modulate = !_canInteract ? Color.Gray : Color.White;
+
+ // Add checkbox events
+ accessLevelEntry.CheckBox.OnPressed += args =>
+ {
+ // If the checkbox and its siblings are checked, check the 'all' checkbox too
+ allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox));
+
+ OnAccessLevelsChangedEvent?.Invoke([accessLevelEntry.AccessLevel], accessLevelEntry.CheckBox.Pressed);
+ };
+
+ AccessLevelChecklist.AddChild(accessLevelEntry);
+ _accessLevelEntries.Add(accessLevelEntry);
+ }
+
+ // Press the 'all' checkbox if all others are pressed
+ allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox));
+ }
+
+ private bool AreAllCheckBoxesPressed(IEnumerable checkBoxes)
+ {
+ foreach (var checkBox in checkBoxes)
+ {
+ if (!checkBox.Pressed)
+ return false;
+ }
+
+ return true;
+ }
+
+ private void SetCheckBoxPressedState(List accessLevelEntries, bool pressed)
+ {
+ foreach (var accessLevelEntry in accessLevelEntries)
+ {
+ accessLevelEntry.CheckBox.Pressed = pressed;
+ }
+ }
+
+
+ ///
+ /// Provides the UI with a list of access groups using which list of tabs should be populated.
+ ///
+ public void SetAccessGroups(HashSet> accessGroups)
+ {
+ _accessGroups = accessGroups;
+
+ ArrangeAccessControls();
+
+ if (TryRebuildAccessGroupControls())
+ RebuildAccessLevelsControls();
+ }
+
+ ///
+ /// Provides the UI with a list of access levels with which it can populate the currently selected tab.
+ ///
+ public void SetAccessLevels(HashSet> accessLevels)
+ {
+ _accessLevels = accessLevels;
+
+ ArrangeAccessControls();
+
+ if (TryRebuildAccessGroupControls())
+ RebuildAccessLevelsControls();
+ }
+
+ ///
+ /// Sets which access level checkboxes should be marked on the UI.
+ ///
+ public void SetActiveAccessLevels(HashSet> activeAccessLevels)
+ {
+ _activeAccessLevels = activeAccessLevels;
+
+ if (TryRebuildAccessGroupControls())
+ RebuildAccessLevelsControls();
+ }
+
+ ///
+ /// Sets whether the local player can interact with the checkboxes.
+ ///
+ public void SetLocalPlayerAccessibility(bool canInteract)
+ {
+ _canInteract = canInteract;
+
+ if (TryRebuildAccessGroupControls())
+ RebuildAccessLevelsControls();
+ }
+
+ ///
+ /// Sets whether the UI should use monotone buttons and checkboxes.
+ ///
+ public void SetMonotone(bool monotone)
+ {
+ _isMonotone = monotone;
+
+ if (TryRebuildAccessGroupControls())
+ RebuildAccessLevelsControls();
+ }
+
+ ///
+ /// Applies the specified style to the labels on the UI buttons and checkboxes.
+ ///
+ public void SetLabelStyleClass(string? styleClass)
+ {
+ _labelStyleClass = styleClass;
+
+ if (TryRebuildAccessGroupControls())
+ RebuildAccessLevelsControls();
+ }
+
+ private void OnAccessGroupChanged(int newTabIndex)
+ {
+ if (newTabIndex == _accessGroupTabIndex)
+ return;
+
+ _accessGroupTabIndex = newTabIndex;
+
+ if (TryRebuildAccessGroupControls())
+ RebuildAccessLevelsControls();
+ }
+
+ private Button CreateAccessGroupButton()
+ {
+ var button = _isMonotone ? new MonotoneButton() : new Button();
+
+ button.ToggleMode = true;
+ button.Group = _accessGroupsButtons;
+ button.Label.HorizontalAlignment = HAlignment.Left;
+
+ return button;
+ }
+
+ private CheckBox CreateAccessLevelCheckbox()
+ {
+ var checkbox = _isMonotone ? new MonotoneCheckBox() : new CheckBox();
+
+ checkbox.Margin = new Thickness(0, 0, 0, 3);
+ checkbox.ToggleMode = true;
+ checkbox.ReservesSpace = false;
+
+ return checkbox;
+ }
+
+ private sealed class AccessLevelEntry : BoxContainer
+ {
+ public ProtoId AccessLevel;
+ public readonly CheckBox CheckBox;
+ public readonly LineRenderer CheckBoxLink;
+
+ public AccessLevelEntry(bool monotone)
+ {
+ HorizontalExpand = true;
+
+ CheckBoxLink = new LineRenderer
+ {
+ SetWidth = 22,
+ VerticalExpand = true,
+ Margin = new Thickness(0, -1),
+ ReservesSpace = false,
+ };
+
+ AddChild(CheckBoxLink);
+
+ CheckBox = monotone ? new MonotoneCheckBox() : new CheckBox();
+ CheckBox.ToggleMode = true;
+ CheckBox.Margin = new Thickness(0f, 0f, 0f, 3f);
+
+ AddChild(CheckBox);
+ }
+
+ public void UpdateCheckBoxLink(List<(Vector2, Vector2)> lines)
+ {
+ CheckBoxLink.Lines = lines;
+ }
+ }
+
+ private sealed class LineRenderer : Control
+ {
+ ///
+ /// List of lines to render (their start and end x-y coordinates).
+ /// Position (0,0) is the top left corner of the control and
+ /// position (1,1) is the bottom right corner.
+ ///
+ ///
+ /// The color of the lines is inherited from the control.
+ ///
+ public List<(Vector2, Vector2)> Lines;
+
+ public LineRenderer()
+ {
+ Lines = new List<(Vector2, Vector2)>();
+ }
+
+ public LineRenderer(List<(Vector2, Vector2)> lines)
+ {
+ Lines = lines;
+ }
+
+ protected override void Draw(DrawingHandleScreen handle)
+ {
+ foreach (var line in Lines)
+ {
+ var start = PixelPosition +
+ new Vector2(PixelWidth * line.Item1.X, PixelHeight * line.Item1.Y);
+
+ var end = PixelPosition +
+ new Vector2(PixelWidth * line.Item2.X, PixelHeight * line.Item2.Y);
+
+ handle.DrawLine(start, end, ActualModulateSelf);
+ }
+ }
+ }
+}
diff --git a/Content.Client/TurretController/DeployableTurretControllerSystem.cs b/Content.Client/TurretController/DeployableTurretControllerSystem.cs
new file mode 100644
index 0000000000..c3b305f821
--- /dev/null
+++ b/Content.Client/TurretController/DeployableTurretControllerSystem.cs
@@ -0,0 +1,9 @@
+using Content.Shared.TurretController;
+
+namespace Content.Client.TurretController;
+
+///
+public sealed class DeployableTurretControllerSystem : SharedDeployableTurretControllerSystem
+{
+
+}
diff --git a/Content.Client/TurretController/TurretControllerWindow.xaml b/Content.Client/TurretController/TurretControllerWindow.xaml
new file mode 100644
index 0000000000..5f4af68f91
--- /dev/null
+++ b/Content.Client/TurretController/TurretControllerWindow.xaml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/TurretController/TurretControllerWindow.xaml.cs b/Content.Client/TurretController/TurretControllerWindow.xaml.cs
new file mode 100644
index 0000000000..020c894f54
--- /dev/null
+++ b/Content.Client/TurretController/TurretControllerWindow.xaml.cs
@@ -0,0 +1,201 @@
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Access;
+using Content.Shared.Access.Systems;
+using Content.Shared.TurretController;
+using Content.Shared.Turrets;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Player;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using System.Numerics;
+
+namespace Content.Client.TurretController;
+
+[GenerateTypedNameReferences]
+public sealed partial class TurretControllerWindow : BaseWindow
+{
+ [Dependency] private readonly IEntityManager _entManager = default!;
+ [Dependency] private readonly IPlayerManager _playerManager = default!;
+ [Dependency] private readonly IResourceCache _cache = default!;
+
+ private readonly AccessReaderSystem _accessReaderSystem;
+
+ private EntityUid? _owner;
+
+ // Button groups
+ private readonly ButtonGroup _armamentButtons = new();
+
+ // Events
+ public event Action>, bool>? OnAccessLevelsChangedEvent;
+ public event Action? OnArmamentSettingChangedEvent;
+
+ // Colors
+ private static readonly Dictionary ThemeColors = new()
+ {
+ [TurretArmamentSetting.Safe] = Color.FromHex("#33e633"),
+ [TurretArmamentSetting.Stun] = Color.FromHex("#dfb827"),
+ [TurretArmamentSetting.Lethal] = Color.FromHex("#da2a2a")
+ };
+
+ public TurretControllerWindow()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ _accessReaderSystem = _entManager.System();
+
+ CloseButton.OnPressed += _ => Close();
+
+ // Set up armament buttons
+ SafeButton.OnToggled += args => OnArmamentButtonPressed(SafeButton, TurretArmamentSetting.Safe);
+ StunButton.OnToggled += args => OnArmamentButtonPressed(StunButton, TurretArmamentSetting.Stun);
+ LethalButton.OnToggled += args => OnArmamentButtonPressed(LethalButton, TurretArmamentSetting.Lethal);
+
+ SafeButton.Group = _armamentButtons;
+ StunButton.Group = _armamentButtons;
+ LethalButton.Group = _armamentButtons;
+
+ SafeButton.Label.AddStyleClass("ConsoleText");
+ StunButton.Label.AddStyleClass("ConsoleText");
+ LethalButton.Label.AddStyleClass("ConsoleText");
+
+ // Set up access configuration buttons
+ AccessConfiguration.SetMonotone(true);
+ AccessConfiguration.SetLabelStyleClass("ConsoleText");
+ AccessConfiguration.OnAccessLevelsChangedEvent += OnAccessLevelsChanged;
+
+ // Override footer font
+ var smallFont = _cache.NotoStack(size: 8);
+ Footer.FontOverride = smallFont;
+ }
+
+ private void OnAccessLevelsChanged(HashSet> accessLevels, bool isPressed)
+ {
+ OnAccessLevelsChangedEvent?.Invoke(accessLevels, isPressed);
+ }
+
+ private void OnArmamentButtonPressed(MonotoneButton pressedButton, TurretArmamentSetting setting)
+ {
+ UpdateTheme(setting);
+ OnArmamentSettingChangedEvent?.Invoke(setting);
+ }
+
+ private void Initialize()
+ {
+ RefreshLinkedTurrets(new());
+
+ if (_entManager.TryGetComponent(_owner, out var turretController))
+ {
+ AccessConfiguration.SetAccessGroups(turretController.AccessGroups);
+ AccessConfiguration.SetAccessLevels(turretController.AccessLevels);
+ UpdateTheme((TurretArmamentSetting)turretController.ArmamentState);
+ }
+
+ if (_entManager.TryGetComponent(_owner, out var turretTargetSettings))
+ {
+ RefreshAccessControls(turretTargetSettings.ExemptAccessLevels);
+ }
+ }
+
+ public void SetOwner(EntityUid owner)
+ {
+ _owner = owner;
+
+ Initialize();
+ }
+
+ private void UpdateTheme(TurretArmamentSetting setting)
+ {
+ var setPressedOn = setting switch
+ {
+ TurretArmamentSetting.Safe => SafeButton,
+ TurretArmamentSetting.Stun => StunButton,
+ TurretArmamentSetting.Lethal => LethalButton,
+ };
+ setPressedOn.Pressed = true;
+
+ var canInteract = IsLocalPlayerAllowedToInteract();
+
+ SafeButton.Disabled = !SafeButton.Pressed && !canInteract;
+ StunButton.Disabled = !StunButton.Pressed && !canInteract;
+ LethalButton.Disabled = !LethalButton.Pressed && !canInteract;
+
+ ContentsContainer.Modulate = ThemeColors[setting];
+ }
+
+ public void UpdateState(DeployableTurretControllerBoundInterfaceState state)
+ {
+ if (_entManager.TryGetComponent(_owner, out var turretController))
+ UpdateTheme((TurretArmamentSetting)turretController.ArmamentState);
+
+ if (_entManager.TryGetComponent(_owner, out var turretTargetSettings))
+ RefreshAccessControls(turretTargetSettings.ExemptAccessLevels);
+
+ RefreshLinkedTurrets(state.TurretStateByAddress);
+ }
+
+ public void RefreshLinkedTurrets(Dictionary turretStates)
+ {
+ var turretCount = turretStates.Count;
+ var hasTurrets = turretCount > 0;
+
+ NoLinkedTurretsText.Visible = !hasTurrets;
+ LinkedTurretsContainer.Visible = hasTurrets;
+
+ LinkedTurretsContainer.RemoveAllChildren();
+
+ foreach (var (address, state) in turretStates)
+ {
+ var text = Loc.GetString(
+ "turret-controls-window-turret-status",
+ ("device", address),
+ ("status", Loc.GetString(state))
+ );
+
+ var label = new Label
+ {
+ Text = text,
+ HorizontalAlignment = HAlignment.Left,
+ Margin = new Thickness(10f, 0f, 10f, 0f),
+ HorizontalExpand = true,
+ SetHeight = 20f,
+ };
+
+ label.AddStyleClass("ConsoleText");
+
+ LinkedTurretsContainer.AddChild(label);
+ }
+
+ TurretStatusHeader.Text = Loc.GetString("turret-controls-window-turret-status-label", ("count", turretCount));
+ }
+
+ public void RefreshAccessControls(HashSet> exemptAccessLevels)
+ {
+ AccessConfiguration.SetActiveAccessLevels(exemptAccessLevels);
+ AccessConfiguration.SetLocalPlayerAccessibility(IsLocalPlayerAllowedToInteract());
+ }
+
+ protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
+ {
+ return DragMode.Move;
+ }
+
+ private bool IsLocalPlayerAllowedToInteract()
+ {
+ if (_owner == null || _playerManager.LocalSession?.AttachedEntity == null)
+ return false;
+
+ return _accessReaderSystem.IsAllowed(_playerManager.LocalSession.AttachedEntity.Value, _owner.Value);
+ }
+
+ public enum TurretArmamentSetting
+ {
+ Safe = -1,
+ Stun = 0,
+ Lethal = 1,
+ }
+}
diff --git a/Content.Client/TurretController/TurretControllerWindowBoundUserInterface.cs b/Content.Client/TurretController/TurretControllerWindowBoundUserInterface.cs
new file mode 100644
index 0000000000..ab1635b8d8
--- /dev/null
+++ b/Content.Client/TurretController/TurretControllerWindowBoundUserInterface.cs
@@ -0,0 +1,44 @@
+using Content.Shared.Access;
+using Content.Shared.TurretController;
+using Robust.Client.UserInterface;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.TurretController;
+
+public sealed class TurretControllerWindowBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
+{
+ [ViewVariables]
+ private TurretControllerWindow? _window;
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = this.CreateWindow();
+ _window.SetOwner(Owner);
+ _window.OpenCentered();
+
+ _window.OnAccessLevelsChangedEvent += OnAccessLevelChanged;
+ _window.OnArmamentSettingChangedEvent += OnArmamentSettingChanged;
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not DeployableTurretControllerBoundInterfaceState { } castState)
+ return;
+
+ _window?.UpdateState(castState);
+ }
+
+ private void OnAccessLevelChanged(HashSet> accessLevels, bool enabled)
+ {
+ SendPredictedMessage(new DeployableTurretExemptAccessLevelChangedMessage(accessLevels, enabled));
+ }
+
+ private void OnArmamentSettingChanged(TurretControllerWindow.TurretArmamentSetting setting)
+ {
+ SendPredictedMessage(new DeployableTurretArmamentSettingChangedMessage((int)setting));
+ }
+}
diff --git a/Content.Client/Turrets/DeployableTurretSystem.cs b/Content.Client/Turrets/DeployableTurretSystem.cs
index 5e84b1e01a..05cacba6f1 100644
--- a/Content.Client/Turrets/DeployableTurretSystem.cs
+++ b/Content.Client/Turrets/DeployableTurretSystem.cs
@@ -84,9 +84,6 @@ public sealed partial class DeployableTurretSystem : SharedDeployableTurretSyste
if (_animation.HasRunningAnimation(ent, animPlayer, DeployableTurretComponent.AnimationKey))
return;
- if (state == ent.Comp.VisualState)
- return;
-
var targetState = state & DeployableTurretState.Deployed;
var destinationState = ent.Comp.VisualState & DeployableTurretState.Deployed;
diff --git a/Content.Client/UserInterface/Controls/MonotoneButton.cs b/Content.Client/UserInterface/Controls/MonotoneButton.cs
index 7271ee7de7..b19a2c640f 100644
--- a/Content.Client/UserInterface/Controls/MonotoneButton.cs
+++ b/Content.Client/UserInterface/Controls/MonotoneButton.cs
@@ -7,7 +7,7 @@ namespace Content.Client.UserInterface.Controls;
///
/// A button intended for use with a monotone color palette
///
-public sealed class MonotoneButton : ContainerButton
+public sealed class MonotoneButton : Button
{
///
/// Specifies the color of the label text when the button is pressed.
@@ -15,43 +15,9 @@ public sealed class MonotoneButton : ContainerButton
[ViewVariables]
public Color AltTextColor { set; get; } = new Color(0.2f, 0.2f, 0.2f);
- ///
- /// The label that holds the button text.
- ///
- public Label Label { get; }
-
- ///
- /// The text displayed by the button.
- ///
- [PublicAPI, ViewVariables]
- public string? Text { get => Label.Text; set => Label.Text = value; }
-
- ///
- /// How to align the text inside the button.
- ///
- [PublicAPI, ViewVariables]
- public AlignMode TextAlign { get => Label.Align; set => Label.Align = value; }
-
- ///
- /// If true, the button will allow shrinking and clip text
- /// to prevent the text from going outside the bounds of the button.
- /// If false, the minimum size will always fit the contained text.
- ///
- [PublicAPI, ViewVariables]
- public bool ClipText
- {
- get => Label.ClipText;
- set => Label.ClipText = value;
- }
-
public MonotoneButton()
{
- Label = new Label
- {
- StyleClasses = { StyleClassButton }
- };
-
- AddChild(Label);
+ RemoveStyleClass("button");
UpdateAppearance();
}
diff --git a/Content.Server/TurretController/DeployableTurretControllerSystem.cs b/Content.Server/TurretController/DeployableTurretControllerSystem.cs
new file mode 100644
index 0000000000..f0b6881431
--- /dev/null
+++ b/Content.Server/TurretController/DeployableTurretControllerSystem.cs
@@ -0,0 +1,152 @@
+using Content.Server.DeviceNetwork.Systems;
+using Content.Shared.Access;
+using Content.Shared.DeviceNetwork;
+using Content.Shared.DeviceNetwork.Components;
+using Content.Shared.DeviceNetwork.Events;
+using Content.Shared.DeviceNetwork.Systems;
+using Content.Shared.TurretController;
+using Content.Shared.Turrets;
+using Robust.Server.GameObjects;
+using Robust.Shared.Prototypes;
+using System.Linq;
+
+namespace Content.Server.TurretController;
+
+///
+public sealed partial class DeployableTurretControllerSystem : SharedDeployableTurretControllerSystem
+{
+ [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
+ [Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
+
+ /// Keys for the device network. See for further examples.
+ public const string CmdSetArmamemtState = "set_armament_state";
+ public const string CmdSetAccessExemptions = "set_access_exemption";
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnBUIOpened);
+ SubscribeLocalEvent(OnDeviceListUpdate);
+ SubscribeLocalEvent(OnPacketReceived);
+ }
+
+ private void OnBUIOpened(Entity ent, ref BoundUIOpenedEvent args)
+ {
+ UpdateUIState(ent);
+ }
+
+ private void OnDeviceListUpdate(Entity ent, ref DeviceListUpdateEvent args)
+ {
+ if (!TryComp(ent, out var deviceNetwork))
+ return;
+
+ // List of new added turrets
+ var turretsToAdd = args.Devices.Except(args.OldDevices);
+
+ // Request data from newly linked devices
+ var payload = new NetworkPayload
+ {
+ [DeviceNetworkConstants.Command] = DeviceNetworkConstants.CmdUpdatedState,
+ };
+
+ foreach (var turretUid in turretsToAdd)
+ {
+ if (!HasComp(turretUid))
+ continue;
+
+ if (!TryComp(turretUid, out var turretDeviceNetwork))
+ continue;
+
+ _deviceNetwork.QueuePacket(ent, turretDeviceNetwork.Address, payload, device: deviceNetwork);
+ }
+
+ // Remove newly unlinked devices
+ var turretsToRemove = args.OldDevices.Except(args.Devices);
+ var refreshUi = false;
+
+ foreach (var turretUid in turretsToRemove)
+ {
+ if (!TryComp(turretUid, out var turretDeviceNetwork))
+ continue;
+
+ if (ent.Comp.LinkedTurrets.Remove(turretDeviceNetwork.Address))
+ refreshUi = true;
+ }
+
+ if (refreshUi)
+ UpdateUIState(ent);
+ }
+
+ private void OnPacketReceived(Entity ent, ref DeviceNetworkPacketEvent args)
+ {
+ if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command))
+ return;
+
+ if (!TryComp(ent, out var deviceNetwork) || deviceNetwork.ReceiveFrequency != args.Frequency)
+ return;
+
+ // If an update was received from a turret, connect to it and update the UI
+ if (command == DeviceNetworkConstants.CmdUpdatedState &&
+ args.Data.TryGetValue(command, out DeployableTurretState updatedState))
+ {
+ ent.Comp.LinkedTurrets[args.SenderAddress] = updatedState;
+ UpdateUIState(ent);
+ }
+ }
+
+ protected override void ChangeArmamentSetting(Entity ent, int armamentState, EntityUid? user = null)
+ {
+ base.ChangeArmamentSetting(ent, armamentState, user);
+
+ if (!TryComp(ent, out var device))
+ return;
+
+ // Update linked turrets' armament statuses
+ var payload = new NetworkPayload
+ {
+ [DeviceNetworkConstants.Command] = CmdSetArmamemtState,
+ [CmdSetArmamemtState] = armamentState,
+ };
+
+ _deviceNetwork.QueuePacket(ent, null, payload, device: device);
+ }
+
+ protected override void ChangeExemptAccessLevels(
+ Entity ent,
+ HashSet> exemptions,
+ bool enabled,
+ EntityUid? user = null
+ )
+ {
+ base.ChangeExemptAccessLevels(ent, exemptions, enabled, user);
+
+ if (!TryComp(ent, out var device) ||
+ !TryComp(ent, out var turretTargetingSettings))
+ return;
+
+ // Update linked turrets' target selection exemptions
+ var payload = new NetworkPayload
+ {
+ [DeviceNetworkConstants.Command] = CmdSetAccessExemptions,
+ [CmdSetAccessExemptions] = turretTargetingSettings.ExemptAccessLevels,
+ };
+
+ _deviceNetwork.QueuePacket(ent, null, payload, device: device);
+ }
+
+ private void UpdateUIState(Entity ent)
+ {
+ var turretStates = new Dictionary();
+
+ foreach (var (address, state) in ent.Comp.LinkedTurrets)
+ {
+ var stateName = state.ToString().ToLower();
+ var stateDesc = Loc.GetString("turret-controls-window-turret-" + stateName);
+ turretStates.Add(address, stateDesc);
+ }
+
+ var uiState = new DeployableTurretControllerBoundInterfaceState(turretStates);
+ _userInterfaceSystem.SetUiState(ent.Owner, DeployableTurretControllerUiKey.Key, uiState);
+ }
+}
diff --git a/Content.Server/Turrets/DeployableTurretSystem.cs b/Content.Server/Turrets/DeployableTurretSystem.cs
index 72c011bc90..9bb382badf 100644
--- a/Content.Server/Turrets/DeployableTurretSystem.cs
+++ b/Content.Server/Turrets/DeployableTurretSystem.cs
@@ -1,20 +1,23 @@
using Content.Server.Destructible;
-using Content.Server.DeviceNetwork;
-using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.NPC.HTN;
using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat.Ranged;
using Content.Server.Power.Components;
using Content.Server.Repairable;
+using Content.Server.TurretController;
+using Content.Shared.Access;
using Content.Shared.Destructible;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Power;
using Content.Shared.Turrets;
+using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
+using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
+using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.Turrets;
@@ -25,6 +28,8 @@ public sealed partial class DeployableTurretSystem : SharedDeployableTurretSyste
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
+ [Dependency] private readonly BatteryWeaponFireModesSystem _fireModes = default!;
+ [Dependency] private readonly TurretTargetSettingsSystem _turretTargetingSettings = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
@@ -36,6 +41,7 @@ public sealed partial class DeployableTurretSystem : SharedDeployableTurretSyste
SubscribeLocalEvent(OnPowerChanged);
SubscribeLocalEvent(OnBroken);
SubscribeLocalEvent(OnRepaired);
+ SubscribeLocalEvent(OnPacketReceived);
SubscribeLocalEvent(OnBeforeBroadcast);
}
@@ -68,6 +74,39 @@ public sealed partial class DeployableTurretSystem : SharedDeployableTurretSyste
_appearance.SetData(ent, DeployableTurretVisuals.Broken, false, appearance);
}
+ private void OnPacketReceived(Entity ent, ref DeviceNetworkPacketEvent args)
+ {
+ if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? command))
+ return;
+
+ // Received a command to change armament state
+ if (command == DeployableTurretControllerSystem.CmdSetArmamemtState &&
+ args.Data.TryGetValue(command, out int? armamentState))
+ {
+ if (TryComp(ent, out var batteryWeaponFireModes))
+ _fireModes.TrySetFireMode(ent, batteryWeaponFireModes, armamentState.Value);
+
+ TrySetState(ent, armamentState.Value >= 0);
+ return;
+ }
+
+ // Received a command to change access exemptions
+ if (command == DeployableTurretControllerSystem.CmdSetAccessExemptions &&
+ args.Data.TryGetValue(command, out HashSet>? accessExemptions) &&
+ TryComp(ent, out var turretTargetSettings))
+ {
+ _turretTargetingSettings.SyncAccessLevelExemptions((ent, turretTargetSettings), accessExemptions);
+ return;
+ }
+
+ // Received a command to update the device network
+ if (command == DeviceNetworkConstants.CmdUpdatedState)
+ {
+ SendStateUpdateToDeviceNetwork(ent);
+ return;
+ }
+ }
+
private void OnBeforeBroadcast(Entity ent, ref BeforeBroadcastAttemptEvent args)
{
if (!TryComp(ent, out var deviceNetwork))
diff --git a/Content.Shared/Access/AccessGroupPrototype.cs b/Content.Shared/Access/AccessGroupPrototype.cs
index ca73aa3dd0..78292df44d 100644
--- a/Content.Shared/Access/AccessGroupPrototype.cs
+++ b/Content.Shared/Access/AccessGroupPrototype.cs
@@ -1,6 +1,5 @@
-using Content.Shared.Access.Components;
+using Content.Shared.Access.Components;
using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Shared.Access;
@@ -14,6 +13,23 @@ public sealed partial class AccessGroupPrototype : IPrototype
[IdDataField]
public string ID { get; private set; } = default!;
- [DataField("tags", required: true)]
+ ///
+ /// The player-visible name of the access level group
+ ///
+ [DataField]
+ public string? Name { get; set; }
+
+ ///
+ /// The access levels associated with this group
+ ///
+ [DataField(required: true)]
public HashSet> Tags = default!;
+
+ public string GetAccessGroupName()
+ {
+ if (Name is { } name)
+ return Loc.GetString(name);
+
+ return ID;
+ }
}
diff --git a/Content.Shared/TurretController/DeployableTurretControllerComponent.cs b/Content.Shared/TurretController/DeployableTurretControllerComponent.cs
new file mode 100644
index 0000000000..3bada93413
--- /dev/null
+++ b/Content.Shared/TurretController/DeployableTurretControllerComponent.cs
@@ -0,0 +1,102 @@
+using Content.Shared.Access;
+using Content.Shared.Turrets;
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.TurretController;
+
+///
+/// Attached to entities that can set data on linked turret-based entities
+///
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+[Access(typeof(SharedDeployableTurretControllerSystem))]
+public sealed partial class DeployableTurretControllerComponent : Component
+{
+ ///
+ /// The states of the turrets linked to this entity, indexed by their device address.
+ /// This is used to populate the controller UI with the address and state of linked turrets.
+ ///
+ [ViewVariables]
+ public Dictionary LinkedTurrets = new();
+
+ ///
+ /// The last armament state index applied to any linked turrets.
+ /// Values greater than zero have no additional effect if the linked turrets
+ /// do not have the
+ ///
+ ///
+ /// -1: Inactive, 0: weapon mode A, 1: weapon mode B, etc.
+ ///
+ [DataField, AutoNetworkedField]
+ public int ArmamentState = -1;
+
+ ///
+ /// Access level prototypes that are known to the entity.
+ /// Determines what access permissions can be adjusted.
+ /// It is also used to populate the controller UI.
+ ///
+ [DataField]
+ public HashSet> AccessLevels = new();
+
+ ///
+ /// Access group prototypes that are known to the entity.
+ /// Determines how access permissions are organized on the controller UI.
+ ///
+ [DataField]
+ public HashSet> AccessGroups = new();
+
+ ///
+ /// Sound to play when denying access to the device.
+ ///
+ [DataField]
+ public SoundSpecifier AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
+}
+
+[Serializable, NetSerializable]
+public sealed class DeployableTurretControllerBoundInterfaceState : BoundUserInterfaceState
+{
+ public Dictionary TurretStateByAddress;
+
+ public DeployableTurretControllerBoundInterfaceState(Dictionary turretStateByAddress)
+ {
+ TurretStateByAddress = turretStateByAddress;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class DeployableTurretArmamentSettingChangedMessage : BoundUserInterfaceMessage
+{
+ public int ArmamentState;
+
+ public DeployableTurretArmamentSettingChangedMessage(int armamentState)
+ {
+ ArmamentState = armamentState;
+ }
+}
+
+[Serializable, NetSerializable]
+public sealed class DeployableTurretExemptAccessLevelChangedMessage : BoundUserInterfaceMessage
+{
+ public HashSet> AccessLevels;
+ public bool Enabled;
+
+ public DeployableTurretExemptAccessLevelChangedMessage(HashSet> accessLevels, bool enabled)
+ {
+ AccessLevels = accessLevels;
+ Enabled = enabled;
+ }
+}
+
+[Serializable, NetSerializable]
+public enum TurretControllerVisuals : byte
+{
+ ControlPanel,
+}
+
+[Serializable, NetSerializable]
+public enum DeployableTurretControllerUiKey : byte
+{
+ Key,
+}
diff --git a/Content.Shared/TurretController/SharedDeployableTurretControllerSystem.cs b/Content.Shared/TurretController/SharedDeployableTurretControllerSystem.cs
new file mode 100644
index 0000000000..314a98071a
--- /dev/null
+++ b/Content.Shared/TurretController/SharedDeployableTurretControllerSystem.cs
@@ -0,0 +1,96 @@
+using Content.Shared.Access;
+using Content.Shared.Access.Systems;
+using Content.Shared.Popups;
+using Content.Shared.Turrets;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.TurretController;
+
+///
+/// Oversees entities that can change the component values of linked deployable turrets,
+/// specifically their armament and access level exemptions, via an associated UI
+///
+public abstract partial class SharedDeployableTurretControllerSystem : EntitySystem
+{
+ [Dependency] private readonly AccessReaderSystem _accessreader = default!;
+ [Dependency] private readonly TurretTargetSettingsSystem _turretTargetingSettings = default!;
+ [Dependency] private readonly SharedUserInterfaceSystem _userInterfaceSystem = default!;
+ [Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly SharedAudioSystem _audio = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ // Handling of client messages
+ SubscribeLocalEvent(OnArmamentSettingChanged);
+ SubscribeLocalEvent(OnExemptAccessLevelsChanged);
+ }
+
+ private void OnArmamentSettingChanged(Entity ent, ref DeployableTurretArmamentSettingChangedMessage args)
+ {
+ if (IsUserAllowedAccess(ent, args.Actor))
+ ChangeArmamentSetting(ent, args.ArmamentState, args.Actor);
+
+ if (_userInterfaceSystem.TryGetOpenUi(ent.Owner, DeployableTurretControllerUiKey.Key, out var bui))
+ bui.Update();
+ }
+
+ private void OnExemptAccessLevelsChanged(Entity ent, ref DeployableTurretExemptAccessLevelChangedMessage args)
+ {
+ if (IsUserAllowedAccess(ent, args.Actor))
+ ChangeExemptAccessLevels(ent, args.AccessLevels, args.Enabled, args.Actor);
+
+ if (_userInterfaceSystem.TryGetOpenUi(ent.Owner, DeployableTurretControllerUiKey.Key, out var bui))
+ bui.Update();
+ }
+
+ protected virtual void ChangeArmamentSetting(Entity ent, int armamentState, EntityUid? user = null)
+ {
+ ent.Comp.ArmamentState = armamentState;
+ Dirty(ent);
+
+ _appearance.SetData(ent, TurretControllerVisuals.ControlPanel, armamentState);
+
+ // Linked turrets are updated on the server side
+ }
+
+ protected virtual void ChangeExemptAccessLevels(
+ Entity ent,
+ HashSet> exemptions,
+ bool enabled,
+ EntityUid? user = null
+ )
+ {
+ // Update the controller
+ if (!TryComp(ent, out var targetSettings))
+ return;
+
+ var controller = new Entity(ent, targetSettings);
+
+ foreach (var accessLevel in exemptions)
+ {
+ if (!ent.Comp.AccessLevels.Contains(accessLevel))
+ continue;
+
+ _turretTargetingSettings.SetAccessLevelExemption(controller, accessLevel, enabled);
+ }
+
+ Dirty(controller);
+
+ // Linked turrets are updated on the server side
+ }
+
+ public bool IsUserAllowedAccess(Entity ent, EntityUid user)
+ {
+ if (_accessreader.IsAllowed(user, ent))
+ return true;
+
+ _popup.PopupClient(Loc.GetString("turret-controls-access-denied"), ent, user);
+ _audio.PlayPredicted(ent.Comp.AccessDeniedSound, ent, user);
+
+ return false;
+ }
+}
diff --git a/Content.Shared/Turrets/TurretTargetSettingsSystem.cs b/Content.Shared/Turrets/TurretTargetSettingsSystem.cs
index 56f60e0e69..3a8edc9955 100644
--- a/Content.Shared/Turrets/TurretTargetSettingsSystem.cs
+++ b/Content.Shared/Turrets/TurretTargetSettingsSystem.cs
@@ -23,13 +23,17 @@ public sealed partial class TurretTargetSettingsSystem : EntitySystem
/// The entity and its
/// The proto ID for the access level
/// Set 'true' to add the exemption, or 'false' to remove it
+ /// Set 'true' to dirty the component
[PublicAPI]
- public void SetAccessLevelExemption(Entity ent, ProtoId exemption, bool enabled)
+ public void SetAccessLevelExemption(Entity ent, ProtoId exemption, bool enabled, bool dirty = true)
{
if (enabled)
ent.Comp.ExemptAccessLevels.Add(exemption);
else
ent.Comp.ExemptAccessLevels.Remove(exemption);
+
+ if (dirty)
+ Dirty(ent);
}
///
@@ -42,7 +46,9 @@ public sealed partial class TurretTargetSettingsSystem : EntitySystem
public void SetAccessLevelExemptions(Entity ent, ICollection> exemptions, bool enabled)
{
foreach (var exemption in exemptions)
- SetAccessLevelExemption(ent, exemption, enabled);
+ SetAccessLevelExemption(ent, exemption, enabled, false);
+
+ Dirty(ent);
}
///
diff --git a/Resources/Locale/en-US/ui/turret-controls.ftl b/Resources/Locale/en-US/ui/turret-controls.ftl
new file mode 100644
index 0000000000..549781a866
--- /dev/null
+++ b/Resources/Locale/en-US/ui/turret-controls.ftl
@@ -0,0 +1,31 @@
+# Headings
+turret-controls-window-title = Autonomous Defense Control System
+turret-controls-window-turret-status-label = Linked devices [{$count}]
+turret-controls-window-armament-controls-label = Armament setting
+turret-controls-window-targeting-controls-label = Authorized personnel
+
+# Status reports
+turret-controls-window-no-turrets =
+turret-controls-window-turret-status = » {$device} - Status: {$status}
+turret-controls-window-turret-disabled = ***OFFLINE***
+turret-controls-window-turret-retracted = INACTIVE
+turret-controls-window-turret-retracting = DEACTIVATING
+turret-controls-window-turret-deployed = SEARCHING...
+turret-controls-window-turret-deploying = ACTIVATING
+turret-controls-window-turret-firing = ENGAGING TARGET
+turret-controls-window-turret-error = ERROR [404]
+
+# Buttons
+turret-controls-window-safe = Inactive
+turret-controls-window-stun = Stun
+turret-controls-window-lethal = Lethal
+turret-controls-window-ignore = Ignore
+turret-controls-window-target = Target
+turret-controls-window-access-group-label = {$prefix} {$label}
+turret-controls-window-all-checkbox = All
+
+# Flavor
+turret-controls-window-footer = Unauthorized personnel should ensure defenses are inactive before proceeding
+
+# Warnings
+turret-controls-access-denied = Access denied
\ No newline at end of file
diff --git a/Resources/Prototypes/Access/misc.yml b/Resources/Prototypes/Access/misc.yml
index d3f6df775b..5c8d5e3660 100644
--- a/Resources/Prototypes/Access/misc.yml
+++ b/Resources/Prototypes/Access/misc.yml
@@ -34,3 +34,8 @@
- Atmospherics
- GenpopEnter
- GenpopLeave
+
+- type: accessGroup
+ id: General
+ tags:
+ - Maintenance
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml
index 66860ae98c..a8b3c9a88c 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml
@@ -1,11 +1,9 @@
- type: entity
parent: [BaseWeaponEnergyTurret, ConstructibleMachine]
id: WeaponEnergyTurretStation
- name: sentry turret
+ name: security turret
description: A high-tech autonomous weapons system designed to keep unauthorized personnel out of sensitive areas.
components:
-
- # Physics
- type: Fixtures
fixtures:
body:
@@ -25,8 +23,6 @@
layer:
- MachineLayer
hard: false
-
- # Sprites and appearance
- type: Sprite
sprite: Objects/Weapons/Guns/Turrets/sentry_turret.rsi
drawdepth: HighFloorObjects
@@ -70,20 +66,14 @@
enum.WiresVisualLayers.MaintenancePanel:
True: { visible: false }
False: { visible: true }
-
- # HTN
- type: HTN
enabled: false
-
- # Faction / control
- type: StationAiWhitelist
- type: NpcFactionMember
factions:
- AllHostile
- type: AccessReader
access: [["Security"]]
-
- # Weapon systems
- type: ProjectileBatteryAmmoProvider
proto: BulletEnergyTurretDisabler
fireCost: 100
@@ -98,8 +88,6 @@
- Security
- Borg
- BasicSilicon
-
- # Defenses / destruction
- type: DeployableTurret
retractedDamageModifierSetId: Metallic
deployedDamageModifierSetId: FlimsyMetallic
@@ -130,8 +118,6 @@
node: machineFrame
- !type:DoActsBehavior
acts: ["Destruction"]
-
- # Device network
- type: DeviceNetwork
deviceNetId: Wired
receiveFrequencyId: TurretControl
@@ -141,8 +127,6 @@
examinableAddress: true
- type: DeviceNetworkRequiresPower
- type: WiredNetworkConnection
-
- # Wires
- type: UserInterface
interfaces:
enum.WiresUiKey.Key:
@@ -156,8 +140,6 @@
locked: true
unlockOnClick: false
- type: LockedWiresPanel
-
- # General properties
- type: Machine
board: WeaponEnergyTurretStationMachineCircuitboard
- type: UseDelay
@@ -170,7 +152,7 @@
description: A high-tech autonomous weapons system under the direct control of a local artifical intelligence.
components:
- type: AccessReader
- access: [["StationAi"]]
+ access: [["StationAi"], ["ResearchDirector"]]
- type: TurretTargetSettings
exemptAccessLevels:
- Borg
@@ -180,3 +162,4 @@
- type: DeviceNetwork
receiveFrequencyId: TurretControlAI
transmitFrequencyId: TurretAI
+
diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/turret_controls.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/turret_controls.yml
new file mode 100644
index 0000000000..95a3e74b1f
--- /dev/null
+++ b/Resources/Prototypes/Entities/Structures/Wallmounts/turret_controls.yml
@@ -0,0 +1,198 @@
+- type: entity
+ id: WeaponEnergyTurretControlPanelFrame
+ name: sentry turret control panel assembly
+ description: An incomplete wall-mounted assembly for a sentry turret control panel.
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: Sprite
+ noRot: false
+ drawdepth: SmallObjects
+ sprite: Structures/Wallmounts/turret_controls.rsi
+ layers:
+ - state: base
+ - type: Damageable
+ damageContainer: StructuralInorganic
+ damageModifierSet: StructuralMetallic
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 200
+ behaviors:
+ - !type:PlaySoundBehavior
+ sound:
+ collection: MetalGlassBreak
+ params:
+ volume: -4
+ #- !type:ChangeConstructionNodeBehavior - To be added in a later PR
+ # node: machineFrame
+ - !type:DoActsBehavior
+ acts: [ "Destruction" ]
+ - type: Transform
+ anchored: true
+ - type: WallMount
+ - type: Clickable
+ - type: InteractionOutline
+ - type: ContainerContainer
+ containers:
+ board: !type:Container
+ #- type: Construction - To be added in a later PR
+ # graph: WeaponEnergyTurretControlPanel
+ # node: frame
+ placement:
+ mode: SnapgridCenter
+ snap:
+ - Wallmount
+
+- type: entity
+ parent: WeaponEnergyTurretControlPanelFrame
+ id: WeaponEnergyTurretStationControlPanel
+ name: security turret control panel
+ description: A wall-mounted interface for remotely configuring the operational parameters of linked security turrets.
+ components:
+ - type: Appearance
+ - type: Sprite
+ noRot: false
+ drawdepth: SmallObjects
+ sprite: Structures/Wallmounts/turret_controls.rsi
+ layers:
+ - state: base
+ - state: safe
+ map: ["enum.PowerDeviceVisualLayers.Powered"]
+ shader: unshaded
+ - state: wires
+ map: ["enum.WiresVisualLayers.MaintenancePanel"]
+ visible: false
+ - type: GenericVisualizer
+ visuals:
+ enum.WiresVisualLayers.MaintenancePanel:
+ enum.WiresVisualLayers.MaintenancePanel:
+ True: { visible: true }
+ False: { visible: false }
+ enum.PowerDeviceVisuals.Powered:
+ enum.PowerDeviceVisualLayers.Powered:
+ True: { visible: true }
+ False: { visible: false }
+ enum.TurretControllerVisuals.ControlPanel:
+ enum.PowerDeviceVisualLayers.Powered:
+ -1: { state: safe }
+ 0: { state: stun }
+ 1: { state: lethal }
+ - type: StationAiWhitelist
+ - type: AccessReader
+ access: [["Security"]]
+ - type: TurretTargetSettings
+ exemptAccessLevels:
+ - Security
+ - Borg
+ - BasicSilicon
+ - type: DeployableTurretController
+ accessGroups:
+ - Cargo
+ - Command
+ - Engineering
+ - General
+ - Medical
+ - Research
+ - Security
+ - Service
+ - Silicon
+ accessLevels:
+ - Armory
+ - Atmospherics
+ - Bar
+ - BasicSilicon
+ - Borg
+ - Brig
+ - Detective
+ - Captain
+ - Cargo
+ - Chapel
+ - Chemistry
+ - ChiefEngineer
+ - ChiefMedicalOfficer
+ - Command
+ - Cryogenics
+ - Engineering
+ - External
+ - HeadOfPersonnel
+ - HeadOfSecurity
+ - Hydroponics
+ - Janitor
+ - Kitchen
+ - Lawyer
+ - Maintenance
+ - Medical
+ - Quartermaster
+ - Research
+ - ResearchDirector
+ - Salvage
+ - Security
+ - Service
+ - Theatre
+ - type: DeviceList
+ isAllowList: true
+ - type: DeviceNetwork
+ deviceNetId: Wired
+ receiveFrequencyId: Turret
+ transmitFrequencyId: TurretControl
+ sendBroadcastAttemptEvent: true
+ prefix: device-address-prefix-console
+ - type: DeviceNetworkRequiresPower
+ - type: WiredNetworkConnection
+ - type: ActivatableUI
+ key: enum.DeployableTurretControllerUiKey.Key
+ - type: ActivatableUIRequiresPower
+ - type: UserInterface
+ interfaces:
+ enum.DeployableTurretControllerUiKey.Key:
+ type: TurretControllerWindowBoundUserInterface
+ enum.WiresUiKey.Key:
+ type: WiresBoundUserInterface
+ - type: WiresPanel
+ - type: WiresVisuals
+ - type: Wires
+ boardName: wires-board-name-turret-controls
+ layoutId: TurretControls
+ - type: Lock
+ locked: true
+ unlockOnClick: false
+ - type: LockedWiresPanel
+ - type: ApcPowerReceiver
+ - type: ExtensionCableReceiver
+ - type: Electrified
+ enabled: false
+ usesApcPower: true
+ #- type: ContainerFill - Will be added in a later PR
+ # containers:
+ # board:
+ # - WeaponEnergyTurretStationControlPanelElectronics
+ #- type: Construction - Will be added in a later PR
+ # graph: WeaponEnergyTurretControlPanel
+ # node: finish
+
+- type: entity
+ parent: WeaponEnergyTurretStationControlPanel
+ id: WeaponEnergyTurretAIControlPanel
+ name: AI sentry turret control panel
+ description: A wall-mounted interface that allows a local artifical intelligence to adjust the operational parameters of linked sentry turrets.
+ components:
+ - type: AccessReader
+ access: [["StationAi"], ["ResearchDirector"]]
+ #- type: ContainerFill - Will be added in a later PR
+ # containers:
+ # board:
+ # - WeaponEnergyTurretAIControlPanelElectronics
+ - type: DeviceNetwork
+ receiveFrequencyId: TurretAI
+ transmitFrequencyId: TurretControlAI
+ - type: TurretTargetSettings
+ exemptAccessLevels:
+ - BasicSilicon
+ - Borg
+ - type: DeployableTurretController
+ accessGroups:
+ - Silicon
+ accessLevels:
+ - BasicSilicon
+ - Borg