Files
wwdpublic/Content.Client/TurretController/TurretControllerWindow.xaml.cs
Solaris a64b5164e3 Port AI Sentry Turrets (#1990)
# Description

I am trying to port over the AI turrets being implemented into wizden
made by chromiumboy. It looks fantastic and would like to port this now
and work on any issues that might show.

---

# Original PRs
https://github.com/space-wizards/space-station-14/issues/35223

https://github.com/space-wizards/space-station-14/pull/35025
https://github.com/space-wizards/space-station-14/pull/35031
https://github.com/space-wizards/space-station-14/pull/35058
https://github.com/space-wizards/space-station-14/pull/35123
https://github.com/space-wizards/space-station-14/pull/35149
https://github.com/space-wizards/space-station-14/pull/35235
https://github.com/space-wizards/space-station-14/pull/35236
---

# TODO

- [x] Port all related PRs to EE.
- [x] Patch any bugs with turrets or potential issues.
- [x] Cleanup my shitcode or changes.
---

# Changelog

🆑
- add: Added recharging sentry turrets, one is AI-based or the other is
Sec can make.
- add: The sentry turrets can be made after researching in T3 arsenal.
The boards are made in the sec fab.
- add: New ID permissions for borgs and minibots for higher turret
options.
- tweak: Turrets stop shooting after someone goes crit.

---------

Co-authored-by: Nathaniel Adams <60526456+Nathaniel-Adams@users.noreply.github.com>

(cherry picked from commit 209d0537401cbda448a03e910cca9a898c9d566f)
2025-03-21 18:28:40 +03:00

502 lines
17 KiB
C#

using Content.Client.Resources;
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.Player;
using Robust.Shared.Prototypes;
using System.Linq;
using System.Numerics;
namespace Content.Client.TurretController;
[GenerateTypedNameReferences]
public sealed partial class TurretControllerWindow : BaseWindow
{
[Dependency] private IEntityManager _entManager = default!;
[Dependency] private IPrototypeManager _protoManager = default!;
[Dependency] private IPlayerManager _playerManager = default!;
private readonly IResourceCache _cache;
private readonly AccessReaderSystem _accessReaderSystem;
private EntityUid? _owner;
private int _tabIndex = 0;
// Button groups
private readonly ButtonGroup _armamentButtons = new();
private readonly ButtonGroup _accessGroupsButtons = new();
// Temp values
private List<CheckBox> _checkBoxes = new();
private HashSet<AccessLevelPrototype> _accessLevelsForTab = new();
private List<AccessLevelEntry> _accessLevelEntries = new();
// Events
private event Action<int>? OnAccessGroupChangedEvent;
public event Action<HashSet<ProtoId<AccessLevelPrototype>>, bool>? OnAccessLevelsChangedEvent;
public event Action<int>? OnArmamentSettingChangedEvent;
// Colors
private Color[] _themeColors = [Color.FromHex("#33e633"), Color.FromHex("#dfb827"), Color.FromHex("#da2a2a")];
public TurretControllerWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_cache = IoCManager.Resolve<IResourceCache>();
_accessReaderSystem = _entManager.System<AccessReaderSystem>();
CloseButton.OnPressed += _ => Close();
XamlChildren = ContentsContainer.Children;
OnAccessGroupChangedEvent += OnAccessGroupChanged;
var smallFont = _cache.NotoStack(size: 8);
Footer.FontOverride = smallFont;
}
private void Initialize()
{
if (_owner == null)
return;
// Set up armament buttons
SafeButton.OnToggled += args => OnArmamentButtonPressed(SafeButton, -1);
StunButton.OnToggled += args => OnArmamentButtonPressed(StunButton, 0);
LethalButton.OnToggled += args => OnArmamentButtonPressed(LethalButton, 1);
SafeButton.Group = _armamentButtons;
StunButton.Group = _armamentButtons;
LethalButton.Group = _armamentButtons;
SafeButton.Label.AddStyleClass("ConsoleText");
StunButton.Label.AddStyleClass("ConsoleText");
LethalButton.Label.AddStyleClass("ConsoleText");
// Refresh UI
RefreshLinkedTurrets(new());
if (_entManager.TryGetComponent<DeployableTurretControllerComponent>(_owner, out var turretController))
UpdateTheme(turretController.ArmamentState);
if (_entManager.TryGetComponent<TurretTargetSettingsComponent>(_owner, out var turretTargetSettings))
RefreshAccessControls(turretTargetSettings.ExemptAccessLevels);
}
private void OnArmamentButtonPressed(Button pressedButton, int index)
{
UpdateTheme(index);
OnArmamentSettingChangedEvent?.Invoke(index);
}
private void UpdateTheme(int index)
{
switch (index)
{
case -1:
SafeButton.Pressed = true;
break;
case 0:
StunButton.Pressed = true;
break;
case 1:
LethalButton.Pressed = true;
break;
}
var canInteract = IsLocalPlayerAllowedToInteract();
SafeButton.Disabled = !SafeButton.Pressed && !canInteract;
StunButton.Disabled = !StunButton.Pressed && !canInteract;
LethalButton.Disabled = !LethalButton.Pressed && !canInteract;
var shiftedIndex = index + 1;
if (shiftedIndex >= 0 && shiftedIndex < _themeColors.Length)
ContentsContainer.Modulate = _themeColors[shiftedIndex];
}
public void SetOwner(EntityUid owner)
{
_owner = owner;
Initialize();
}
public void UpdateState(DeployableTurretControllerBoundInterfaceState state)
{
if (_entManager.TryGetComponent<DeployableTurretControllerComponent>(_owner, out var turretController))
UpdateTheme(turretController.ArmamentState);
if (_entManager.TryGetComponent<TurretTargetSettingsComponent>(_owner, out var turretTargetSettings))
RefreshAccessControls(turretTargetSettings.ExemptAccessLevels);
RefreshLinkedTurrets(state.TurretStates);
}
public void UpdateMessage(DeployableTurretControllerBoundInterfaceMessage message)
{
RefreshLinkedTurrets(message.TurretStates);
}
public void RefreshLinkedTurrets(List<(string, string)> turretStates)
{
var turretCount = turretStates.Count;
var hasTurrets = turretCount > 0;
NoLinkedTurretsText.Visible = !hasTurrets;
LinkedTurretsContainer.Visible = hasTurrets;
LinkedTurretsContainer.RemoveAllChildren();
foreach (var turretState in turretStates)
{
var box = new BoxContainer()
{
HorizontalExpand = true,
};
var label = new Label()
{
Text = Loc.GetString("turret-controls-window-turret-status", ("device", turretState.Item1), ("status", Loc.GetString(turretState.Item2))),
HorizontalAlignment = HAlignment.Left,
Margin = new Thickness(10f, 0f, 10f, 0f),
HorizontalExpand = true,
SetHeight = 20f,
};
label.AddStyleClass("ConsoleText");
box.AddChild(label);
LinkedTurretsContainer.AddChild(box);
}
TurretStatusHeader.Text = Loc.GetString("turret-controls-window-turret-status-label", ("count", turretCount));
}
public void RefreshAccessControls(HashSet<ProtoId<AccessLevelPrototype>> exemptAccessLevels)
{
if (_owner == null)
return;
if (!_entManager.TryGetComponent<DeployableTurretControllerComponent>(_owner, out var turretControls))
return;
var canInteract = IsLocalPlayerAllowedToInteract();
// Create a list of known access groups with which to populate the UI
var groupedAccessLevels = new Dictionary<AccessGroupPrototype, HashSet<AccessLevelPrototype>>();
foreach (var accessGroup in turretControls.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<AccessGroupPrototype>("General", out var generalAccessProto))
groupedAccessLevels.TryAdd(generalAccessProto, new());
// Assign known access levels with their associated groups
foreach (var accessLevel in turretControls.AccessLevels)
{
if (!_protoManager.TryIndex(accessLevel, out var accessLevelProto))
continue;
IEnumerable<AccessGroupPrototype> associatedGroups =
groupedAccessLevels.Keys.Where(x => x.Tags.Contains(accessLevelProto.ID) == true);
if (!associatedGroups.Any() && generalAccessProto != null)
groupedAccessLevels[generalAccessProto].Add(accessLevelProto);
else
{
foreach (var group in associatedGroups)
groupedAccessLevels[group].Add(accessLevelProto);
}
}
// Remove access groups that have no assigned access levels
foreach (var (group, accessLevels) in groupedAccessLevels)
{
if (accessLevels.Count == 0)
groupedAccessLevels.Remove(group);
}
// Did something go wrong...?
if (groupedAccessLevels.Count == 0)
{
AccessGroupList.DisposeAllChildren();
AccessLevelGrid.DisposeAllChildren();
return;
}
// Adjust the current tab index so it remains in range
if (_tabIndex >= groupedAccessLevels.Count)
_tabIndex = groupedAccessLevels.Count - 1;
// Reorder the access groups alphabetically
var orderedAccessGroups = groupedAccessLevels.Keys.OrderBy(x => x.GetAccessGroupName()).ToList();
// Remove excess group access buttons from the UI
while (AccessGroupList.ChildCount > orderedAccessGroups.Count)
AccessGroupList.RemoveChild(orderedAccessGroups.Count - 1);
// Add missing group access buttons to the UI
while (AccessGroupList.ChildCount < orderedAccessGroups.Count)
{
var monotoneButton = new MonotoneButton
{
ToggleMode = true,
};
AccessGroupList.AddChild(monotoneButton);
// Add button styling
monotoneButton.Label.AddStyleClass("ConsoleText");
monotoneButton.Label.HorizontalAlignment = HAlignment.Left;
monotoneButton.Group = _accessGroupsButtons;
var childIndex = AccessGroupList.ChildCount - 1;
if (orderedAccessGroups.Count > 1)
{
if (childIndex == 0)
monotoneButton.Shape = MonotoneButtonShape.OpenLeft;
else if (orderedAccessGroups.Count > 1 && childIndex == (orderedAccessGroups.Count - 1))
monotoneButton.Shape = MonotoneButtonShape.OpenRight;
else
monotoneButton.Shape = MonotoneButtonShape.OpenBoth;
}
// Add button events
monotoneButton.OnPressed += _ =>
{
OnAccessGroupChangedEvent?.Invoke(monotoneButton.GetPositionInParent());
};
}
// Update the group access buttons
for (int i = 0; i < orderedAccessGroups.Count; i++)
{
if (AccessGroupList.GetChild(i) is not Button { } accessGroupButton)
continue;
var accessGroup = orderedAccessGroups[i];
var prefix = groupedAccessLevels[accessGroup].Any(x => exemptAccessLevels.Contains(x)) ? "»" : " ";
accessGroupButton.Text = Loc.GetString("turret-controls-window-access-group-label",
("prefix", prefix), ("label", accessGroup.GetAccessGroupName()));
accessGroupButton.Pressed = _tabIndex == orderedAccessGroups.IndexOf(accessGroup);
}
// Get the access levels associated with the current tab
_accessLevelsForTab = groupedAccessLevels[orderedAccessGroups[_tabIndex]];
_accessLevelsForTab = _accessLevelsForTab.OrderBy(x => x.GetAccessLevelName()).ToHashSet();
// Remove excess access level buttons from the UI
// Note: if _accessLevelsForTab is length 'n', AccessLevelGrid should have 'n + 1' children at the end
while (AccessLevelGrid.ChildCount > (_accessLevelsForTab.Count + 1))
{
var index = AccessLevelGrid.ChildCount - 1;
if (AccessLevelGrid.GetChild(AccessLevelGrid.ChildCount - 1) is AccessLevelEntry { } accessLevelEntry)
_accessLevelEntries.Remove(accessLevelEntry);
AccessLevelGrid.RemoveChild(index);
}
// Add an 'all' checkbox as the first child of the list if it hasn't been initalized yet
// Toggling this checkbox on will mark all other boxes below it on/off
if (AccessLevelGrid.ChildCount == 0)
{
var checkBox = new MonotoneCheckBox
{
Text = Loc.GetString("turret-controls-window-all-checkbox"),
Margin = new Thickness(0, 0, 0, 3),
ToggleMode = true,
ReservesSpace = false,
};
AccessLevelGrid.AddChild(checkBox);
// Add checkbox styling
checkBox.Label.AddStyleClass("ConsoleText");
// Add checkbox events
checkBox.OnPressed += args =>
{
SetCheckBoxPressedState(_checkBoxes, checkBox.Pressed);
var accessLevels = new HashSet<ProtoId<AccessLevelPrototype>>();
foreach (var accessLevel in _accessLevelsForTab)
accessLevels.Add(accessLevel);
OnAccessLevelsChangedEvent?.Invoke(accessLevels, checkBox.Pressed);
};
}
// Hide the 'all' checkbox if the tab has only one access level
var allCheckBoxVisible = _accessLevelsForTab.Count > 1;
// Did something go wrong...?
if (AccessLevelGrid.GetChild(0) is not CheckBox { } allCheckBox)
return;
allCheckBox.Visible = allCheckBoxVisible;
allCheckBox.Disabled = !canInteract;
// Add any remaining missing access level buttons to the UI
while (AccessLevelGrid.ChildCount < (_accessLevelsForTab.Count + 1))
{
var accessLevelEntry = new AccessLevelEntry();
AccessLevelGrid.AddChild(accessLevelEntry);
_accessLevelEntries.Add(accessLevelEntry);
// 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 => (CheckBox)x.CheckBox));
OnAccessLevelsChangedEvent?.Invoke
(new HashSet<ProtoId<AccessLevelPrototype>>() { accessLevelEntry.AccessLevel }, accessLevelEntry.CheckBox.Pressed);
};
}
// Update the access levels buttons' appearance
for (int i = 0; i < _accessLevelEntries.Count; i++)
{
var accessLevel = _accessLevelsForTab.ElementAt(i);
var accessLevelEntry = _accessLevelEntries[i];
accessLevelEntry.AccessLevel = accessLevel;
accessLevelEntry.CheckBox.Text = accessLevel.GetAccessLevelName();
accessLevelEntry.CheckBox.Pressed = exemptAccessLevels.Contains(accessLevel);
var isEndOfList = i == (_accessLevelEntries.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;
accessLevelEntry.CheckBox.Disabled = !canInteract;
}
// Press the 'all' checkbox if all others are pressed
allCheckBox.Pressed = AreAllCheckBoxesPressed(_accessLevelEntries.Select(x => x.CheckBox));
}
private bool AreAllCheckBoxesPressed(IEnumerable<CheckBox> checkBoxes)
{
foreach (var checkBox in checkBoxes)
{
if (!checkBox.Pressed)
return false;
}
return true;
}
private void SetCheckBoxPressedState(IEnumerable<CheckBox> checkBoxes, bool pressed)
{
foreach (var checkBox in checkBoxes)
checkBox.Pressed = pressed;
}
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
{
return DragMode.Move;
}
private void OnAccessGroupChanged(int newTabIndex)
{
if (newTabIndex == _tabIndex)
return;
_tabIndex = newTabIndex;
if (_entManager.TryGetComponent<TurretTargetSettingsComponent>(_owner, out var turretTargetSettings))
RefreshAccessControls(turretTargetSettings.ExemptAccessLevels);
}
private bool IsLocalPlayerAllowedToInteract()
{
if (_owner == null || _playerManager.LocalSession?.AttachedEntity == null)
return false;
return _accessReaderSystem.IsAllowed(_playerManager.LocalSession.AttachedEntity.Value, _owner.Value);
}
private sealed class AccessLevelEntry : BoxContainer
{
public ProtoId<AccessLevelPrototype> AccessLevel = default!;
public MonotoneCheckBox CheckBox;
public LineRenderer CheckBoxLink;
public AccessLevelEntry()
{
HorizontalExpand = true;
var lines = new List<(Vector2, Vector2)>()
{
(new Vector2(0,0), new Vector2(0,0)),
(new Vector2(0,0), new Vector2(0,0))
};
CheckBoxLink = new LineRenderer(lines)
{
SetWidth = 22,
VerticalExpand = true,
Margin = new Thickness(0, -1),
ReservesSpace = false,
};
AddChild(CheckBoxLink);
CheckBox = new MonotoneCheckBox
{
ToggleMode = true,
Margin = new Thickness(0f, 0f, 0f, 3f),
};
AddChild(CheckBox);
CheckBox.Label.AddStyleClass("ConsoleText");
}
public void UpdateCheckBoxLink(List<(Vector2, Vector2)> lines)
{
CheckBoxLink.Lines = lines;
}
}
}